chore(project): initial project structure

This commit is contained in:
2026-04-01 16:17:37 +03:00
commit dbdc80b3a0
6 changed files with 3364 additions and 0 deletions
+383
View File
@@ -0,0 +1,383 @@
# Tanabata File Manager — Frontend Structure
## Stack
- **Framework**: SvelteKit (SPA mode, `ssr: false`)
- **Language**: TypeScript
- **CSS**: Tailwind CSS + CSS custom properties (hybrid)
- **API types**: Auto-generated via openapi-typescript
- **PWA**: Service worker + web manifest
- **Font**: Epilogue (variable weight)
- **Package manager**: npm
## Monorepo Layout
```
tanabata/
├── backend/ ← Go project (go.mod in here)
│ ├── cmd/
│ ├── internal/
│ ├── migrations/
│ ├── go.mod
│ └── go.sum
├── frontend/ ← SvelteKit project (package.json in here)
│ └── (see below)
├── openapi.yaml ← Shared API contract (root level)
├── docker-compose.yml
├── Dockerfile
├── .env.example
└── README.md
```
`openapi.yaml` lives at repository root — both backend and frontend
reference it. The frontend generates types from it; the backend
validates its handlers against it.
## Frontend Directory Layout
```
frontend/
├── package.json
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
├── tailwind.config.ts
├── postcss.config.js
├── src/
│ ├── app.html # Shell HTML (PWA meta, font preload)
│ ├── app.css # Tailwind directives + CSS custom properties
│ ├── hooks.server.ts # Server hooks (not used in SPA mode)
│ ├── hooks.client.ts # Client hooks (global error handling)
│ │
│ ├── lib/ # Shared code ($lib/ alias)
│ │ │
│ │ ├── api/ # API client layer
│ │ │ ├── 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/
├── favicon.png
├── favicon.ico
├── manifest.webmanifest # PWA manifest (name, icons, theme_color)
├── images/
│ ├── tanabata-left.png # Login page decorations (from current design)
│ ├── tanabata-right.png
│ └── icons/ # PWA icons (192×192, 512×512, etc.)
└── fonts/
└── Epilogue-VariableFont_wght.ttf
```
## Key Architecture Decisions
### CSS Hybrid: Tailwind + Custom Properties
Theme colors defined as CSS custom properties in `app.css`:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-bg-primary: #312F45;
--color-bg-secondary: #181721;
--color-bg-elevated: #111118;
--color-accent: #9592B5;
--color-accent-hover: #7D7AA4;
--color-text-primary: #f0f0f0;
--color-text-muted: #9999AD;
--color-danger: #DB6060;
--color-info: #4DC7ED;
--color-warning: #F5E872;
--color-tag-default: #444455;
}
:root[data-theme="light"] {
--color-bg-primary: #f5f5f5;
--color-bg-secondary: #ffffff;
/* ... */
}
```
Tailwind references them in `tailwind.config.ts`:
```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">`.
Complex cases use scoped `<style>` inside `.svelte` files.
### API Client Pattern
`client.ts` — thin wrapper around fetch:
```ts
// $lib/api/client.ts
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 = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body?: unknown) =>
request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
patch: <T>(path: string, body?: unknown) =>
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
put: <T>(path: string, body?: unknown) =>
request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
upload: <T>(path: string, formData: FormData) =>
request<T>(path, { method: 'POST', body: formData, headers: {} }),
};
```
Domain-specific modules use it:
```ts
// $lib/api/files.ts
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
Script in `package.json`:
```json
{
"scripts": {
"generate:types": "openapi-typescript ../openapi.yaml -o src/lib/api/schema.ts",
"dev": "npm run generate:types && vite dev",
"build": "npm run generate:types && vite build"
}
}
```
Friendly aliases in `types.ts`:
```ts
import type { components } from './schema';
export type File = components['schemas']['File'];
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
`svelte.config.js`:
```js
import adapter from '@sveltejs/adapter-static';
export default {
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).
In development, Vite dev server proxies `/api` to the Go backend.
### PWA
`service-worker.ts` handles:
- App shell caching (HTML, CSS, JS, fonts)
- User-pinned file caching (explicit, via UI button)
- Cache versioning and cleanup on update
- Reset function (clear all caches except pinned files)
+320
View File
@@ -0,0 +1,320 @@
# Tanabata File Manager — Go Project Structure
## Stack
- **Router**: Gin
- **Database**: pgx v5 (pgxpool)
- **Migrations**: goose v3 + go:embed (auto-migrate on startup)
- **Auth**: JWT (golang-jwt/jwt/v5)
- **Config**: environment variables via .env (joho/godotenv)
- **Logging**: slog (stdlib, Go 1.21+)
- **Validation**: go-playground/validator/v10
- **EXIF**: rwcarlsen/goexif or dsoprea/go-exif
- **Image processing**: disintegration/imaging (thumbnails, previews)
- **Architecture**: Clean Architecture (domain → service → repository/handler)
## Monorepo Layout
```
tanabata/
├── backend/ ← Go project
├── frontend/ ← SvelteKit project
├── openapi.yaml ← Shared API contract
├── docker-compose.yml
├── Dockerfile
├── .env.example
└── README.md
```
## Backend Directory Layout
```
backend/
├── cmd/
│ └── server/
│ └── main.go # Entrypoint: config → DB → migrate → wire → run
├── internal/
│ │
│ ├── domain/ # Pure business entities & value objects
│ │ ├── file.go # File, FileFilter, FilePage
│ │ ├── tag.go # Tag, TagRule
│ │ ├── category.go # Category
│ │ ├── pool.go # Pool, PoolFile
│ │ ├── user.go # User, Session
│ │ ├── acl.go # Permission, ObjectType
│ │ ├── audit.go # AuditEntry, ActionType
│ │ └── errors.go # Domain error types (ErrNotFound, ErrForbidden, etc.)
│ │
│ ├── port/ # Interfaces (ports) — contracts between layers
│ │ ├── repository.go # FileRepo, TagRepo, CategoryRepo, PoolRepo,
│ │ │ # UserRepo, SessionRepo, ACLRepo, AuditRepo,
│ │ │ # MimeRepo, TagRuleRepo
│ │ └── storage.go # FileStorage interface (disk operations)
│ │
│ ├── service/ # Business logic (use cases)
│ │ ├── file_service.go # Upload, update, delete, trash/restore, replace,
│ │ │ # import, filter/list, duplicate detection
│ │ ├── tag_service.go # CRUD + auto-tag application logic
│ │ ├── category_service.go # CRUD (thin, delegates to repo + ACL + audit)
│ │ ├── pool_service.go # CRUD + file ordering, add/remove files
│ │ ├── auth_service.go # Login, logout, JWT issue/refresh, session management
│ │ ├── acl_service.go # Permission checks, grant/revoke
│ │ ├── audit_service.go # Log actions, query audit log
│ │ └── user_service.go # Profile update, admin CRUD, block/unblock
│ │
│ ├── handler/ # HTTP layer (Gin handlers)
│ │ ├── router.go # Route registration, middleware wiring
│ │ ├── middleware.go # Auth middleware (JWT extraction → context)
│ │ ├── request.go # Common request parsing helpers
│ │ ├── response.go # Error/success response builders
│ │ ├── file_handler.go # /files endpoints
│ │ ├── tag_handler.go # /tags endpoints
│ │ ├── category_handler.go # /categories endpoints
│ │ ├── pool_handler.go # /pools endpoints
│ │ ├── auth_handler.go # /auth endpoints
│ │ ├── acl_handler.go # /acl endpoints
│ │ ├── user_handler.go # /users endpoints
│ │ └── audit_handler.go # /audit endpoints
│ │
│ ├── db/ # Database adapters
│ │ ├── db.go # Common helpers: pagination, repo factory, transactor base
│ │ └── postgres/ # PostgreSQL implementation
│ │ ├── postgres.go # pgxpool init, tx-from-context helpers
│ │ ├── file_repo.go # FileRepo implementation
│ │ ├── tag_repo.go # TagRepo + TagRuleRepo implementation
│ │ ├── category_repo.go # CategoryRepo implementation
│ │ ├── pool_repo.go # PoolRepo implementation
│ │ ├── user_repo.go # UserRepo implementation
│ │ ├── session_repo.go # SessionRepo implementation
│ │ ├── acl_repo.go # ACLRepo implementation
│ │ ├── audit_repo.go # AuditRepo implementation
│ │ ├── mime_repo.go # MimeRepo implementation
│ │ └── filter_parser.go # DSL → SQL WHERE clause builder
│ │
│ ├── storage/ # File storage adapter
│ │ └── disk.go # FileStorage implementation (read/write/delete on disk)
│ │
│ └── config/ # Configuration
│ └── config.go # Struct + loader from env vars
├── migrations/ # SQL migration files (goose format)
│ ├── 001_init_schemas.sql
│ ├── 002_core_tables.sql
│ ├── 003_data_tables.sql
│ ├── 004_acl_tables.sql
│ ├── 005_activity_tables.sql
│ ├── 006_indexes.sql
│ └── 007_seed_data.sql
├── go.mod
└── go.sum
```
## Layer Dependency Rules
```
handler → service → port (interfaces) ← db/postgres / storage
domain (entities, value objects, errors)
```
- **domain/**: zero imports from other internal packages. Only stdlib.
- **port/**: imports only domain/. Defines interfaces.
- **service/**: imports domain/ and port/. Never imports db/ or handler/.
- **handler/**: imports domain/ and service/. Never imports db/.
- **db/postgres/**: imports domain/, port/, and db/ (common helpers). Implements port interfaces.
- **db/**: imports domain/ and port/. Shared utilities for all DB adapters.
- **storage/**: imports domain/ and port/. Implements FileStorage.
No layer may import a layer above it. No circular dependencies.
## Key Design Decisions
### Dependency Injection (Wiring)
Manual wiring in `cmd/server/main.go`. No DI frameworks.
```go
// Pseudocode
pool := postgres.NewPool(cfg.DatabaseURL)
goose.Up(pool, migrations)
// Repos (all from internal/db/postgres/)
fileRepo := postgres.NewFileRepo(pool)
tagRepo := postgres.NewTagRepo(pool)
// ...
// Storage
diskStore := storage.NewDiskStorage(cfg.FilesPath)
// Services
aclSvc := service.NewACLService(aclRepo, objectTypeRepo)
auditSvc := service.NewAuditService(auditRepo, actionTypeRepo)
fileSvc := service.NewFileService(fileRepo, mimeRepo, tagRepo, diskStore, aclSvc, auditSvc)
tagSvc := service.NewTagService(tagRepo, tagRuleRepo, aclSvc, auditSvc)
// ...
// Handlers
fileHandler := handler.NewFileHandler(fileSvc, tagSvc)
// ...
router := handler.NewRouter(cfg, fileHandler, tagHandler, ...)
router.Run(cfg.ListenAddr)
```
### Context Propagation
Every service method receives `context.Context` as the first argument.
The handler extracts user info from JWT (via middleware) and puts it
into context. Services read the current user from context for ACL checks
and audit logging.
```go
// middleware.go
func (m *AuthMiddleware) Handle(c *gin.Context) {
claims := parseJWT(c.GetHeader("Authorization"))
ctx := domain.WithUser(c.Request.Context(), claims.UserID, claims.IsAdmin)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
// domain/context.go
type ctxKey int
const userKey ctxKey = iota
func WithUser(ctx context.Context, userID int16, isAdmin bool) context.Context { ... }
func UserFromContext(ctx context.Context) (userID int16, isAdmin bool) { ... }
```
### Transaction Management
Repository interfaces include a `Transactor`:
```go
// port/repository.go
type Transactor interface {
WithTx(ctx context.Context, fn func(ctx context.Context) error) error
}
```
The postgres implementation wraps `pgxpool.Pool.BeginTx`. Inside `fn`,
all repo calls use the transaction from context. This allows services
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 {
file, err := s.fileRepo.Create(ctx, ...) // uses tx
if err != nil { return err }
for _, tagID := range input.TagIDs {
s.tagRepo.AddFileTag(ctx, file.ID, tagID) // same tx
}
s.auditRepo.Log(ctx, ...) // same tx
return nil
})
}
```
### ACL Check Pattern
ACL logic is centralized in `ACLService`. Other services call it before
any data mutation or retrieval:
```go
// service/acl_service.go
func (s *ACLService) CanView(ctx context.Context, objectType string, objectID uuid.UUID) error {
userID, isAdmin := domain.UserFromContext(ctx)
if isAdmin { return nil }
// Check is_public on the object
// If not public, check creator_id == userID
// If not creator, check acl.permissions
// Return domain.ErrForbidden if none match
}
```
### Error Mapping
Domain errors → HTTP status codes (handled in handler/response.go):
| Domain Error | HTTP Status | Error Code |
|-----------------------|-------------|-------------------|
| ErrNotFound | 404 | not_found |
| ErrForbidden | 403 | forbidden |
| ErrUnauthorized | 401 | unauthorized |
| ErrConflict | 409 | conflict |
| ErrValidation | 400 | validation_error |
| ErrUnsupportedMIME | 415 | unsupported_mime |
| (unexpected) | 500 | internal_error |
### Filter DSL
The DSL parser lives in `db/postgres/filter_parser.go` because it produces
SQL WHERE clauses — it is a PostgreSQL-specific adapter concern.
The service layer passes the raw DSL string to the repository; the
repository parses it and builds the query.
For a different DBMS, a corresponding parser would live in
`db/<dbms>/filter_parser.go`.
The interface:
```go
// port/repository.go
type FileRepo interface {
List(ctx context.Context, params FileListParams) (*domain.FilePage, error)
// ...
}
// domain/file.go
type FileListParams struct {
Filter string // raw DSL string
Sort string
Order string
Cursor string
Anchor *uuid.UUID
Direction string // "forward" or "backward"
Limit int
Trash bool
Search string
}
```
### JWT Structure
```go
type Claims struct {
jwt.RegisteredClaims
UserID int16 `json:"uid"`
IsAdmin bool `json:"adm"`
SessionID int `json:"sid"`
}
```
Access token: short-lived (15 min). Refresh token: long-lived (30 days),
stored as hash in `activity.sessions.token_hash`.
### Configuration (.env)
```env
# Server
LISTEN_ADDR=:8080
JWT_SECRET=<random-32-bytes>
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
```
+148
View File
@@ -0,0 +1,148 @@
## О проекте
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 отсутствует в БД (нет в БД — нет поддержки)