docs(project): sync docs with code and bump to 3.0.0

Prepare the 3.0.0 release:
- Bump the version in openapi.yaml and frontend/package.json to 3.0.0.
- Document the existing GET /health endpoint in openapi.yaml (served at the
  root, outside /api/v1) and refine the auth note.
- Add docs/REQUIREMENTS.md (product requirements, in English) and
  docs/ARCHITECTURE.md (system overview); remove the old Russian
  docs/Описание.md.
- Rewrite GO_PROJECT_STRUCTURE.md and FRONTEND_STRUCTURE.md to match the
  current code (dedup CLI, imagehash, real components/stores, Tailwind v4).
- Fix stale counts and references in CLAUDE.md and link the new docs from
  README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 08:35:16 +03:00
parent 4883820f25
commit 28f5b5d150
9 changed files with 740 additions and 626 deletions
+7 -3
View File
@@ -13,16 +13,18 @@ Monorepo: `backend/` (Go) + `frontend/` (SvelteKit).
## Key documents (read before coding) ## Key documents (read before coding)
- `openapi.yaml` — full REST API specification (36 paths, 58 operations) - `openapi.yaml` — full REST API specification (44 paths, 67 operations)
- `docs/REQUIREMENTS.md` — product requirements (functional + non-functional)
- `docs/ARCHITECTURE.md` — system architecture overview (layers, data flow, decisions)
- `docs/GO_PROJECT_STRUCTURE.md` — backend architecture, layer rules, DI pattern - `docs/GO_PROJECT_STRUCTURE.md` — backend architecture, layer rules, DI pattern
- `docs/FRONTEND_STRUCTURE.md` — frontend architecture, CSS approach, API client - `docs/FRONTEND_STRUCTURE.md` — frontend architecture, CSS approach, API client
- `docs/Описание.md` — product requirements in Russian - `backend/migrations/` — database schema as goose migrations (4 schemas, 19 tables)
- `backend/migrations/001_init.sql` — database schema (4 schemas, 16 tables)
## Design reference ## Design reference
Visual design tokens for the frontend (carried over from the previous Visual design tokens for the frontend (carried over from the previous
Python/Flask version): Python/Flask version):
- Color palette: #312F45 (bg), #9592B5 (accent), #444455 (tag default), #111118 (elevated) - Color palette: #312F45 (bg), #9592B5 (accent), #444455 (tag default), #111118 (elevated)
- Font: Epilogue (variable weight) - Font: Epilogue (variable weight)
- Dark theme is primary - Dark theme is primary
@@ -32,6 +34,7 @@ Python/Flask version):
- Floating selection bar for multi-select - Floating selection bar for multi-select
## Backend commands ## Backend commands
```bash ```bash
cd backend cd backend
go run ./cmd/server # run dev server go run ./cmd/server # run dev server
@@ -39,6 +42,7 @@ go test ./... # run all tests
``` ```
## Frontend commands ## Frontend commands
```bash ```bash
cd frontend cd frontend
npm run dev # vite dev server npm run dev # vite dev server
+2
View File
@@ -8,6 +8,8 @@ built SPA on one port.
## Documentation ## Documentation
- [`openapi.yaml`](openapi.yaml) — full REST API specification - [`openapi.yaml`](openapi.yaml) — full REST API specification
- [`docs/REQUIREMENTS.md`](docs/REQUIREMENTS.md) — product requirements
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — system architecture overview
- [`docs/DEPLOY.md`](docs/DEPLOY.md) — production deploy (Gitea Actions → host) - [`docs/DEPLOY.md`](docs/DEPLOY.md) — production deploy (Gitea Actions → host)
- [`docs/GO_PROJECT_STRUCTURE.md`](docs/GO_PROJECT_STRUCTURE.md) — backend architecture - [`docs/GO_PROJECT_STRUCTURE.md`](docs/GO_PROJECT_STRUCTURE.md) — backend architecture
- [`docs/FRONTEND_STRUCTURE.md`](docs/FRONTEND_STRUCTURE.md) — frontend architecture - [`docs/FRONTEND_STRUCTURE.md`](docs/FRONTEND_STRUCTURE.md) — frontend architecture
+180
View File
@@ -0,0 +1,180 @@
# Tanabata File Manager — Architecture
System-level overview of Tanabata File Manager (TFM). For the product-level
requirements see [REQUIREMENTS.md](REQUIREMENTS.md); for per-side detail see
[GO_PROJECT_STRUCTURE.md](GO_PROJECT_STRUCTURE.md) (backend) and
[FRONTEND_STRUCTURE.md](FRONTEND_STRUCTURE.md) (frontend). The full HTTP contract
lives in [`openapi.yaml`](../openapi.yaml).
## System Context
TFM is a multi-user, tag-based web file manager for images and video. It is a
single deployable unit — one Docker image that serves both the REST API and the
built single-page app on one port — plus a PostgreSQL database.
```
┌───────────────────────────────────────────┐
Browser / installed │ Reverse proxy (nginx, TLS) │
PWA (desktop/mobile) │ host: 443 → 127.0.0.1:${APP_PORT} │
│ HTTPS └─────────────────────┬─────────────────────┘
└─────────────────────────────────────┼──────────────► 127.0.0.1:42776
┌─────────────────────────▼────────────────────────┐
│ Tanabata container (single image) │
│ │
│ Go server (Gin) │
│ ├─ /api/v1/* REST API │
│ ├─ /health liveness │
│ └─ /* static SPA + index.html │
│ fallback │
│ │
│ Disk: /data/files (originals, name = UUID) │
│ /data/thumbs (thumbnail/preview cache) │
│ /data/import (server-side import drop) │
└─────────────────────────┬────────────────────────┘
│ pgx (private network)
┌─────────▼─────────┐
│ PostgreSQL 14+ │
│ (bundled or host)│
└───────────────────┘
```
Optional companion process: a one-shot **dedup CLI** (same image, different
entrypoint) that backfills perceptual hashes and rebuilds the duplicate-pairs
table. It is not a daemon — it is run on demand.
## Components
| Component | Tech | Responsibility |
| -------------- | -------------------------------------------------------------------------- | -------------------------------------------------------- |
| Frontend (SPA) | SvelteKit (adapter-static, `ssr=false`), Svelte 5, Tailwind v4, TypeScript | UI, client routing, PWA/offline, calls the REST API |
| API server | Go + Gin, Clean Architecture | REST API, auth, ACL, business logic, thumbnailing, audit |
| Database | PostgreSQL 14+ (pgx v5, goose) | All structured data across 4 schemas / 19 tables |
| File storage | Local disk, flat, keyed by UUID | Originals + a regenerable thumbnail/preview cache |
| dedup CLI | Go (same image) | Offline perceptual-hash backfill + pairs rescan |
| Reverse proxy | nginx (host, not shipped) | TLS termination, large-body/streaming config |
## Backend Architecture (Clean Architecture)
Dependencies point inward; no layer imports a layer above it.
```
handler → service → port (interfaces) ← db/postgres, storage, imagehash
domain (entities, value objects, errors) — stdlib only
```
- **domain** — entities and errors, zero internal imports.
- **port** — interfaces (repositories, `FileStorage`, `Transactor`).
- **service** — use cases; the only place business rules live.
- **handler** — Gin HTTP layer; maps domain errors to HTTP status codes.
- **db/postgres**, **storage**, **imagehash** — adapters implementing the ports.
Wiring is manual in `cmd/server/main.go` (no DI framework). See
[GO_PROJECT_STRUCTURE.md](GO_PROJECT_STRUCTURE.md) for the file-by-file layout,
the transaction/context patterns, and the DI sketch.
## Request Flow (typical authenticated call)
1. The SPA sends `Authorization: Bearer <access token>` to `/api/v1/...`.
2. Gin middleware runs: security headers → (for `/auth`) per-IP rate limiter →
auth middleware validates the JWT and puts `(userID, isAdmin, sessionID)`
into the request context.
3. The handler parses/validates input and calls a service method
(`ctx` first arg).
4. The service enforces ACL via `ACLService`, performs the use case — composing
repository calls inside a `Transactor.WithTx` when several writes must be
atomic — and writes an audit entry.
5. Repositories run SQL through pgx (pool or the tx carried in `ctx`).
6. The handler serializes the result; domain errors are mapped to
`{ code, message, details? }` with the right HTTP status.
## Cross-Cutting Concerns
### Authentication & sessions
JWT bearer auth. A short-lived **access token** (15 min default) authorizes API
calls; a long-lived **refresh token** (30 days default) rotates on use and is
stored as a hash in `activity.sessions`. A separate **content token** (6 h
default) is a single-file capability embedded in media URLs so long video keeps
streaming past access-token expiry. The `/auth` endpoints are rate-limited per
client IP.
### Authorization (ACL)
Private-by-default. Admins see everything; otherwise access requires a `public`
flag, creator ownership, or an explicit grant in `acl.permissions` (read / edit).
All checks are centralized in `ACLService` and applied before reads and writes.
### File storage
Originals are stored flat under `FILES_PATH`, each named by its file UUID (no
directory tree, no original-name collisions). Thumbnails and previews are a
**regenerable cache** under `THUMBS_CACHE_PATH`: still images via vipsthumbnail
(shrink-on-load) with a pure-Go `imaging` fallback, video frames via ffmpeg;
metadata/EXIF via exiftool with a pure-Go fallback. Uploads are rejected unless
their sniffed MIME type is whitelisted in `core.mime_types`.
### Near-duplicate detection
Images are dHash-ed (64-bit perceptual hash) inline on upload; video hashes are
backfilled by the dedup CLI. A rescan rebuilds `data.duplicate_pairs` using a
BK-tree over Hamming distance (within `DUPLICATE_HASH_THRESHOLD`), and the API
groups pairs into connected-component clusters. Dismissed pairs are remembered so
they stop resurfacing. See the duplicate sections in
[GO_PROJECT_STRUCTURE.md](GO_PROJECT_STRUCTURE.md).
### Audit logging
User-visible actions (file/tag/category/pool CRUD, relations, ACL changes,
auth, session termination, admin user actions) are recorded in
`activity.audit_log` against a seeded set of action types.
### Frontend / PWA
Pure client-side SPA: static assets served by the Go binary, with `index.html`
as the fallback for client routes. Installable PWA with a service worker for
app-shell caching and optional offline viewing of pinned files. See
[FRONTEND_STRUCTURE.md](FRONTEND_STRUCTURE.md).
## Data Model
PostgreSQL, four schemas (see `backend/migrations/`):
- **core** — users, MIME whitelist, object types.
- **data** — categories, tags, tag rules, files, filetag, pools, filepool,
duplicate pairs, duplicate dismissals.
- **acl** — per-object permission grants.
- **activity** — sessions, file/pool views, tag uses, audit log, action types.
Migrations are goose files embedded via `go:embed` and applied automatically on
server startup, so a fresh database bootstraps itself.
## Deployment
- **One image, one port.** The multi-stage `Dockerfile` builds the SPA (Node
stage) and the static Go binary (Go stage), then ships an Alpine runtime with
vips-tools / ffmpeg / exiftool and a non-root user. The server serves both the
API and the SPA on port **42776** (the sum of the code points of 七夕).
- **Compose.** `docker-compose.yml` runs the app plus, optionally, a bundled
PostgreSQL (`with-db` profile); a host Postgres is supported by leaving the
profile empty. The app is published on loopback only and expects a host
reverse proxy; the DB sits on a private `internal` network with no route
off-host. The dedup CLI is a `tools`-profile, run-on-demand service.
- **Config.** All runtime config is environment variables, fully documented in
[`.env.example`](../.env.example) (1:1 with `config.Config`). Secrets
(`JWT_SECRET`, `ADMIN_PASSWORD`, `DATABASE_URL`) are never baked into the image.
- **First run.** Migrations auto-apply and the initial admin is bootstrapped
from `ADMIN_USERNAME` / `ADMIN_PASSWORD`, so setup is: fill `.env`,
`docker compose up`.
See [DEPLOY.md](DEPLOY.md) for the production deploy (Gitea Actions → host) and
the reverse-proxy notes in [README.md](../README.md).
## Design Constraints & Future Direction
- **DDD / Clean Architecture** on the server keeps business rules independent of
Gin and pgx.
- **PostgreSQL-specific adapters are isolated** behind the `port` interfaces (the
filter DSL → SQL translation lives in `db/postgres`), leaving room for other
database engines in a future version without touching the service layer.
+169 -315
View File
@@ -1,13 +1,21 @@
# Tanabata File Manager — Frontend Structure # Tanabata File Manager — Frontend Structure
> Frontend counterpart of [ARCHITECTURE.md](ARCHITECTURE.md). This document
> details the SvelteKit layout, the CSS approach, and the API client.
## Stack ## Stack
- **Framework**: SvelteKit (SPA mode, `ssr: false`) - **Framework**: SvelteKit in **SPA mode** (`adapter-static`, `ssr = false`),
- **Language**: TypeScript Svelte 5 (runes)
- **CSS**: Tailwind CSS + CSS custom properties (hybrid) - **Language**: TypeScript (strict)
- **API types**: Auto-generated via openapi-typescript - **Build**: Vite 7
- **PWA**: Service worker + web manifest - **CSS**: Tailwind CSS **v4** via `@tailwindcss/vite` + CSS custom properties
(`@theme` in `app.css`) — no `tailwind.config.*` / `postcss.config.*` file
- **API types**: auto-generated via `openapi-typescript` (`src/lib/api/schema.ts`)
- **PWA**: service worker + web manifest
- **Font**: Epilogue (variable weight) - **Font**: Epilogue (variable weight)
- **Dev**: `vite-mock-plugin.ts` serves a mock API so the UI can run without the
Go backend
- **Package manager**: npm - **Package manager**: npm
## SPA mode — why SvelteKit without the server ## SPA mode — why SvelteKit without the server
@@ -19,7 +27,7 @@ static assets, and the only backend is the Go API. SvelteKit is used here
purely as an SPA framework: file-based routing, the client router, and build purely as an SPA framework: file-based routing, the client router, and build
tooling. tooling.
**SvelteKit features we *do* use:** **SvelteKit features we _do_ use:**
- File-based routing with nested layouts (`admin/` has its own guard) and - File-based routing with nested layouts (`admin/` has its own guard) and
dynamic segments (`[id]`). dynamic segments (`[id]`).
@@ -30,11 +38,11 @@ tooling.
over the still-mounted list via shallow routing, so the browser back button over the still-mounted list via shallow routing, so the browser back button
dismisses it without reloading the grid. This is the single biggest reason we dismisses it without reloading the grid. This is the single biggest reason we
stay on SvelteKit rather than a plain router. stay on SvelteKit rather than a plain router.
- `load` functions, used *only* as client-side route guards (auth redirect, - `load` functions, used _only_ as client-side route guards (auth redirect,
admin redirect, `/``/files`). admin redirect, `/``/files`).
- `$lib` alias, generated `./$types`, Vite/HMR integration. - `$lib` alias, generated `./$types`, Vite/HMR integration.
**SvelteKit features we deliberately do *not* use** (the "server half"): **SvelteKit features we deliberately do _not_ use** (the "server half"):
- SSR / hydration. - SSR / hydration.
- `+page.server.ts`, `+server.ts` endpoints, form actions — all data goes - `+page.server.ts`, `+server.ts` endpoints, form actions — all data goes
@@ -43,7 +51,7 @@ tooling.
`hooks.client.ts` files exist. `hooks.client.ts` files exist.
**Decision: stay on SvelteKit, do not migrate to a bare Svelte + router SPA.** **Decision: stay on SvelteKit, do not migrate to a bare Svelte + router SPA.**
The project already *is* an SPA, so there is no runtime gain from switching The project already _is_ an SPA, so there is no runtime gain from switching
(adapter-static tree-shakes the unused server bits; the client-runtime size (adapter-static tree-shakes the unused server bits; the client-runtime size
difference is negligible). A migration would mean re-implementing nested difference is negligible). A migration would mean re-implementing nested
layouts, guards, dynamic params, and — most painfully — shallow routing / layouts, guards, dynamic params, and — most painfully — shallow routing /
@@ -55,15 +63,7 @@ expect SSR, endpoints, or hooks to do anything here; that is intentional.
``` ```
tanabata/ tanabata/
├── backend/ ← Go project (go.mod in here) ├── backend/ ← Go project (go.mod in here)
│ ├── cmd/
│ ├── internal/
│ ├── migrations/
│ ├── go.mod
│ └── go.sum
├── frontend/ ← SvelteKit project (package.json in here) ├── frontend/ ← SvelteKit project (package.json in here)
│ └── (see below)
├── openapi.yaml ← Shared API contract (root level) ├── openapi.yaml ← Shared API contract (root level)
├── docker-compose.yml ├── docker-compose.yml
├── Dockerfile ├── Dockerfile
@@ -71,352 +71,206 @@ tanabata/
└── README.md └── README.md
``` ```
`openapi.yaml` lives at repository root — both backend and frontend `openapi.yaml` lives at repository root — both backend and frontend reference
reference it. The frontend generates types from it; the backend it. The frontend generates types from it; the backend implements it.
validates its handlers against it.
## Frontend Directory Layout ## Frontend Directory Layout
``` ```
frontend/ frontend/
├── package.json ├── package.json
├── svelte.config.js ├── svelte.config.js # adapter-static, fallback: index.html
├── vite.config.ts ├── vite.config.ts # plugins: tailwindcss(), sveltekit(), mockApiPlugin()
├── vite-mock-plugin.ts # dev-only mock API (run the UI without the Go backend)
├── tsconfig.json ├── tsconfig.json
├── tailwind.config.ts
├── postcss.config.js
├── src/ ├── static/ # Copied verbatim into the build
│ ├── app.html # Shell HTML (PWA meta, font preload) │ ├── manifest.webmanifest # PWA manifest
│ ├── app.css # Tailwind directives + CSS custom properties │ ├── browserconfig.xml
│ # (no hooks.* — see "SPA mode" above) ├── robots.txt
├── favicon.ico
│ ├── lib/ # Shared code ($lib/ alias) │ ├── fonts/
│ │ │ │ └── Epilogue-VariableFont_wght.ttf
│ ├── api/ # API client layer └── images/ # PWA icons, section icons (svg), login decorations
│ │ │ ├── client.ts # Base fetch wrapper: auth headers, token refresh,
│ │ │ │ # error parsing, base URL
│ │ │ ├── files.ts # listFiles, getFile, uploadFile, deleteFile, etc.
│ │ │ ├── tags.ts # listTags, createTag, getTag, updateTag, etc.
│ │ │ ├── categories.ts # Category API functions
│ │ │ ├── pools.ts # Pool API functions
│ │ │ ├── auth.ts # login, logout, refresh, listSessions
│ │ │ ├── acl.ts # getPermissions, setPermissions
│ │ │ ├── users.ts # getMe, updateMe, admin user CRUD
│ │ │ ├── audit.ts # queryAuditLog
│ │ │ ├── schema.ts # AUTO-GENERATED from openapi.yaml (do not edit)
│ │ │ └── types.ts # Friendly type aliases:
│ │ │ # export type File = components["schemas"]["File"]
│ │ │ # export type Tag = components["schemas"]["Tag"]
│ │ │
│ │ ├── components/ # Reusable UI components
│ │ │ │
│ │ │ ├── layout/ # App shell
│ │ │ │ ├── Navbar.svelte # Bottom navigation bar (mobile-first)
│ │ │ │ ├── Header.svelte # Section header with sorting controls
│ │ │ │ ├── SelectionBar.svelte # Floating bar for multi-select actions
│ │ │ │ └── Loader.svelte # Full-screen loading overlay
│ │ │ │
│ │ │ ├── file/ # File-related components
│ │ │ │ ├── FileGrid.svelte # Thumbnail grid with infinite scroll
│ │ │ │ ├── FileCard.svelte # Single thumbnail (160×160, selectable)
│ │ │ │ ├── FileViewer.svelte # Full-screen preview with prev/next navigation
│ │ │ │ ├── FileUpload.svelte # Upload form + drag-and-drop zone
│ │ │ │ ├── FileDetail.svelte # Metadata editor (notes, datetime, tags)
│ │ │ │ └── FilterBar.svelte # DSL filter builder UI
│ │ │ │
│ │ │ ├── tag/ # Tag-related components
│ │ │ │ ├── TagBadge.svelte # Colored pill with tag name
│ │ │ │ ├── TagPicker.svelte # Searchable tag selector (add/remove)
│ │ │ │ ├── TagList.svelte # Tag grid for section view
│ │ │ │ └── TagRuleEditor.svelte # Auto-tag rule management
│ │ │ │
│ │ │ ├── pool/ # Pool-related components
│ │ │ │ ├── PoolCard.svelte # Pool preview card
│ │ │ │ ├── PoolFileList.svelte # Ordered file list with drag reorder
│ │ │ │ └── PoolDetail.svelte # Pool metadata editor
│ │ │ │
│ │ │ ├── acl/ # Access control components
│ │ │ │ └── PermissionEditor.svelte # User permission grid
│ │ │ │
│ │ │ └── common/ # Shared primitives
│ │ │ ├── Button.svelte
│ │ │ ├── Modal.svelte
│ │ │ ├── ConfirmDialog.svelte
│ │ │ ├── Toast.svelte
│ │ │ ├── InfiniteScroll.svelte
│ │ │ ├── Pagination.svelte
│ │ │ ├── SortDropdown.svelte
│ │ │ ├── SearchInput.svelte
│ │ │ ├── ColorPicker.svelte
│ │ │ ├── Checkbox.svelte # Three-state: checked, unchecked, partial
│ │ │ └── EmptyState.svelte
│ │ │
│ │ ├── stores/ # Svelte stores (global state)
│ │ │ ├── auth.ts # Current user, JWT tokens, isAuthenticated
│ │ │ ├── selection.ts # Selected item IDs, selection mode toggle
│ │ │ ├── sorting.ts # Per-section sort key + order (persisted to localStorage)
│ │ │ ├── theme.ts # Dark/light mode (persisted, respects prefers-color-scheme)
│ │ │ └── toast.ts # Notification queue (success, error, info)
│ │ │
│ │ └── utils/ # Pure helper functions
│ │ ├── format.ts # formatDate, formatFileSize, formatDuration
│ │ ├── dsl.ts # Filter DSL builder: UI state → query string
│ │ ├── pwa.ts # PWA reset, cache clear, update prompt
│ │ └── keyboard.ts # Keyboard shortcut helpers (Ctrl+A, Escape, etc.)
│ │
│ ├── routes/ # SvelteKit file-based routing
│ │ │
│ │ ├── +layout.svelte # Root layout: Navbar, theme wrapper, toast container
│ │ ├── +layout.ts # Root load: auth guard → redirect to /login if no token
│ │ │
│ │ ├── +page.svelte # / → redirect to /files
│ │ │
│ │ ├── login/
│ │ │ └── +page.svelte # Login form (decorative Tanabata images)
│ │ │
│ │ ├── files/
│ │ │ ├── +page.svelte # File grid: filter bar, sort, multi-select, upload
│ │ │ ├── +page.ts # Load: initial file list (cursor page)
│ │ │ ├── [id]/
│ │ │ │ ├── +page.svelte # File view: preview, metadata, tags, ACL
│ │ │ │ └── +page.ts # Load: file detail + tags
│ │ │ └── trash/
│ │ │ ├── +page.svelte # Trash: restore / permanent delete
│ │ │ └── +page.ts
│ │ │
│ │ ├── tags/
│ │ │ ├── +page.svelte # Tag list: search, sort, multi-select
│ │ │ ├── +page.ts
│ │ │ ├── new/
│ │ │ │ └── +page.svelte # Create tag form
│ │ │ └── [id]/
│ │ │ ├── +page.svelte # Tag detail: edit, category, rules, parent tags
│ │ │ └── +page.ts
│ │ │
│ │ ├── categories/
│ │ │ ├── +page.svelte # Category list
│ │ │ ├── +page.ts
│ │ │ ├── new/
│ │ │ │ └── +page.svelte
│ │ │ └── [id]/
│ │ │ ├── +page.svelte # Category detail: edit, view tags
│ │ │ └── +page.ts
│ │ │
│ │ ├── pools/
│ │ │ ├── +page.svelte # Pool list
│ │ │ ├── +page.ts
│ │ │ ├── new/
│ │ │ │ └── +page.svelte
│ │ │ └── [id]/
│ │ │ ├── +page.svelte # Pool detail: files (reorderable), filter, edit
│ │ │ └── +page.ts
│ │ │
│ │ ├── settings/
│ │ │ ├── +page.svelte # Profile: name, password, active sessions
│ │ │ └── +page.ts
│ │ │
│ │ └── admin/
│ │ ├── +layout.svelte # Admin layout: restrict to is_admin
│ │ ├── users/
│ │ │ ├── +page.svelte # User management list
│ │ │ ├── +page.ts
│ │ │ └── [id]/
│ │ │ ├── +page.svelte # User detail: role, block/unblock
│ │ │ └── +page.ts
│ │ └── audit/
│ │ ├── +page.svelte # Audit log with filters
│ │ └── +page.ts
│ │
│ └── service-worker.ts # PWA: offline cache for pinned files, app shell caching
└── static/ └── src/
├── favicon.png ├── app.html # Shell HTML (PWA meta, font preload)
├── favicon.ico ├── app.css # `@import 'tailwindcss'` + `@theme` custom properties
├── manifest.webmanifest # PWA manifest (name, icons, theme_color) ├── app.d.ts # Ambient types
├── images/ ├── service-worker.ts # PWA: app-shell + pinned-file offline cache
├── tanabata-left.png # Login page decorations (from current design)
│ ├── tanabata-right.png ├── lib/ # Shared code ($lib alias)
── icons/ # PWA icons (192×192, 512×512, etc.) ── index.ts
└── fonts/ │ │
── Epilogue-VariableFont_wght.ttf ── api/ # API client layer
│ │ ├── client.ts # fetch wrapper: bearer auth, 401 refresh+retry, error parsing,
│ │ │ # upload-with-progress (XHR), NDJSON streaming; exports `api`
│ │ ├── auth.ts # login, refresh, logout, sessions
│ │ ├── tags.ts # tag + tag-rule calls
│ │ ├── categories.ts # category calls
│ │ ├── duplicates.ts # duplicate list / dismiss / resolve
│ │ ├── schema.ts # AUTO-GENERATED from openapi.yaml (gitignored; do not edit)
│ │ └── types.ts # Friendly aliases: components['schemas'][...]
│ │
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Header.svelte # Section header with sorting controls
│ │ │ ├── SelectionBar.svelte # Floating bar for multi-select actions
│ │ │ └── KeyboardHelp.svelte # Keyboard-shortcut overlay
│ │ │
│ │ ├── file/
│ │ │ ├── FileCard.svelte # Single thumbnail (160×160, selectable)
│ │ │ ├── Thumb.svelte # Lazy-loaded thumbnail (IntersectionObserver)
│ │ │ ├── FileViewer.svelte # Full-screen viewer with prev/next
│ │ │ ├── FileUpload.svelte # Upload form + drag-and-drop
│ │ │ ├── FilterBar.svelte # DSL filter builder UI
│ │ │ ├── MetadataEditor.svelte # Notes / datetime / metadata (nested) editor
│ │ │ ├── TagPicker.svelte # Searchable tag selector (add/remove)
│ │ │ ├── PoolPicker.svelte # Add-to-pool dialog
│ │ │ ├── BulkTagEditor.svelte # Multi-select tag add/remove
│ │ │ └── DuplicateMergeDialog.svelte # Field-by-field duplicate resolution
│ │ │
│ │ ├── tag/
│ │ │ ├── TagBadge.svelte # Colored pill
│ │ │ └── TagRuleEditor.svelte # Auto-tag rule management
│ │ │
│ │ └── common/
│ │ ├── ConfirmDialog.svelte
│ │ └── InfiniteScroll.svelte # Below-the-fold lazy loading on scroll
│ │
│ ├── stores/ # Svelte stores (global state)
│ │ ├── auth.ts # Current user + JWT tokens (persisted, cross-tab sync)
│ │ ├── selection.ts # Selected item IDs, selection mode
│ │ ├── sorting.ts # Per-section sort key + order (persisted)
│ │ ├── theme.ts # Dark/light theme (persisted)
│ │ ├── appSettings.ts # Misc client-side settings
│ │ ├── listScroll.ts # Restore list scroll position after overlay/back
│ │ └── sectionCache.ts # Cached list snapshots, invalidated on mutation
│ │
│ └── utils/ # Pure helpers
│ ├── dsl.ts # Filter DSL builder: UI state → query string
│ ├── metadata.ts # Nested metadata <-> editor rows
│ ├── pwa.ts # PWA reset / update prompt
│ └── rovingGrid.svelte.ts # Roving-tabindex keyboard grid navigation
└── routes/ # SvelteKit file-based routing (guards only in load)
├── +layout.svelte # Root layout: nav, theme
├── +layout.ts # ssr=false; root auth guard
├── +page.svelte / +page.ts # / → redirect to /files
├── login/+page.svelte
├── files/
│ ├── +page.svelte / +page.ts # Grid: filter, sort, multi-select, upload
│ ├── [id]/+page.svelte # File view (also opened as shallow-routing overlay)
│ ├── duplicates/+page.svelte # Duplicate clusters
│ └── trash/+page.svelte # Trash: restore / permanent delete
├── tags/ { +page.svelte, new/, [id]/ }
├── categories/ { +page.svelte, new/, [id]/ }
├── pools/ { +page.svelte, new/, [id]/ }
├── settings/+page.svelte # Profile: name, password, sessions, import path
└── admin/
├── +layout.svelte / +layout.ts # Restrict to admins
├── users/{ +page.svelte, [id]/ }
└── audit/+page.svelte
``` ```
## Key Architecture Decisions ## Key Architecture Decisions
### CSS Hybrid: Tailwind + Custom Properties ### CSS: Tailwind v4 + Custom Properties
Theme colors defined as CSS custom properties in `app.css`: Tailwind v4 is configured **in CSS**, not in a JS config file. `app.css` imports
Tailwind and declares the theme tokens as CSS custom properties inside `@theme`;
Tailwind then generates utilities (`bg-bg-primary`, `text-text-primary`,
`font-sans`, …) from those tokens automatically.
```css ```css
@tailwind base; /* src/app.css */
@tailwind components; @import "tailwindcss";
@tailwind utilities;
:root { @theme {
--color-bg-primary: #312F45; --color-bg-primary: #312f45;
--color-bg-secondary: #181721; --color-bg-secondary: #181721;
--color-bg-elevated: #111118; --color-bg-elevated: #111118;
--color-accent: #9592B5; --color-accent: #9592b5;
--color-accent-hover: #7D7AA4; --color-accent-hover: #7d7aa4;
--color-text-primary: #f0f0f0; --color-text-primary: #f0f0f0;
--color-text-muted: #9999AD; --color-tag-default: #444455;
--color-danger: #DB6060; /* … info / danger / warning / success / nav tokens … */
--color-info: #4DC7ED;
--color-warning: #F5E872;
--color-tag-default: #444455;
}
:root[data-theme="light"] { --font-sans: "Epilogue", sans-serif;
--color-bg-primary: #f5f5f5;
--color-bg-secondary: #ffffff;
/* ... */
} }
``` ```
Tailwind references them in `tailwind.config.ts`: Dark theme is primary; the light theme overrides the same custom properties.
```ts
export default {
theme: {
extend: {
colors: {
bg: {
primary: 'var(--color-bg-primary)',
secondary: 'var(--color-bg-secondary)',
elevated: 'var(--color-bg-elevated)',
},
accent: {
DEFAULT: 'var(--color-accent)',
hover: 'var(--color-accent-hover)',
},
// ...
},
fontFamily: {
sans: ['Epilogue', 'sans-serif'],
},
},
},
darkMode: 'class', // controlled via data-theme attribute
};
```
Usage in components: `<div class="bg-bg-primary text-text-primary rounded-xl p-4">`. Usage in components: `<div class="bg-bg-primary text-text-primary rounded-xl p-4">`.
Complex cases use scoped `<style>` inside `.svelte` files. Complex cases use scoped `<style>` inside `.svelte` files.
### API Client Pattern ### API Client Pattern
`client.ts` thin wrapper around fetch: `src/lib/api/client.ts` is a thin fetch wrapper exporting a generic `api`
object. It attaches the bearer token, transparently handles a single `401`
refresh-and-retry (deduplicating concurrent refreshes and syncing rotated
tokens across tabs), parses `{ code, message, details }` errors into `ApiError`,
and invalidates cached list snapshots on mutation. It also provides
`uploadWithProgress` (XHR, for upload progress) and `postStream` (NDJSON, for
the live import progress).
```ts ```ts
// $lib/api/client.ts // $lib/api/client.ts (shape)
import { authStore } from '$lib/stores/auth';
const BASE = '/api/v1';
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const token = get(authStore).accessToken;
const res = await fetch(BASE + path, {
...init,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...init?.headers,
},
});
if (res.status === 401) {
// attempt refresh, retry once
}
if (!res.ok) {
const err = await res.json();
throw new ApiError(res.status, err.code, err.message, err.details);
}
if (res.status === 204) return undefined as T;
return res.json();
}
export const api = { export const api = {
get: <T>(path: string) => request<T>(path), get: <T>(path) => request<T>(path),
post: <T>(path: string, body?: unknown) => post: <T>(path, body?) =>
request<T>(path, { method: 'POST', body: JSON.stringify(body) }), request<T>(path, { method: "POST", body: JSON.stringify(body) }),
patch: <T>(path: string, body?: unknown) => patch: <T>(path, body?) =>
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }), request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
put: <T>(path: string, body?: unknown) => put: <T>(path, body?) =>
request<T>(path, { method: 'PUT', body: JSON.stringify(body) }), request<T>(path, { method: "PUT", body: JSON.stringify(body) }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }), delete: <T>(path) => request<T>(path, { method: "DELETE" }),
upload: <T>(path: string, formData: FormData) => upload: <T>(path, fd) => request<T>(path, { method: "POST", body: fd }),
request<T>(path, { method: 'POST', body: formData, headers: {} }),
}; };
``` ```
Domain-specific modules use it: Resource modules (`auth.ts`, `tags.ts`, `categories.ts`, `duplicates.ts`) wrap
`api` with typed helpers. Endpoints without a dedicated module (files, pools,
```ts users, acl, audit) are called through `api.*` directly from their route
// $lib/api/files.ts components.
import { api } from './client';
import type { File, FileCursorPage } from './types';
export function listFiles(params: Record<string, string>) {
const qs = new URLSearchParams(params).toString();
return api.get<FileCursorPage>(`/files?${qs}`);
}
export function uploadFile(formData: FormData) {
return api.upload<File>('/files', formData);
}
```
### Type Generation ### Type Generation
Script in `package.json`:
```json ```json
// package.json
{ {
"scripts": { "scripts": {
"generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts", "generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts",
"dev": "npm run generate:types && vite dev", "dev": "npm run generate:types && vite dev",
"build": "npm run generate:types && vite build" "build": "npm run generate:types && vite build"
} }
} }
``` ```
Friendly aliases in `types.ts`: `schema.ts` is generated (and gitignored) — never edit it by hand. `types.ts`
re-exports friendly aliases:
```ts ```ts
import type { components } from './schema'; import type { components } from "./schema";
export type File = components["schemas"]["File"];
export type File = components['schemas']['File']; export type Tag = components["schemas"]["Tag"];
export type Tag = components['schemas']['Tag']; // …
export type Category = components['schemas']['Category'];
export type Pool = components['schemas']['Pool'];
export type FileCursorPage = components['schemas']['FileCursorPage'];
export type TagOffsetPage = components['schemas']['TagOffsetPage'];
export type Error = components['schemas']['Error'];
// ...
``` ```
### SPA Mode ### SPA Mode (build)
`svelte.config.js`:
```js ```js
import adapter from '@sveltejs/adapter-static'; // svelte.config.js
import adapter from "@sveltejs/adapter-static";
export default { export default { kit: { adapter: adapter({ fallback: "index.html" }) } };
kit: {
adapter: adapter({ fallback: 'index.html' }),
// SPA: all routes handled client-side
},
};
``` ```
The Go backend serves `index.html` for all non-API routes (SPA fallback). The Go backend serves `index.html` for all non-API routes (SPA fallback, see
In development, Vite dev server proxies `/api` to the Go backend. `handler/static.go`). In development the Vite dev server serves the UI and the
mock plugin (or a proxied Go backend) answers `/api`.
### PWA ### PWA
`service-worker.ts` handles: `service-worker.ts` handles app-shell caching (HTML/CSS/JS/fonts) and optional
- App shell caching (HTML, CSS, JS, fonts) user-pinned file caching for offline viewing; `utils/pwa.ts` exposes the reset /
- User-pinned file caching (explicit, via UI button) update flow (clear caches and reload from the server, keeping pinned files).
- Cache versioning and cleanup on update
- Reset function (clear all caches except pinned files)
+168 -156
View File
@@ -1,18 +1,28 @@
# Tanabata File Manager — Go Project Structure # Tanabata File Manager — Go Project Structure
> Backend counterpart of [ARCHITECTURE.md](ARCHITECTURE.md). This document
> details the Go layout, the layer rules, and the key backend decisions.
## Stack ## Stack
- **Router**: Gin - **Router**: Gin
- **Database**: pgx v5 (pgxpool) - **Database**: pgx v5 (pgxpool)
- **Migrations**: goose v3 + go:embed (auto-migrate on startup) - **Migrations**: goose v3 + `go:embed` (auto-applied on startup)
- **Auth**: JWT (golang-jwt/jwt/v5) - **Auth**: JWT (golang-jwt/jwt/v5), Bearer access tokens + rotating refresh tokens
- **Config**: environment variables via .env (joho/godotenv) - **Config**: environment variables via `.env` (joho/godotenv)
- **Logging**: slog (stdlib, Go 1.21+) - **Logging**: slog (stdlib)
- **Validation**: go-playground/validator/v10 - **Metadata**: exiftool (external, preferred) with a pure-Go EXIF fallback
- **EXIF**: rwcarlsen/goexif or dsoprea/go-exif (rwcarlsen/goexif)
- **Image processing**: disintegration/imaging (thumbnails, previews) - **Thumbnails / previews**: vipsthumbnail (external, shrink-on-load) and ffmpeg
(video frames), with a pure-Go fallback (disintegration/imaging)
- **Near-duplicate detection**: 64-bit dHash perceptual hashing + a BK-tree /
Hamming-distance pairing (`internal/imagehash`, `internal/service/duplicate_*`)
- **Architecture**: Clean Architecture (domain → service → repository/handler) - **Architecture**: Clean Architecture (domain → service → repository/handler)
The binary is fully static (`CGO_ENABLED=0`). External tools are invoked as
subprocesses when present and are optional — the pure-Go paths keep the server
working without them.
## Monorepo Layout ## Monorepo Layout
``` ```
@@ -31,81 +41,98 @@ tanabata/
``` ```
backend/ backend/
├── cmd/ ├── cmd/
── server/ ── server/
└── main.go # Entrypoint: config → DB → migrate → wire → run └── main.go # Entrypoint: config → DB → migrate → bootstrap admin → wire → serve
│ └── dedup/
│ └── main.go # Offline maintenance CLI: perceptual-hash backfill + duplicate-pairs rescan
├── internal/ ├── internal/
│ │ │ │
│ ├── domain/ # Pure business entities & value objects │ ├── domain/ # Pure business entities & value objects (stdlib only)
│ │ ├── file.go # File, FileFilter, FilePage │ │ ├── file.go # File, FileFilter, FileListParams, FilePage
│ │ ├── tag.go # Tag, TagRule │ │ ├── tag.go # Tag, TagRule
│ │ ├── category.go # Category │ │ ├── category.go # Category
│ │ ├── pool.go # Pool, PoolFile │ │ ├── pool.go # Pool, PoolFile
│ │ ├── user.go # User, Session │ │ ├── user.go # User, Session
│ │ ├── acl.go # Permission, ObjectType │ │ ├── acl.go # Permission, ObjectType
│ │ ├── audit.go # AuditEntry, ActionType │ │ ├── audit.go # AuditEntry, ActionType
│ │ ── errors.go # Domain error types (ErrNotFound, ErrForbidden, etc.) │ │ ── duplicate.go # DuplicatePair, PHashEntry
│ │ ├── context.go # WithUser / UserFromContext (identity + session in ctx)
│ │ └── errors.go # Domain error types (ErrNotFound, ErrForbidden, …)
│ │ │ │
│ ├── port/ # Interfaces (ports) — contracts between layers │ ├── port/ # Interfaces (ports) — contracts between layers
│ │ ├── repository.go # FileRepo, TagRepo, CategoryRepo, PoolRepo, │ │ ├── repository.go # Transactor, FileRepo, TagRepo, TagRuleRepo, CategoryRepo,
│ │ │ # UserRepo, SessionRepo, ACLRepo, AuditRepo, │ │ │ # PoolRepo, UserRepo, SessionRepo, ACLRepo, AuditRepo,
│ │ │ # MimeRepo, TagRuleRepo │ │ │ # MimeRepo, DuplicatePairRepo, DismissalRepo
│ │ └── storage.go # FileStorage interface (disk operations) │ │ └── storage.go # FileStorage (originals + thumbnail/preview cache)
│ │ │ │
│ ├── service/ # Business logic (use cases) │ ├── service/ # Business logic (use cases)
│ │ ├── file_service.go # Upload, update, delete, trash/restore, replace, │ │ ├── file_service.go # Upload, update, delete, trash/restore, replace, import, filter/list
│ │ │ # import, filter/list, duplicate detection │ │ ├── tag_service.go # CRUD + auto-tag (rule) application
│ │ ├── tag_service.go # CRUD + auto-tag application logic │ │ ├── category_service.go # CRUD (thin: repo + ACL + audit)
│ │ ├── category_service.go # CRUD (thin, delegates to repo + ACL + audit)
│ │ ├── pool_service.go # CRUD + file ordering, add/remove files │ │ ├── pool_service.go # CRUD + file ordering, add/remove files
│ │ ├── auth_service.go # Login, logout, JWT issue/refresh, session management │ │ ├── auth_service.go # Login, logout, JWT issue/refresh, content tokens, sessions
│ │ ├── acl_service.go # Permission checks, grant/revoke │ │ ├── acl_service.go # Permission checks, grant/revoke
│ │ ├── audit_service.go # Log actions, query audit log │ │ ├── audit_service.go # Log actions, query audit log
│ │ ── user_service.go # Profile update, admin CRUD, block/unblock │ │ ── user_service.go # Profile update, admin CRUD, block/unblock, EnsureAdmin
│ │ ├── duplicate_service.go # Cluster / resolve (merge) / dismiss + rescan orchestration
│ │ ├── duplicate_index.go # BK-tree, Hamming pairing, connected-component clustering
│ │ └── metadata.go # EXIF / media metadata extraction (exiftool + pure-Go fallback)
│ │ │ │
│ ├── handler/ # HTTP layer (Gin handlers) │ ├── handler/ # HTTP layer (Gin handlers)
│ │ ├── router.go # Route registration, middleware wiring │ │ ├── router.go # Route registration, middleware, security headers, SPA fallback
│ │ ├── middleware.go # Auth middleware (JWT extraction → context) │ │ ├── middleware.go # Auth middleware (JWT / content token → context)
│ │ ├── request.go # Common request parsing helpers │ │ ├── ratelimit.go # Per-IP token-bucket limiter for /auth
│ │ ├── response.go # Error/success response builders │ │ ├── response.go # Error/success builders, domain-error → HTTP mapping
│ │ ├── static.go # Built SPA serving + index.html fallback
│ │ ├── file_handler.go # /files endpoints │ │ ├── file_handler.go # /files endpoints
│ │ ├── tag_handler.go # /tags endpoints │ │ ├── duplicate_handler.go # /files/duplicates endpoints
│ │ ├── tag_handler.go # /tags endpoints (+ filetag relations)
│ │ ├── category_handler.go # /categories endpoints │ │ ├── category_handler.go # /categories endpoints
│ │ ├── pool_handler.go # /pools endpoints │ │ ├── pool_handler.go # /pools endpoints
│ │ ├── auth_handler.go # /auth endpoints │ │ ├── auth_handler.go # /auth endpoints
│ │ ├── acl_handler.go # /acl endpoints │ │ ├── acl_handler.go # /acl endpoints
│ │ ├── user_handler.go # /users endpoints │ │ ├── user_handler.go # /users endpoints
│ │ └── audit_handler.go # /audit endpoints │ │ └── audit_handler.go # /audit endpoint
│ │ │ │
│ ├── db/ # Database adapters │ ├── db/ # Database adapters
│ │ ├── db.go # Common helpers: pagination, repo factory, transactor base │ │ ├── db.go # Shared helpers: Querier, tx-from-context, ScanRow, limit/offset clamps
│ │ └── postgres/ # PostgreSQL implementation │ │ └── postgres/ # PostgreSQL implementation
│ │ ├── postgres.go # pgxpool init, tx-from-context helpers │ │ ├── postgres.go # pgxpool init, Transactor, conn-or-tx helper
│ │ ├── file_repo.go # FileRepo implementation │ │ ├── file_repo.go # FileRepo (incl. perceptual-hash projections)
│ │ ├── tag_repo.go # TagRepo + TagRuleRepo implementation │ │ ├── tag_repo.go # TagRepo + TagRuleRepo
│ │ ├── category_repo.go # CategoryRepo implementation │ │ ├── category_repo.go # CategoryRepo
│ │ ├── pool_repo.go # PoolRepo implementation │ │ ├── pool_repo.go # PoolRepo
│ │ ├── user_repo.go # UserRepo implementation │ │ ├── user_repo.go # UserRepo
│ │ ├── session_repo.go # SessionRepo implementation │ │ ├── session_repo.go # SessionRepo
│ │ ├── acl_repo.go # ACLRepo implementation │ │ ├── acl_repo.go # ACLRepo
│ │ ├── audit_repo.go # AuditRepo implementation │ │ ├── audit_repo.go # AuditRepo
│ │ ├── mime_repo.go # MimeRepo implementation │ │ ├── mime_repo.go # MimeRepo
│ │ ── filter_parser.go # DSL → SQL WHERE clause builder │ │ ── duplicate_repo.go # DuplicatePairRepo + DismissalRepo
│ │ └── filter_parser.go # Filter DSL → SQL WHERE clause builder
│ │ │ │
│ ├── storage/ # File storage adapter │ ├── storage/ # File storage adapter
│ │ └── disk.go # FileStorage implementation (read/write/delete on disk) │ │ └── disk.go # FileStorage on disk: originals + thumbnail/preview cache
│ │ # (vipsthumbnail / ffmpeg / pure-Go imaging)
│ │
│ ├── imagehash/ # Perceptual hashing (64-bit dHash) for near-duplicate detection
│ │ └── imagehash.go
│ │
│ ├── integration/ # End-to-end HTTP tests against a disposable Postgres
│ │ └── server_test.go
│ │ │ │
│ └── config/ # Configuration │ └── config/ # Configuration
│ └── config.go # Struct + loader from env vars │ └── config.go # Config struct + loader from env vars
├── migrations/ # SQL migration files (goose format) ├── migrations/ # SQL migration files (goose format), embedded via go:embed
│ ├── 001_init_schemas.sql │ ├── 001_init_schemas.sql
│ ├── 002_core_tables.sql │ ├── 002_core_tables.sql
│ ├── 003_data_tables.sql │ ├── 003_data_tables.sql
│ ├── 004_acl_tables.sql │ ├── 004_acl_tables.sql
│ ├── 005_activity_tables.sql │ ├── 005_activity_tables.sql
│ ├── 006_indexes.sql │ ├── 006_indexes.sql
── 007_seed_data.sql ── 007_seed_data.sql
│ └── embed.go # //go:embed *.sql → migrations.FS
├── go.mod ├── go.mod
└── go.sum └── go.sum
@@ -126,6 +153,7 @@ handler → service → port (interfaces) ← db/postgres / storage
- **db/postgres/**: imports domain/, port/, and db/ (common helpers). Implements port interfaces. - **db/postgres/**: imports domain/, port/, and db/ (common helpers). Implements port interfaces.
- **db/**: imports domain/ and port/. Shared utilities for all DB adapters. - **db/**: imports domain/ and port/. Shared utilities for all DB adapters.
- **storage/**: imports domain/ and port/. Implements FileStorage. - **storage/**: imports domain/ and port/. Implements FileStorage.
- **imagehash/**: leaf package (stdlib + image libs); used by service/ and storage/.
No layer may import a layer above it. No circular dependencies. No layer may import a layer above it. No circular dependencies.
@@ -133,138 +161,126 @@ No layer may import a layer above it. No circular dependencies.
### Dependency Injection (Wiring) ### Dependency Injection (Wiring)
Manual wiring in `cmd/server/main.go`. No DI frameworks. Manual wiring in `cmd/server/main.go`. No DI frameworks. Constructors take their
collaborators explicitly; the shape below matches the real signatures.
```go ```go
// Pseudocode // Pseudocode — see cmd/server/main.go for the exact calls.
pool := postgres.NewPool(cfg.DatabaseURL) pool := postgres.NewPool(ctx, cfg.DatabaseURL)
goose.Up(pool, migrations) goose.Up(stdlib.OpenDBFromPool(pool), ".") // migrations.FS embedded
// Repos (all from internal/db/postgres/)
fileRepo := postgres.NewFileRepo(pool)
tagRepo := postgres.NewTagRepo(pool)
// ...
// Storage // Storage
diskStore := storage.NewDiskStorage(cfg.FilesPath) diskStorage := storage.NewDiskStorage(
cfg.FilesPath, cfg.ThumbsCachePath,
cfg.ThumbWidth, cfg.ThumbHeight, cfg.PreviewWidth, cfg.PreviewHeight,
cfg.ThumbMaxPixels, cfg.ThumbConcurrency,
)
// Repos (all from internal/db/postgres/)
fileRepo := postgres.NewFileRepo(pool)
// … tag, tagRule, category, pool, user, session, acl, audit, mime,
// duplicatePair, dismissal repos + transactor
// Services // Services
aclSvc := service.NewACLService(aclRepo, objectTypeRepo) authSvc := service.NewAuthService(userRepo, sessionRepo,
auditSvc := service.NewAuditService(auditRepo, actionTypeRepo) cfg.JWTSecret, cfg.JWTAccessTTL, cfg.JWTRefreshTTL, cfg.ContentTokenTTL)
fileSvc := service.NewFileService(fileRepo, mimeRepo, tagRepo, diskStore, aclSvc, auditSvc) aclSvc := service.NewACLService(aclRepo, fileRepo, tagRepo, categoryRepo, poolRepo, transactor)
tagSvc := service.NewTagService(tagRepo, tagRuleRepo, aclSvc, auditSvc) auditSvc := service.NewAuditService(auditRepo)
// ... tagSvc := service.NewTagService(tagRepo, tagRuleRepo, aclSvc, auditSvc, transactor)
dupSvc := service.NewDuplicateService(fileRepo, duplicatePairRepo, dismissalRepo,
aclSvc, auditSvc, transactor, cfg.DuplicateHashThreshold)
fileSvc := service.NewFileService(fileRepo, mimeRepo, diskStorage,
aclSvc, auditSvc, tagSvc, transactor, cfg.ImportPath)
// … category, pool, user services
// Handlers // Bootstrap the initial admin from env (idempotent).
fileHandler := handler.NewFileHandler(fileSvc, tagSvc) userSvc.EnsureAdmin(ctx, cfg.AdminUsername, cfg.AdminPassword)
// ...
router := handler.NewRouter(cfg, fileHandler, tagHandler, ...) // Handlers → router (also wires trusted proxies + optional static SPA dir)
router.Run(cfg.ListenAddr) router, _ := handler.NewRouter(authMiddleware, authHandler, fileHandler,
duplicateHandler, tagHandler, categoryHandler, poolHandler,
userHandler, aclHandler, auditHandler, cfg.StaticDir, cfg.TrustedProxies)
srv.ListenAndServe()
``` ```
### Context Propagation ### Context Propagation
Every service method receives `context.Context` as the first argument. Every service method receives `context.Context` as the first argument.
The handler extracts user info from JWT (via middleware) and puts it The auth middleware parses the JWT and puts the caller's identity (user id,
into context. Services read the current user from context for ACL checks admin flag, session id) into the context. Services read it for ACL checks and
and audit logging. audit logging.
```go ```go
// middleware.go // handler/middleware.go
func (m *AuthMiddleware) Handle(c *gin.Context) { claims := parseJWT(c.GetHeader("Authorization"))
claims := parseJWT(c.GetHeader("Authorization")) ctx := domain.WithUser(c.Request.Context(), claims.UserID, claims.IsAdmin, claims.SessionID)
ctx := domain.WithUser(c.Request.Context(), claims.UserID, claims.IsAdmin) c.Request = c.Request.WithContext(ctx)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
// domain/context.go // domain/context.go
type ctxKey int func WithUser(ctx context.Context, userID int16, isAdmin bool, sessionID int) context.Context
const userKey ctxKey = iota func UserFromContext(ctx context.Context) (userID int16, isAdmin bool, sessionID int)
func WithUser(ctx context.Context, userID int16, isAdmin bool) context.Context { ... }
func UserFromContext(ctx context.Context) (userID int16, isAdmin bool) { ... }
``` ```
### Transaction Management ### Transaction Management
Repository interfaces include a `Transactor`: The `Transactor` port lets services compose multiple repo calls atomically.
The postgres implementation stores the active `pgx.Tx` in the context; repo
methods pick it up via a conn-or-tx helper, so the same method works inside or
outside a transaction.
```go ```go
// port/repository.go // port/repository.go
type Transactor interface { type Transactor interface {
WithTx(ctx context.Context, fn func(ctx context.Context) error) error WithTx(ctx context.Context, fn func(ctx context.Context) error) error
} }
```
The postgres implementation wraps `pgxpool.Pool.BeginTx`. Inside `fn`, // service/file_service.go (sketch)
all repo calls use the transaction from context. This allows services func (s *FileService) Upload(ctx context.Context, p UploadParams) (*domain.File, error) {
to compose multiple repo calls in a single transaction:
```go
// service/file_service.go
func (s *FileService) Upload(ctx context.Context, input UploadInput) (*domain.File, error) {
return s.tx.WithTx(ctx, func(ctx context.Context) error { return s.tx.WithTx(ctx, func(ctx context.Context) error {
file, err := s.fileRepo.Create(ctx, ...) // uses tx created, err := s.files.Create(ctx, f) // uses tx from ctx
if err != nil { return err } // apply initial tags, etc., in the same tx
for _, tagID := range input.TagIDs { return err
s.tagRepo.AddFileTag(ctx, file.ID, tagID) // same tx
}
s.auditRepo.Log(ctx, ...) // same tx
return nil
}) })
} }
``` ```
### ACL Check Pattern ### ACL Check Pattern
ACL logic is centralized in `ACLService`. Other services call it before ACL logic is centralized in `ACLService`. Other services call it before any
any data mutation or retrieval: mutation or retrieval. The model is private-by-default: admins see everything;
otherwise a `public` flag, creator ownership, or an explicit `acl.permissions`
grant is required.
```go ```go
// service/acl_service.go // service/acl_service.go (shape)
func (s *ACLService) CanView(ctx context.Context, objectType string, objectID uuid.UUID) error { func (s *ACLService) CanView(ctx context.Context, userID int16, isAdmin bool,
userID, isAdmin := domain.UserFromContext(ctx) creatorID int16, isPublic bool, objectType int16, objectID uuid.UUID) (bool, error)
if isAdmin { return nil } func (s *ACLService) CanEdit(ctx context.Context, userID int16, isAdmin bool,
// Check is_public on the object creatorID int16, objectType int16, objectID uuid.UUID) (bool, error)
// If not public, check creator_id == userID
// If not creator, check acl.permissions
// Return domain.ErrForbidden if none match
}
``` ```
### Error Mapping ### Error Mapping
Domain errors → HTTP status codes (handled in handler/response.go): Domain errors → HTTP status codes (handled in handler/response.go):
| Domain Error | HTTP Status | Error Code | | Domain Error | HTTP Status | Error Code |
|-----------------------|-------------|-------------------| | ------------------ | ----------- | ---------------- |
| ErrNotFound | 404 | not_found | | ErrNotFound | 404 | not_found |
| ErrForbidden | 403 | forbidden | | ErrForbidden | 403 | forbidden |
| ErrUnauthorized | 401 | unauthorized | | ErrUnauthorized | 401 | unauthorized |
| ErrConflict | 409 | conflict | | ErrConflict | 409 | conflict |
| ErrValidation | 400 | validation_error | | ErrValidation | 400 | validation_error |
| ErrUnsupportedMIME | 415 | unsupported_mime | | ErrUnsupportedMIME | 415 | unsupported_mime |
| (unexpected) | 500 | internal_error | | (unexpected) | 500 | internal_error |
### Filter DSL ### Filter DSL
The DSL parser lives in `db/postgres/filter_parser.go` because it produces The DSL parser lives in `db/postgres/filter_parser.go` because it produces SQL
SQL WHERE clauses — it is a PostgreSQL-specific adapter concern. WHERE clauses — a PostgreSQL-specific adapter concern. The service layer passes
The service layer passes the raw DSL string to the repository; the the raw DSL string down; the repository parses it and builds the query. For a
repository parses it and builds the query. different DBMS, a corresponding parser would live in `db/<dbms>/filter_parser.go`.
For a different DBMS, a corresponding parser would live in
`db/<dbms>/filter_parser.go`.
The interface:
```go ```go
// port/repository.go
type FileRepo interface {
List(ctx context.Context, params FileListParams) (*domain.FilePage, error)
// ...
}
// domain/file.go // domain/file.go
type FileListParams struct { type FileListParams struct {
Filter string // raw DSL string Filter string // raw DSL string
@@ -279,42 +295,38 @@ type FileListParams struct {
} }
``` ```
The DSL grammar itself is documented in `openapi.yaml` (the `filter` query
parameter), so the contract stays in one place.
### JWT Structure ### JWT Structure
```go ```go
type Claims struct { type Claims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
UserID int16 `json:"uid"` UserID int16 `json:"uid"`
IsAdmin bool `json:"adm"` IsAdmin bool `json:"adm"`
SessionID int `json:"sid"` SessionID int `json:"sid"`
} }
``` ```
Access token: short-lived (15 min). Refresh token: long-lived (30 days), Access token: short-lived (15 min default). Refresh token: long-lived (30 days
stored as hash in `activity.sessions.token_hash`. default), rotated on use, stored as a hash in `activity.sessions`. A separate
**content token** (default 6 h) is a single-file capability minted for media
URLs, so a long video keeps streaming past access-token expiry — see
`CONTENT_TOKEN_TTL` in `.env.example`.
### Perceptual Duplicate Detection
Images are dHash-ed inline on upload (`internal/imagehash`); video hashes are
backfilled by the `dedup` CLI (ffmpeg stays off the upload path). A rescan
rebuilds `data.duplicate_pairs` by inserting every pair within
`DUPLICATE_HASH_THRESHOLD` Hamming distance (BK-tree lookups, not O(N²)); the
duplicates API then groups pairs into connected-component clusters. See
`service/duplicate_service.go` and `service/duplicate_index.go`.
### Configuration (.env) ### Configuration (.env)
```env Every variable the server reads is documented in `.env.example` (1:1 with
# Server `config.Config`). Required at startup: `JWT_SECRET`, `ADMIN_PASSWORD`,
LISTEN_ADDR=:42776 `DATABASE_URL`, `FILES_PATH`, `THUMBS_CACHE_PATH`, `IMPORT_PATH`. Everything
JWT_SECRET=<random-32-bytes> else has a sensible default (see `config.go`).
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=720h
# Database
DATABASE_URL=postgres://user:pass@host:5432/tanabata?sslmode=disable
# Storage
FILES_PATH=/data/files
THUMBS_CACHE_PATH=/data/thumbs
# Thumbnails
THUMB_WIDTH=160
THUMB_HEIGHT=160
PREVIEW_WIDTH=1920
PREVIEW_HEIGHT=1080
# Import
IMPORT_PATH=/data/import
```
+179
View File
@@ -0,0 +1,179 @@
# Tanabata File Manager — Requirements
> Product requirements for Tanabata File Manager (TFM). Architecture and code
> layout are described separately in [ARCHITECTURE.md](ARCHITECTURE.md),
> [GO_PROJECT_STRUCTURE.md](GO_PROJECT_STRUCTURE.md) and
> [FRONTEND_STRUCTURE.md](FRONTEND_STRUCTURE.md).
## Overview
Tanabata File Manager (TFM) is a multi-user, tag-based web file manager. It runs
on a clientserver architecture and is operated entirely through a web
interface. Its goal is centralized, server-side storage of files with access and
management from both desktop and mobile browsers. The application is primarily
oriented toward **images and video**.
The app is a PWA that can be installed on a desktop or a phone. Files managed by
Tanabata are stored flat in a single directory; each file's on-disk name equals
its UUID in the database.
Support for additional database engines is planned for future versions.
## Core Concepts
- **File** — a single file on the server. It may carry any number of tags and
belong to any number of pools. It has a creator and optional access settings
(a user — which may be null, making the file public — plus read and edit
permission flags). It has an original name and metadata (keyvalue, including
all EXIF data).
- **Tag** — a label on a file. It may be attached to any number of files and
belong to at most one category. It has a name, a description, keyvalue
metadata, and may define auto-tag rules.
- **Auto-tag (tag rule)** — a rule stating that when tag A is attached to a file,
tag B is attached to the same file automatically.
- **Category** — an entity that logically groups several tags. It has a name, a
description, and keyvalue metadata.
- **Pool** — a logical grouping of files. It has a name, a description, and
keyvalue metadata. Files in a pool can be sorted automatically or arranged in
a user-defined manual order.
## Functional Requirements
### 1. File management
1. Browse the file list (lazy load, pagination).
2. Filter files by tags and metadata.
3. View and edit sort settings (persisted per user).
4. Multi-select files (Ctrl, Shift) and act on the selection:
1. Attach / detach tags.
2. Copy / paste tags.
3. Add to a pool.
4. View and edit access settings.
5. Delete (with a confirmation prompt).
5. View a single file.
6. Single-file actions:
1. Attach / detach tags.
2. Copy / paste tags.
3. Add to a pool.
4. View and edit access settings.
5. Replace the file (upload new content under the same ID).
6. Delete (with a confirmation prompt).
7. Browse files gallery-style (prev/next paging through the viewer).
8. Upload new files through the web UI (form or drag-and-drop onto the list).
9. Import new files from a folder on the server.
10. Near-duplicate detection for images and video:
1. Show groups (clusters) of duplicates.
2. Dismiss false duplicates (the app remembers that file A is _not_ a
duplicate of file B).
3. Choose which duplicate to keep and which to delete.
4. Choose, per field, which duplicate the surviving file inherits it from.
11. Trash:
1. Browse trashed files.
2. Restore from trash.
3. Delete permanently.
### 2. Tag management
1. Browse the tag list (lazy load, pagination).
2. Search by name.
3. View and edit sort settings (persisted per user).
4. Multi-select tags (Ctrl, Shift) and act on the selection:
1. Assign auto-tag rules.
2. Change category.
3. Delete (with a confirmation prompt).
5. View a single tag.
6. Single-tag actions:
1. Edit name, description, and metadata (keyvalue).
2. Change category.
3. Assign auto-tag rules.
4. Delete (with a confirmation prompt).
7. Create a tag:
1. Enter name, description, and metadata (keyvalue).
2. Assign a category (optional).
3. Assign auto-tag rules.
### 3. Category management
1. Browse the category list (lazy load, pagination).
2. Search by name.
3. View and edit sort settings (persisted per user).
4. Multi-select categories (Ctrl, Shift) and act on the selection:
1. View shared tags and tags attached to some (but not all) of them.
2. Attach / detach tags.
3. Delete (with a confirmation prompt).
5. View a single category.
6. Single-category actions:
1. Edit name, description, and metadata (keyvalue).
2. View attached tags.
3. Attach / detach tags.
4. Delete (with a confirmation prompt).
7. Create a category:
1. Enter name, description, and metadata (keyvalue).
2. Attach tags.
### 4. Pool management
1. Browse the pool list (lazy load, pagination).
2. Search by name.
3. View and edit sort settings (persisted per user).
4. Multi-select pools (Ctrl, Shift) and act on the selection:
1. View and edit access settings.
2. Delete (with a confirmation prompt).
5. View a single pool.
6. Single-pool actions:
1. Edit name, description, and metadata (keyvalue).
2. View and edit access settings.
3. View all files in the pool.
4. Filter the pool's files by tags.
5. Change the file sort setting (including disabling automatic sorting).
6. Reorder files manually (when automatic sorting is disabled).
7. Delete (with a confirmation prompt).
7. Create a pool:
1. Enter name, description, and metadata (keyvalue).
2. Attach files.
### 5. User settings
1. Username.
2. Password.
3. Sessions:
1. Terminate a session.
4. Path to the server folder scanned during file import.
### 6. Server administration (admin panel)
1. Users:
1. Browse the list.
2. View a single user.
3. Create.
4. Delete.
5. Block / unblock.
6. Set role (reader / editor).
### 7. Audit logging (in the database)
Log the following user actions:
1. File views.
2. Changes to file access settings.
3. Create / edit / delete of a file, tag, category, pool, or filetag relation.
4. Create / block / unblock / delete of a user.
5. User role changes.
6. User login / logout.
7. Session termination.
## Non-Functional Requirements
1. The interface must be as simple and convenient as possible: everything needed
should be at hand, reachable in the fewest possible actions.
2. The interface must adapt to both desktop and mobile devices.
3. The interface must offer dark and light themes.
4. Use PWA technology, including a button that fully resets the PWA (except the
cache) and reloads it from the server.
5. Allow selected files to be cached and viewed offline in the installed PWA.
6. First-run setup must require minimal effort: automatic database migration, a
ready-made Docker Compose file, and a `.env` file with the configurable
installation parameters.
7. Use a Domain-Driven Design approach on the API server.
8. Reject files whose MIME type is not present in the database (no DB entry — no
support).
-148
View File
@@ -1,148 +0,0 @@
## О проекте
Tanabata File Manager или сокращенно TFM — многопользовательский веб-файловый менеджер, организующий файлы по тегам. Работает на клиент-серверной архитектуре, управляется через веб-интерфейс. Главная цель проекта — обеспечить централизованное хранение файлов на сервере, доступ к ним и управление ими через веб как с компьютера, так и со смартфона. В первую очередь данное приложение ориентировано на изображения и видео.
## Общая архитектура
- File storage
- Relational database (PostgreSQL)
- REST API service (Go)
- Frontend (SvelteKit)
Приложение предполагается разворачивать внутри контейнера Docker. Фронтенд и бэкенд - в одном контейнере, СУБД - отдельно (на моем сервере планируется подключать к СУБД на хосте). Все файлы, управляемые Танабатой, будут храниться кучей в одной папке. Имя файла на диске совпадает с его UUID в БД.
Приложение является PWA, которое можно установить на компьютер или смартфон.
В будущих версиях планируется введение поддержки других СУБД.
## Основные понятия
**Файл** — один файл на сервере. Может иметь сколько угодно тегов, может принадлежать скольким угодно пулам. Имеет автора, а также может иметь настройки доступа (пользователь (может быть null - таким образом можно делать файл публичным), флаг права на чтение, флаг права на изменение). Имеет оригинальное название и метаданные (ключ-значение, в том числе все данные EXIF).
**Тег** — метка файла. Может быть привязан к скольким угодно файлам, может быть привязан к одной категории. Имеет название, описание, метаданные (ключ-значение). Может иметь автотеги.
**Автотег** — правило, согласно которому при привязке к файлу условного тега А к этому же файлу автоматически привязывается условный тег Б.
**Категория** — сущность, логически объединяющая собой несколько тегов. Имеет название, описание, метаданные (ключ-значение).
**Пул** — логическое объединение файлов. Имеет название, описание, метаданные (ключ-значение). Файлы внутри могут быть как отсортированы автоматически, так и расположены в порядке, заданном пользователем вручную.
## Функциональные требования
1. Управление файлами
1. Просмотр списка файлов (lazy load, pagination)
2. Фильтрация файлов по тегам и метаданным
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
4. Выделение нескольких файлов (Ctrl, Shift) и действия с ними
1. Привязка/отвязка тегов
2. Копирование/вставка тегов
3. Добавление в пул
4. Просмотр и редактирование настроек доступа
5. Удаление (с запросом подтверждения)
5. Просмотр одного файла
6. Действия с одним файлом
1. Привязка/отвязка тегов
2. Копирование/вставка тегов
3. Добавление в пул
4. Просмотр и редактирование настроек доступа
5. Замена файла (загрузка нового под таким же ID)
6. Удаление (с запросом подтверждения)
7. Листание файлов, как в галерее
8. Загрузка новых файлов через веб-интерфейс (через форму или drag-n-drop прямо на список)
9. Импорт новых файлов из папки на сервере
10. Выявление дубликатов, в частности, изображений и видео
1. Отображение групп дубликатов
2. Возможность отвязывания фальшивых дубликатов (чтобы приложение запомнило, что изображение А не является дубликатом изображения Б)
3. Возможность выбора дубликата для удаления/сохранения
4. Возможность выбора, какие поля от какого дубликата подтягивать
11. Корзина
1. Просмотр файлов в корзине
2. Восстановление из корзины
3. Окончательное удаление
2. Управление тегами
1. Просмотр списка тегов (lazy load, pagination)
2. Поиск по названию
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
4. Выделение нескольких тегов (Ctrl, Shift) и действия с ними
1. Назначение автотегов
2. Изменение категории
3. Удаление (с запросом подтверждения)
5. Просмотр одного тега
6. Действия с одним тегом
1. Редактирование названия, описания и метаданных (ключ-значение)
2. Изменение категории
3. Назначение автотегов
4. Удаление (с запросом подтверждения)
7. Создание тега
1. Внесение названия, описания и метаданных (ключ-значение)
2. Назначение категории (опционально)
3. Назначение автотегов
3. Управление категориями
1. Просмотр списка категорий (lazy load, pagination)
2. Поиск по названию
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
4. Выделение нескольких категорий (Ctrl, Shift) и действия с ними
1. Просмотр привязанных общих тегов и тегов, привязанных к некоторым, но не ко всем
2. Привязка/отвязка тегов
3. Удаление (с запросом подтверждения)
5. Просмотр одной категории
6. Действия с одной категорией
1. Редактирование названия, описания и метаданных (ключ-значение)
2. Просмотр привязанных тегов
3. Привязка/отвязка тегов
4. Удаление (с запросом подтверждения)
7. Создание категории
1. Внесение названия, описания и метаданных (ключ-значение)
2. Привязка тегов
4. Управление пулами
1. Просмотр списка пулов (lazy load, pagination)
2. Поиск по названию
3. Просмотр и редактирование настроек сортировки (сохраняется для каждого пользователя)
4. Выделение нескольких пулов (Ctrl, Shift) и действия с ними
1. Просмотр и редактирование настроек доступа
2. Удаление (с запросом подтверждения)
5. Просмотр одного пула
6. Действия с одним пулом
1. Редактирование названия, описания и метаданных (ключ-значение)
2. Просмотр и редактирование настроек доступа
3. Просмотр всех файлов, входящих в пул
4. Фильтрация файлов по тегам
5. Изменение настройки сортировки файлов (в том числе можно отключить автоматическую сортировку)
6. Ручное изменение порядка файлов (при отключенной сортировке)
7. Удаление (с запросом подтверждения)
7. Создание категории
1. Внесение названия, описания и метаданных (ключ-значение)
2. Привязка тегов
5. Управление пользовательскими настройками
1. Имя пользователя
2. Пароль
3. Сессии
1. Завершение сессии
4. Путь к папке на сервере, которая будет сканироваться при импорта файлов
6. Управление настройками сервера (админка)
1. Пользователи
1. Просмотр списка
2. Просмотр одного
3. Создание
4. Удаление
5. Блокировка/разблокировка
6. Установка роли (читатель/редактор)
7. Журналирование пользовательских действий в БД
1. Просмотры файлов
2. Смены настроек доступа к файлам
3. Создание/редактирование/удаление файла, тега, категории, пула, связи файл-тег
4. Создание/блокировка/разблокировка/удаление пользователя
5. Смена роли пользователя
6. Авторизация/логаут пользователя
7. Завершение сессии
## Нефункциональные требования
1. Интерфейс должен быть максимально простым и удобным, все необходимое должно быть под рукой, доступным за минимальное количество действий
2. Интерфейс должен быть адаптирован под десктоп и под мобильные устройства
3. Интерфейс должен иметь темную и светлую темы
4. Использование технологии PWA (также должна быть кнопка, при нажатии которой PWA будет полностью сбрасываться (кроме кэша) и заново загружаться с сервера)
5. Возможность сохранять некоторые файлы в кэш и просматривать их оффлайн при использовании установленного PWA
6. При первичном запуске приложение должно требовать минимума действий: автоматическая миграция БД, заранее готовый файл docker compose, файл .env с настраиваемыми параметрами установки
7. Использование подхода DDD для сервера API
8. Не принимать файлы, чей MIME отсутствует в БД (нет в БД — нет поддержки)
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.1", "version": "3.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts", "generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts",
+34 -3
View File
@@ -5,8 +5,9 @@ info:
REST API for Tanabata File Manager — a multi-user, tag-based web file manager. REST API for Tanabata File Manager — a multi-user, tag-based web file manager.
## Authentication ## Authentication
All endpoints except `POST /auth/login` require a Bearer JWT token All endpoints require a Bearer JWT token in the `Authorization` header,
in the `Authorization` header. except `POST /auth/login`, `POST /auth/refresh` (which carry their own
credentials) and `GET /health`.
## Pagination ## Pagination
- **Files**: cursor-based (`cursor` parameter, returned in `next_cursor`). - **Files**: cursor-based (`cursor` parameter, returned in `next_cursor`).
@@ -32,7 +33,7 @@ info:
Example: `{t=uuid1,&,!,t=uuid2}` → has tag1 AND NOT tag2. Example: `{t=uuid1,&,!,t=uuid2}` → has tag1 AND NOT tag2.
Example: `{r=1,&,m~image%}` → needs review AND is an image. Example: `{r=1,&,m~image%}` → needs review AND is an image.
version: 1.0.0 version: 3.0.0
license: license:
name: Proprietary name: Proprietary
@@ -59,12 +60,42 @@ tags:
description: User management (admin) description: User management (admin)
- name: Audit - name: Audit
description: Audit log (admin) description: Audit log (admin)
- name: System
description: Service health and liveness
# =========================================================================== # ===========================================================================
# Paths # Paths
# =========================================================================== # ===========================================================================
paths: paths:
# -------------------------------------------------------------------------
# System
# -------------------------------------------------------------------------
/health:
# Served at the server root, outside the /api/v1 prefix — override the
# global server so the documented path is /health, not /api/v1/health.
servers:
- url: /
get:
tags: [System]
summary: Health check
description: |
Liveness probe. Requires no authentication and is used by the container
HEALTHCHECK. Always returns 200 while the process is serving.
security: []
responses:
'200':
description: Service is healthy.
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
example: ok
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Auth # Auth
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------