Files
Oxicloud/AGENTS.md
T
cjw b7640e9be4
CI / changes (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy Docs / build (push) Has been cancelled
Docker Build and Test / Build and Test Docker Image (push) Has been cancelled
Docker Publish (release, main, dry-run) / Pre-publish Tests (push) Has been cancelled
CI / Frontend — svelte-check, ESLint, Stylelint, Prettier (push) Has been cancelled
CI / Message-bus spec — AsyncAPI + TypeScript DTO drift (push) Has been cancelled
CI / Migration ordering (new migrations postdate target branch) (push) Has been cancelled
CI / Rustfmt (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Wasm — fmt + clippy (push) Has been cancelled
CI / Wasm — release tests (push) Has been cancelled
CI / Plugins — fixtures + runtime tests (push) Has been cancelled
CI / Server Unit and Functionnal Tests (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / API, WebDAV & OIDC tests (push) Has been cancelled
CI / Bundled-assets binary — embed + SPA-serve integration (push) Has been cancelled
CI / WebDAV RFC 4918 — litmus (59/59) (push) Has been cancelled
CI / CalDAV + CardDAV — python-caldav (push) Has been cancelled
CI / Frontend end-to-end tests (via Playwright) (push) Has been cancelled
Deploy Docs / deploy (push) Has been cancelled
Docker Publish (release, main, dry-run) / Build & Push Multi-Arch (push) Has been cancelled
x
2026-09-20 00:28:52 +08:00

390 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md
This file provides guidance to coding agents (Claude Code, Codex, Cursor, Aider, …) working with this repository. Claude Code reads it via `@AGENTS.md` in `CLAUDE.md`.
# Purpose — what OxiCloud is, and what it is not
Read this before designing anything. Most "should we…" questions are answered by
the scale target rather than by taste.
- **Open source, and it stays that way.** MIT (`LICENSE`). This is a constraint on
what you may add, not a footnote: dependencies must be license-compatible — a
GPL/AGPL crate or npm package would force the whole project to relicense, and
that is not on the table. No feature may be gated behind a licence key or an
"enterprise edition", and nothing core may hard-depend on a proprietary service
or SDK. Vendored frontend assets (`frontend/static/vendors/`) carry the same
rule; record the licence when vendoring.
- **Self-hosted**, for an individual or an enterprise. The operator is not an SRE
team: defaults must be safe, failures loud, and nothing may silently depend on a
cloud service.
- **Target scale is up to ~10k users.** Design against that number in both
directions. Do not build million-user machinery (sharding, eventual consistency,
service decomposition) for a load that will never arrive; equally, never ship
anything O(users) per request, or a table scan that is fine at 50 users and
fatal at 10k.
- **Not a mass hoster.** OxiCloud does not claim to serve millions of users on one
deployment, and trade-offs should not pretend otherwise.
- **Decentralised by intent.** Many instances federating beats one large instance —
OpenCloudMesh is one route. Prefer designs that survive "this is one of many
instances" over ones assuming a single authoritative deployment.
Targets:
- **Feature ambition: Google Workspace / Office 365.** Breadth of capability is a
goal, not scope creep.
- **Collaboration is the main feature axis.** OxiCloud is not a personal backup
drive that happens to have sharing bolted on — sharing, shared drives, grants,
co-editing (WOPI) and live updates are the product. When choosing what to build
or how to build it, the multi-user case is the primary one, not the case to
generalise to later. A feature that works only for a single owner is unfinished.
- **Customer target: NextCloud users.** Hence the NextCloud-compatible API surface
(`/remote.php`, `/ocs`, `/status.php`) — compatibility is a feature, and breaking
it costs adopters.
## Design axes
Four things decide an open design question. **Security and resilience are
absolute** — they are not traded against anything. Performance is measured against
the 10k target. Privacy is a direction with a stated endpoint.
- **Resilience.** This is a storage product: **no data loss, no data corruption,
ever.** Anything that can silently drop or alter bytes is a top-severity defect,
not a trade-off. In practice that means: a job that skips work must never report
success (pause at a cursor instead — `docs/plan/jobs-handling-recoverable-error.md`);
a read failure is never proof that data is absent; content-addressing and
ref-counting are load-bearing, not decoration; and consistency checks are
discovery-only unless repair is explicitly requested.
- **Security.** Prefer deny-by-default over assert-later; a guarantee enforced by
the type system or the router beats one a reviewer must remember. AuthZ lives in
the service layer, never in handlers. See `src/AGENTS.md` § AuthZ enforcement
points.
- **Performance.** Measure against 10k users, not a dev instance. The hot paths are
listing, thumbnails and auth — a per-row query or an extra round trip there is a
real regression even when it looks harmless.
- **Privacy.** When the backend belongs to a third party (S3, Azure), encryption at
rest is a *should-have*; **end-to-end encryption is the target.** Designs that
assume the server can always read plaintext will have to be undone — the `Vault`
drive kind is reserved for the E2E case.
Where two conflict, resilience and security win, and the cost is documented.
# Architecture
This project is split into two parts:
- `/src` — OxiCloud Backend server in **Rust**
- `/frontend` — OxiCloud Frontend: a **SvelteKit (Svelte 5) + TypeScript** single-page app built with Vite
> The original vanilla-JS/CSS frontend still lives in `/static` and is retained
> during the migration, but new frontend work goes in `/frontend`. Vite builds
> the SvelteKit app to `static-dist/`, which the Rust web layer serves in
> release.
# Backend part
## Backend Build & Dev Commands
```bash
cargo build # Dev build
cargo build --release # Optimized release build
cargo run # Run server (port 8086)
cargo test --workspace # Run all tests (~208)
cargo test <test_name> # Run a single test by name
cargo test --features test_utils # Run tests that use mockall mocks
cargo clippy -- -D warnings # Lint (zero warnings policy)
cargo fmt --all --check # Format check
cargo fmt --all # Auto-format
RUST_LOG=debug cargo run # Run with debug logging
cargo run --bin generate-openapi # Regenerate resources/gen/openapi.json
```
A `justfile` is available for common tasks (`just --list` to see all). Key recipes: `just check` (fmt + clippy), `just test`, `just openapi`.
Requires **Rust 1.93+** (edition 2024) and **PostgreSQL 13+** (with `pg_trgm` and `ltree` extensions).
Database setup: `docker compose up -d postgres` — schema is applied automatically via sqlx migrations on app startup. Migration files live in `migrations/`. For local dev, set `DATABASE_URL` in `.env` (see `example.env`).
## Backend Pre-commit checks
Always run these before committing, in this order:
```bash
cargo fmt --all # Auto-format
cargo clippy --all-features --all-targets -- -D warnings # Lint (must pass with zero warnings)
```
CI enforces both — commits that fail either check will not merge.
## Backend Pre-push checks
When the change touches server code (anything under `src/`, `migrations/`,
`Cargo.toml`, or `tests/`), run the full suite locally before pushing — CI
is slower and a red CI run after a public push wastes maintainer attention:
```bash
just check # cargo fmt --check + cargo clippy -D warnings
just test # cargo test --workspace
just test-integration # cargo test --tests with integration cfg
just api-test # Hurl API + WebDAV scenarios
```
Run them in that order — `just check` is fastest and catches the most
common issues first. Don't push if any step fails; investigate locally.
## Backend Architecture
Hexagonal / Clean Architecture with four layers. Dependencies point inward only.
### Layer structure (`src/`)
- **`domain/`** — Core business entities (`entities/`) and repository trait definitions (`repositories/`). Pure Rust, no framework dependencies. Entity types: `File`, `Folder`, `User`, `Calendar`, `CalendarEvent`, `Contact`, `Share`, `TrashedItem`, `Session`, `DeviceCode`, `AppPassword`.
- **`application/`** — Use cases and orchestration.
- `ports/` — Trait definitions (inbound/outbound) for storage, auth, caching, compression, dedup, thumbnails, chunked uploads, CalDAV/CardDAV, etc. This is the hexagonal "ports" layer.
- `services/` — Use case implementations (`FileManagementService`, `FolderService`, `ShareService`, `TrashService`, `CalendarService`, `ContactService`, `SearchService`, `BatchOperations`, etc.).
- `adapters/` — CalDAV/CardDAV protocol adapters (iCalendar/vCard parsing).
- `dtos/` — Data transfer objects for API boundaries.
- **`infrastructure/`** — Concrete implementations of ports.
- `repositories/pg/` — All PostgreSQL repository implementations (via `sqlx`). Uses `auth` schema for users/sessions, `storage` schema for files/folders/blobs (content-addressable dedup with ltree paths).
- `services/` — JWT, password hashing (Argon2), OIDC, compression, thumbnails, chunked uploads, WOPI discovery, WebDAV locking, file content caching (moka).
- `adapters/` — CalDAV/CardDAV storage adapters bridging domain traits to PG.
- `db.rs` — Dual connection pool setup (user pool + maintenance pool).
- **`interfaces/`** — HTTP layer (Axum).
- `api/handlers/` — REST API handlers for files, folders, auth, admin, search, shares, WebDAV, CalDAV, CardDAV, WOPI, chunked uploads, batch operations.
- `api/routes.rs` — Route registration, splits protected vs public routes.
- `nextcloud/` — NextCloud-compatible API (WebDAV, OCS, login flow v2, trashbin) with Basic Auth middleware.
- `middleware/` — Auth (JWT validation), CSRF, rate limiting.
- `web/` — Static file serving.
- **`common/`** — Cross-cutting concerns.
- `di.rs` — `AppServiceFactory` builds all services and produces `AppState` (the central DI container passed to Axum). This is the composition root.
- `config.rs` — `AppConfig::from_env()` loads all `OXICLOUD_*` env vars.
### Key patterns
- **DI via `AppState`**: All services are `Arc`-wrapped and assembled in `common/di.rs`. `AppState` is wrapped in `Arc` and passed as Axum state. Many services are `Option<Arc<T>>` because they depend on features being enabled (auth, WOPI, trash, etc.).
- **Content-addressable storage**: Files use BLAKE3 blob dedup. `storage.file_blobs` stores content; `storage.file_metadata` references blobs with ref-counting. See `file_blob_write_repository.rs` and `file_blob_read_repository.rs`.
- **ltree paths**: Folder hierarchy uses PostgreSQL `ltree` for efficient subtree queries (recursive copies, moves, searches).
- **Dual DB pools**: `DbPools` in `infrastructure/db.rs` separates user-facing queries from maintenance/background tasks to prevent starvation.
- **Feature flags**: Major features (auth, trash, search, sharing, quotas) are toggled via `OXICLOUD_ENABLE_*` env vars in `FeaturesConfig`.
- **UUID columns**: All ID columns use native PostgreSQL `UUID` type. SQL queries must use `::uuid` casts when passing string parameters to UUID columns.
### Database schemas
- `auth` schema: `users`, `sessions`, `app_passwords`, `device_codes`, `admin_settings`
- `storage` schema: `folders`, `file_metadata`, `file_blobs`, `trash`, `shares`, `favorites`, `recent_items`, `nextcloud_object_ids`
- `caldav` schema: `calendars`, `calendar_events`
- `carddav` schema: `address_books`, `contacts`, `contact_groups`, `contact_group_members`
Schema definition: `migrations/` (sqlx migrations, applied on startup)
### Protocol support
The server exposes multiple protocol interfaces simultaneously:
- REST API under `/api/`
- WebDAV at `/webdav/` (RFC 4918)
- CalDAV at `/caldav/`
- CardDAV at `/carddav/`
- NextCloud-compatible API at `/remote.php/`, `/ocs/`, `/status.php`
- WOPI at `/wopi/` (when enabled)
- Well-known discovery at `/.well-known/caldav` and `/.well-known/carddav`
### Test organization
Tests are primarily `#[cfg(test)]` modules within source files (~36 files have inline tests). Dedicated test files exist at `*_test.rs` alongside their source. The `test_utils` feature flag enables `mockall` mock generation for trait-heavy testing. No separate `tests/` directory.
### Code duplication
Never duplicate logic across handlers or services. If the same behaviour is needed in more than one place, extract it into a shared function, method, or service before writing the second callsite. Preferred homes by layer:
- Cross-handler request logic → method on `CoreServices` or `AppState` (`common/di.rs`)
- Reusable infrastructure behaviour → method on the relevant service struct
- Shared port behaviour → default method on the trait
### Authorization (AuthZ)
**AuthZ is enforced exclusively in the application service layer, never in handlers.** All permission checks go through `AuthorizationEngine` (port: `application/ports/authorization_ports.rs`) via service methods named with the `_with_perms` suffix. HTTP handlers (REST, WebDAV, NextCloud, CalDAV, CardDAV) authenticate the caller and pass `caller_id` into the service — they MUST NOT perform their own ownership/permission checks. The authentication middleware extracts the caller; the service decides if the action is allowed.
This rule prevents drift between layers and ensures every code path goes through the same policy. New service methods that touch a user-scoped resource must take `caller_id: Uuid` and call `authz.require(...)` before any read or mutation.
### Audit logging for denials and rejections
**Every permission denial or auth rejection MUST emit a structured audit log line before returning the error.** Without one, security-relevant outcomes are invisible to operators and incident response loses its primary signal.
The convention:
```rust
tracing::info!(
target: "audit",
event = "<domain>.<outcome>", // e.g. "authz.denied", "auth.login_rejected",
// "magic_link.redemption_rejected",
// "user_profile.rejected"
reason = "<short_key>", // stable machine-readable key for filtering
// (e.g. "bad_password", "expired", "no_visibility_path")
// …structured fields naming the actors / targets…
caller_id = %caller_id, // or subject_id, user_id, granted_by, etc.
target_id = %target_id, // or resource_id, subject_id, etc.
"👮🏻‍♂️ human-readable message: …", // helpful for live tailing, do not parse
);
```
Rules:
- **`target: "audit"`** routes the line to the audit channel (separable from operational `oxicloud::*` debug noise).
- **`event`** uses the dotted form `<domain>.<verb_past_tense>` and stays stable — log aggregators key off it.
- **`reason`** is a machine-readable enum-style key. Don't reword across releases. New denial cause → new `reason` value, never repurpose an existing one.
- **Structured fields** carry every actor/target involved (`caller_id`, `target_id`, `resource_id`, `subject_id`, role, is_external flag, etc.). Request id and client IP come from the request-scope span automatically — don't duplicate them.
- **Anti-enumeration is preserved.** Returning `NotFound` to the caller while logging the real reason internally is the canonical pattern (e.g. `user_profile.rejected` with `reason = "external_caller_no_relationship"` returns 404, never 403). Operators see the truth; the attacker sees the same response shape regardless of whether the user exists.
- **Success paths stay quiet** by default — every authorized request would otherwise flood the log. Use `tracing::debug!` with `target: "oxicloud::authz"` (or similar) when a low-volume granted-trace helps debugging. Reserve `tracing::info!(target: "audit", …)` for outcomes worth surfacing in security reviews.
Canonical examples to mirror: `authz.denied` in `application/ports/authorization_ports.rs::require`, `auth.login_rejected` and `magic_link.redemption_rejected` and `user_profile.rejected` in `application/services/auth_application_service.rs`.
# Frontend part
The frontend is a **SvelteKit** single-page app (Svelte 5 + TypeScript, Vite,
`adapter-static`) under `frontend/`. Vite builds it to `static-dist/`, which the
Rust web layer serves in release (unmatched client routes fall back to the SPA
shell); `PROFILE=dev` serves the unbuilt source. The legacy vanilla frontend in
`static/` is retained for now but is **not** where new work goes.
## Frontend Build & Dev Commands
Run from `frontend/` (or via the `fe-*` justfile recipes from the repo root):
```bash
npm ci # install deps (just fe-install)
npm run dev # Vite dev server + HMR (just fe-dev) — backend must run on :8086
npm run build # build the SPA → static-dist/ (just fe-build)
npm run check # svelte-check + ESLint + Stylelint + Prettier (just fe-check)
npm run test:unit # Vitest (just fe-test)
npm run format # prettier --write .
```
`just dev` runs the backend and the Vite dev server together. CI uses **Node 26**; Node 24+ works locally.
## Frontend Architecture (`frontend/src/`)
- `routes/` — SvelteKit pages (`+page.svelte`, `+layout.svelte`), one folder per route (`files/[...path]`, `photos`, `shared`, `trash`, `admin`, `s/[token]`, …).
- `lib/components/` — reusable Svelte components (`AppShell`, `PhotoLightbox`, `ShareDialog`, `Modal`, …).
- `lib/api/` — HTTP layer: `client.ts` (`apiFetch`/`apiJson`), `csrf.ts` (`getCsrfHeaders`), `types.ts` (API DTO types — map the backend here), and `endpoints/*.ts` (one module per area: files, folders, photos, people, grants, …).
- `lib/stores/` — global reactive state as `*.svelte.ts` rune stores (`session`, `ui`, `theme`, `dialogs`).
- `lib/composables/` — reusable rune logic (`useSelection`, `useOwnerCache`).
- `lib/i18n/` — bespoke reactive i18n; `t(key, [params], fallback)` reads `frontend/static/locales/*.json` (16 locales) with `{{param}}` interpolation and an English fallback.
- `lib/icons/` — `Icon.svelte` + a generated Font Awesome `registry.ts`.
- `lib/utils/`, `lib/vendor/` — shared helpers and minimal typings/loaders for vendored libs.
- `lib/styles/` — global CSS (`app.css`, `base/`, `ported/`).
- `static/` — served at the web root: `locales/`, `vendors/` (maplibre-gl, pmtiles, hash-wasm), `workers/` (deltaWorker), optional `basemaps/`.
## Code conventions
### Svelte / TypeScript
- **Svelte 5 runes** — `$state`, `$derived`, `$props`, `$effect`, `$bindable`. No legacy `export let` for new components.
- **TypeScript everywhere** (`lang="ts"` in components). **No `any`** — `typescript-eslint` recommended is enforced; prefer precise types, `unknown` + narrowing, or a minimal declared interface for an untyped global (see `lib/vendor/maplibre.ts`).
- ES Modules; `camelCase` for variables/functions, `PascalCase` for components/classes; `const`/`let`, never `var`.
- API DTO shapes live in `lib/api/types.ts`; call the backend through `lib/api/endpoints/*` — don't bare-`fetch` `/api` from components.
### Code duplication
Never duplicate logic across modules/components. Extract shared behaviour:
- DOM/UI helpers → `lib/utils/`
- API wrappers → the relevant `lib/api/endpoints/*` module
- Cross-component state/logic → a `lib/stores/*.svelte.ts` store or a `lib/composables/*`
- Shared markup → a component (e.g. `PhotoLightbox` is shared by the photos grid, People and Places)
### CSS
- BEM methodology for class names (`.block__element--modifier`).
- Component styles live in the component's scoped `<style>`; cross-cutting tokens/styles in `lib/styles/`.
- **All colors must use `var(--*)`** — no raw hex, rgb, or named colors (Stylelint enforces `function-disallowed-list`); define tokens in `lib/styles/base/variables.css`.
- Mobile-first: media queries expand, they don't restrict.
- Dark mode keys off `<html data-color-scheme="dark">`.
## Frontend Pre-commit checks
Always run from `frontend/` before committing:
```bash
npm run check # svelte-kit sync && svelte-check && eslint . && stylelint "src/**/*.{css,svelte}" && prettier --check .
npm run test:unit # Vitest
```
CI runs the same `npm run check` (plus Vitest) — commits that fail will not merge.
# What agents must NOT do
- Edit `Cargo.lock` or `frontend/package-lock.json` by hand
- Introduce a different JS framework (React, Vue, etc.) — the frontend is SvelteKit/Svelte 5
- Add a heavy runtime npm dependency without discussion — prefer vendoring + lazy-loading under `frontend/static/vendors/` (see maplibre-gl / pmtiles)
- Use `any` in TypeScript
- Leave debug `console.log` statements in code
- Use raw color values in CSS — always use CSS custom properties
- Commit without passing all linters (`npm run check` for the frontend; `cargo fmt` + `cargo clippy` for the backend)
# 本地 fork 维护规则(Local fork rules)
> 本节是仅本地追加的内容,不属于上游 OxiCloud。与上游合并时,若本节之外的部分发生冲突,
> 以上游为准;本节始终保留在文件末尾以减少冲突面。
## 背景
本仓库是开源项目 OxiCloud 的本地副本,上游会持续更新。本地修改必须
**可追溯、可合并**:任何时候都要能知道"我们改了什么",以便与上游主分支合并。
## 规则 1:计划与进度必须记录在 `status.md`
- 每次接到非琐碎任务,**开始前**先在 `status.md` 顶部("进行中"区域)写下计划。
- `status.md` 条目格式(每条任务一个区块):
```markdown
### [YYYY-MM-DD] 任务标题
- **状态**: 进行中 / 已完成 / 已放弃(写明原因)
- **计划**: 要做什么、分几步
- **改动文件**: 列出修改/新增的上游文件(相对路径)+ 一句话说明
- **仅本地文件**: 新增的不属于上游的文件(合并时无需处理)
- **上游冲突风险**: 高 / 中 / 低,以及可能与上游哪些文件冲突
```
- 状态只允许进行中/已完成/已放弃三种;完成的任务移入"已完成"区域,保留记录不删除。
## 规则 2:每次修改后立即更新 `status.md`
- **不需要用户提醒**。任何一次代码/文档修改完成后,agent 必须同步更新
`status.md` 中对应条目的状态、改动文件列表和冲突风险。
- 即使任务中途被打断,也要把当前进度写清(做到哪一步、剩下什么),保证
任何 agent(或人)读了 `status.md` 就能接手。
## 规则 3:与上游主分支合并
- **小步提交**:一个任务一个 commit(或少量 commit),commit message 说清楚改了什么。
不要把多天的工作堆成一个巨型 commit,否则合并时无法选择性丢弃。
- **少改上游文件**:能用新增文件解决的(新组件、新模块、新 endpoint)就不要改上游现有文件;
必须改时尽量小而集中,并在 `status.md` 的"上游冲突风险"里注明。
**`AGENTS.md` 本身也因此只允许在文件末尾追加内容,不得改动上游已有的章节。**
- **不改无关格式**:不要顺手重排上游代码、改无关 import 顺序——纯噪音,制造冲突。
- 合并上游的流程:
```bash
git remote add upstream <上游仓库地址> # 只需配置一次
git fetch upstream
git merge upstream/main # 或 rebase,按团队习惯;首次建议 merge
# 解决冲突时:先读 status.md 的"改动文件"列表,逐个文件核对本地意图
git status # 确认没有遗漏的冲突标记
cargo fmt --all && cargo clippy --all-features --all-targets -- -D warnings
just test
```
- 合并完成后,在 `status.md` 新增一条"上游合并"记录:合并到的 upstream commit、
解决过的冲突文件、是否有本地修改被上游覆盖/废弃。
- 若上游已用别的方式实现了某个本地功能(导致本地补丁不再需要),在 `status.md`
把对应条目标为"已放弃(上游已实现)",并考虑回退本地补丁。
## 规则 4:其他
- `status.md` 属于仅本地文件,不向上游提 PR(除非团队明确决定);
`AGENTS.md` 中仅本节("本地 fork 维护规则")是本地内容,向上游提 PR 时应剔除。