228 lines
9.9 KiB
Markdown
228 lines
9.9 KiB
Markdown
# T04 — DMS Plugin Backend (Folders, Files, Preview, OnlyOffice, Share Links)
|
|
|
|
## Project Root
|
|
/a0/usr/workdir/dev-projects/leocrm
|
|
|
|
## Context Files (READ FIRST)
|
|
- `app/plugins/base.py` — BasePlugin abstract class
|
|
- `app/plugins/manifest.py` — PluginManifest, PluginRouteDef
|
|
- `app/plugins/builtins/tags/` — Reference plugin (subdirectory pattern)
|
|
- `app/plugins/builtins/permissions/` — Already has share links + file permissions
|
|
- `app/plugins/builtins/entity_links/` — Links files to companies/contacts
|
|
- `app/core/db.py` — Base, TenantMixin, TimestampMixin
|
|
- `app/models/company.py` — Model pattern reference
|
|
- `app/routes/companies.py` — Route pattern reference
|
|
- `app/schemas/company.py` — Schema pattern reference
|
|
- `architecture.md` — Architecture decisions
|
|
- `requirements.md` — F-DMS-*, F-FILE-*, F-FILEUI-* requirements
|
|
|
|
## Overview
|
|
Implement DMS (Document Management System) plugin as `app/plugins/builtins/dms/`.
|
|
|
|
## Plugin Structure
|
|
```
|
|
app/plugins/builtins/dms/
|
|
├── __init__.py # Export DmsPlugin
|
|
├── plugin.py # DmsPlugin(BasePlugin) with manifest
|
|
├── models.py # Folder, File models
|
|
├── schemas.py # Pydantic schemas for all endpoints
|
|
├── routes.py # FastAPI APIRouter with all endpoints
|
|
└── migrations/
|
|
└── 0001_initial.sql # Create folders + files tables
|
|
```
|
|
|
|
## Models
|
|
|
|
### Folder
|
|
- id (UUID, PK)
|
|
- name (str, not null)
|
|
- parent_id (UUID, FK to folders.id, nullable — null = root)
|
|
- tenant_id (UUID, not null)
|
|
- created_by (UUID, not null)
|
|
- deleted_at (datetime, nullable — soft delete)
|
|
- created_at, updated_at (TimestampMixin)
|
|
- **Unique constraint**: (name, parent_id, tenant_id) where deleted_at IS NULL
|
|
|
|
### File
|
|
- id (UUID, PK)
|
|
- name (str, not null)
|
|
- folder_id (UUID, FK to folders.id, nullable — null = root)
|
|
- tenant_id (UUID, not null)
|
|
- uploaded_by (UUID, not null)
|
|
- mime_type (str, not null)
|
|
- size_bytes (int, not null)
|
|
- storage_path (str, not null — relative path on disk)
|
|
- deleted_at (datetime, nullable — soft delete)
|
|
- created_at, updated_at (TimestampMixin)
|
|
|
|
## File Storage
|
|
- Store files at: `/data/dms/{tenant_id}/{file_uuid}` (configurable via plugin config)
|
|
- Use `shutil.copyfileobj` for upload streaming
|
|
- Generate UUID for filename on disk, keep original name in DB
|
|
- Create directory with `os.makedirs(path, exist_ok=True)`
|
|
|
|
## Endpoints (19 ACs)
|
|
|
|
### Folders
|
|
1. `GET /api/v1/dms/folders` → 200, folder tree (recursive tree structure)
|
|
- Query param `parent_id` (optional, null = root level)
|
|
- Returns list of folders with children nested
|
|
2. `POST /api/v1/dms/folders` → 201, create folder
|
|
- Body: `{name, parent_id?}`
|
|
- Returns created folder with full path
|
|
3. `PATCH /api/v1/dms/folders/{id}` → 200, rename/move folder
|
|
- Body: `{name?, parent_id?}`
|
|
4. `DELETE /api/v1/dms/folders/{id}` → 204, soft-delete (set deleted_at)
|
|
- Cascade: soft-delete all child folders and files
|
|
|
|
### Files
|
|
5. `POST /api/v1/dms/files/upload` → 201, multipart upload
|
|
- Form fields: `file` (UploadFile), `folder_id?` (optional)
|
|
- Store file on disk, create metadata record
|
|
- Max file size: 100MB (configurable)
|
|
6. `GET /api/v1/dms/files/{id}` → 200, file metadata
|
|
7. `PATCH /api/v1/dms/files/{id}` → 200, rename/move
|
|
- Body: `{name?, folder_id?}`
|
|
8. `DELETE /api/v1/dms/files/{id}` → 204, soft-delete
|
|
9. `POST /api/v1/dms/files/{id}/restore` → 200, restore from trash
|
|
|
|
### Preview & Edit
|
|
10. `GET /api/v1/dms/files/{id}/preview` → 200, PDF stream
|
|
- Only for PDF files (mime_type == application/pdf)
|
|
- Return `StreamingResponse` with `media_type='application/pdf'`
|
|
- Non-PDF files: return 400
|
|
11. `POST /api/v1/dms/files/{id}/edit-session` → 200, OnlyOffice config
|
|
- Return JSON config for OnlyOffice editor:
|
|
```json
|
|
{
|
|
"document": {"fileType": "docx", "key": "<uuid>", "title": "<filename>", "url": "<download_url>"},
|
|
"editorConfig": {"mode": "edit", "callbackUrl": "<callback_url>", "user": {"id": "<user_id>", "name": "<user_name>"}}
|
|
}
|
|
```
|
|
- Only for Office files (docx, xlsx, pptx)
|
|
- Non-Office files: return 400
|
|
|
|
### Sharing (INTERNAL — different from T11 permissions plugin)
|
|
**NOTE**: T11 permissions plugin already handles:
|
|
- `POST /api/v1/dms/files/{id}/share-link` — public share links with token
|
|
- `GET /api/public/share/{token}` — public access
|
|
- `POST /api/v1/dms/files/{id}/permissions` — grant permissions
|
|
|
|
T04 DMS plugin handles INTERNAL sharing (different endpoints):
|
|
12. `POST /api/v1/dms/files/{id}/share` → 200, internal share
|
|
- Body: `{user_ids?: [uuid], group_ids?: [uuid], access_level: 'read'|'write'}`
|
|
- Creates permission records (reuse permissions plugin Permission model OR DMS-specific)
|
|
13. `DELETE /api/v1/dms/files/{id}/share` → 204, remove share
|
|
- Body: `{user_id?: uuid, group_id?: uuid}`
|
|
|
|
**IMPORTANT**: For `GET /api/public/share/{token}` (AC14, AC15) — T11 permissions plugin ALREADY implements this endpoint. Do NOT create a duplicate. If T11's endpoint already handles password-protected links (401 without password), then AC14 and AC15 are already satisfied. Verify by reading `app/plugins/builtins/permissions/routes.py`.
|
|
|
|
### Search & Bulk
|
|
14. `GET /api/v1/dms/search?q=text` → 200, matching files
|
|
- Case-insensitive filename search with ILIKE
|
|
- Search across all files in tenant (not deleted)
|
|
15. `GET /api/v1/dms/shared-with-me` → 200, shared files list
|
|
- Files where user has been granted permission (via permissions plugin)
|
|
16. `POST /api/v1/dms/files/bulk-move` → 200
|
|
- Body: `{file_ids: [uuid], target_folder_id: uuid?}`
|
|
17. `POST /api/v1/dms/files/bulk-delete` → 200
|
|
- Body: `{file_ids: [uuid]}` — soft-delete all
|
|
|
|
## Migration SQL
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS folders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL,
|
|
created_by UUID NOT NULL,
|
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
CREATE INDEX idx_folders_parent ON folders(parent_id) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_folders_tenant ON folders(tenant_id) WHERE deleted_at IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
|
|
tenant_id UUID NOT NULL,
|
|
uploaded_by UUID NOT NULL,
|
|
mime_type VARCHAR(255) NOT NULL,
|
|
size_bytes BIGINT NOT NULL,
|
|
storage_path VARCHAR(1024) NOT NULL,
|
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
CREATE INDEX idx_files_folder ON files(folder_id) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_files_tenant ON files(tenant_id) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_files_name ON files USING gin (to_tsvector('simple', name));
|
|
```
|
|
|
|
## Register Plugin
|
|
Add to `app/plugins/builtins/__init__.py`:
|
|
```python
|
|
from app.plugins.builtins.dms import DmsPlugin
|
|
__all__.append("DmsPlugin")
|
|
```
|
|
|
|
## Test File
|
|
Create `tests/test_dms.py` with tests for ALL 19 ACs.
|
|
|
|
### Test Patterns
|
|
- Follow existing test patterns in `tests/test_tags.py`, `tests/test_permissions.py`
|
|
- Use async test client via `httpx.AsyncClient` with `ASGITransport`
|
|
- Use existing fixtures from `tests/conftest.py`
|
|
- For file upload tests: use `httpx.AsyncClient.post` with `files={'file': ('test.pdf', b'%PDF-1.4...', 'application/pdf')}`
|
|
- For preview tests: verify response status 200 + content-type application/pdf
|
|
- For OnlyOffice: verify config structure returned
|
|
- For bulk operations: create multiple files, then bulk-move/bulk-delete
|
|
|
|
## Verification Commands
|
|
```bash
|
|
cd /a0/usr/workdir/dev-projects/leocrm
|
|
python -m pytest tests/test_dms.py -v --tb=short
|
|
python -m pytest tests/test_dms.py --cov=app/plugins/builtins/dms --cov-report=term-missing
|
|
```
|
|
|
|
## Acceptance Criteria (19 — ALL must pass)
|
|
1. GET /api/v1/dms/folders → 200 + folder tree
|
|
2. POST /api/v1/dms/folders → 201, folder created with path
|
|
3. PATCH /api/v1/dms/folders/{id} → 200, folder renamed/moved
|
|
4. DELETE /api/v1/dms/folders/{id} → 204, soft-delete
|
|
5. POST /api/v1/dms/files/upload (multipart) → 201, file stored + metadata
|
|
6. GET /api/v1/dms/files/{id} → 200 + file metadata
|
|
7. PATCH /api/v1/dms/files/{id} → 200, renamed/moved
|
|
8. DELETE /api/v1/dms/files/{id} → 204, soft-delete
|
|
9. POST /api/v1/dms/files/{id}/restore → 200, restored from trash
|
|
10. GET /api/v1/dms/files/{id}/preview → 200 + PDF stream
|
|
11. POST /api/v1/dms/files/{id}/edit-session → 200 + OnlyOffice config
|
|
12. POST /api/v1/dms/files/{id}/share → 200, internal share created
|
|
13. DELETE /api/v1/dms/files/{id}/share → 204, share removed
|
|
14. GET /api/public/share/{token} → 200 (no auth, public access) — MAY already exist via T11
|
|
15. GET /api/public/share/{token} mit password → 401 ohne password — MAY already exist via T11
|
|
16. GET /api/v1/dms/search?q=text → 200 + matching files
|
|
17. GET /api/v1/dms/shared-with-me → 200 + shared files list
|
|
18. POST /api/v1/dms/files/bulk-move → 200, files moved
|
|
19. POST /api/v1/dms/files/bulk-delete → 200, files soft-deleted
|
|
|
|
## Rules
|
|
- No placeholder code. No Lorem Ipsum.
|
|
- Follow existing patterns exactly (SQLAlchemy 2.0, FastAPI APIRouter, Pydantic v2)
|
|
- All routes must have tenant_id scoping
|
|
- Use `# noqa: F401` for __init__.py re-exports
|
|
- Use `from None` in except blocks (B904)
|
|
- File storage path: `/data/dms/{tenant_id}/{file_uuid}`
|
|
- OnlyOffice: generate config only, don't run OnlyOffice server
|
|
- For AC14/AC15: check if T11 permissions plugin already satisfies these. If yes, write tests that verify existing endpoint. If no, implement in DMS plugin.
|
|
|
|
## Deliverables
|
|
- All plugin files created
|
|
- Plugin registered in builtins __init__.py
|
|
- tests/test_dms.py with all 19 ACs tested
|
|
- All tests passing
|
|
- Coverage ≥80%
|
|
- Report: files created, test count + pass/fail, coverage %
|