- repository.Resources (insert/list/get/soft-delete scoping owner_id, UNIQUE(parent_id,name) → NAME_CONFLICT) + Devices.Upsert au register
- service.Resources : DTOs {id,name,size,mimeType,folderId,createdAt,updatedAt}, upload → UPLOAD_DIR/<device>/<id>.<ext> (nettoyage si métadonnée échoue)
- handlers : files list/get/delete/upload + folders racines ; routes consolidées dans handlers.RegisterRoutes
- dbtest package : test DB jetée par repo (open+reset+migrate, skip si PG down) ; tests repository + handlers end-to-end (register→token→CRUD, scoping cross-device, FILE_TOO_LARGE, NAME_CONFLICT)
- docs: /devices persiste le device, layout UPLOAD_DIR par device, codes NAME_CONFLICT/SERVICE_UNAVAILABLE
8.8 KiB
VaultDrop — Contrat API V1 (autoritatif)
Status : autoritatif. Le client mobile est la source de vérité : mobile/api/types.ts + mobile/api/client.ts sont implémentés et testés ; le serveur Go doit les matcher exactement (méthode + path + enveloppe), il ne re-négocie pas. Document consolidé à partir de ces deux fichiers — toute divergence de ce doc doit être portée dans le client d'abord.
Références : V2.md (modèle cible), mobile/services/db/ (conventions sync), README.md/V1.md (obsolètes).
1. Base et transport
- Base URL serveur : schéma + host configurés côté client via
EXPO_PUBLIC_API_BASE_URL(défauthttp://localhost:8080/api/v1). - JSON partout, sauf
POST /files/upload(multipart). - Enveloppe succès :
{ "data": T, "meta"?: { "page": int, "pageSize": int, "total": int } }(metaprésent sur les listes paginées). - Erreur :
{ "error": { "code": string, "message": string } }+ statut HTTP adéquat. - Côté client, toute réponse non-
2xxest normalisée enApiError:codedu body si présent, sinonHTTP_<status>; échec réseau →NETWORK_ERROR.
2. Identité et identifiants (invariants)
- Device-first : le device s'enregistre (
POST /devices) avec son identité générée localement (device_user_id32-hex mobile) et reçoit en échange un token paseto v4-local qu'il stocke. Requêtes suivantes :Authorization: Bearer <token>(toutes les routes sauf/health), résolu endevice_idpar middleware. V1 : pas de comptes utilisateurs (users.user_idreste NULL surdevices). - Au register, le device est upserté dans
devices(last_seen_atrafraîchi) ; chaque nouvelle requête avec token est l'occasion de rafraîchirlast_seen_at. Une ressource ne peut être créée que par un device enregistré (resources.owner_id→devices.device_id, FK). - Identifiants :
resource_id,device_user_id,tokende share-link = TEXT opaque 32-hex minuscule,^[0-9a-f]{32}$. Le mobile génère toujourslower(hex(randomblob(16))); le serveur stocke tel quel, sans conversion UUID (cf. note V2.md). Contrainte serveur :CHECK (col ~ '^[0-9a-f]{32}$')sur toutes les colonnes id + FK. - Horodatages échangés en millisecondes epoch (le mobile utilise
Date.now()).
3. Endpoints
| Méthode | Path | Requête | Réponse data |
Statut absence |
|---|---|---|---|---|
| GET | /health |
— | { "status": "healthy" } |
— |
| POST | /devices |
{ "deviceId": "…32-hex" } (client-generated) |
{ "deviceId": "…32-hex", "token": "v4.local…" } |
INVALID_DEVICE_ID |
| GET | /files |
query folderId?, page?, pageSize?, sort? |
FileDto[] (+ meta) |
— |
| GET | /files/:id |
— | FileDto |
NOT_FOUND |
| DELETE | /files/:id |
— | { "id": "…" } |
NOT_FOUND |
| GET | /files/search |
query q (obligatoire), page?, pageSize? |
FileDto[] (+ meta) |
— |
| GET | /files/folders |
— | FolderDto[] (racines) |
— |
| POST | /files/upload |
multipart : file (uri/name/type), folderId? |
FileDto |
FILE_TOO_LARGE |
| POST | /ocr/jobs |
{ "fileId": "…" } |
OcrJob |
— |
| GET | /ocr/jobs/:id |
— | OcrJob |
NOT_FOUND |
| POST | /sync/ops |
voir §6 | voir §6 | — |
| GET | /sync/permissions |
query after? (cached_at ms) |
ResourcePermission[] |
— |
DTOs (copie conforme de mobile/api/types.ts)
type FileDto = {
id: string; // resource_id 32-hex
name: string;
size: number;
mimeType?: string | null;
folderId?: string | null; // resource_id 32-hex
tags?: string[];
createdAt?: string;
updatedAt?: string;
};
type FolderDto = { id: string; name: string; parentId?: string | null };
type OcrJobStatus = 'queued' | 'processing' | 'done' | 'failed';
type OcrJob = { id: string; status: OcrJobStatus; text?: string | null; error?: string | null };
4. Upload
- Multipart : champ
file+folderId?optionnel. Le client ne fixe jamaisContent-Type(le boundary doit être généré par la plateforme). - Limite :
MAX_FILE_SIZE_MB(défaut 50). Dépassement → 413{ "error": { "code": "FILE_TOO_LARGE", … } }. - Le fichier physique est stocké sous
UPLOAD_DIR/<device_id>/<resource_id>.<ext>; la métadonnée est persistée en base et renvoyée enFileDto. Si la persistance de la métadonnée échoue (ex.NAME_CONFLICT), le fichier physique est supprimé.
5. OCR
POST /ocr/jobs { fileId }→OcrJobimmédiat (status: queued), traitement asynchrone.GET /ocr/jobs/:id→ statut. Le mobile poll toutes les 3s jusqu'àdone/failed(hooks/useUpload.ts).- Moteur : Tesseract en appel système, langue configurable
OCR_LANG(défautfra+eng). Un stub qui répond indéfinimentstatus: "pending"est un comportement temporaire acceptable (le client ne casse pas). - Extraction texte PDF :
ledongthuc/pdf(déjà en go.mod).
6. Contrat de sync (outbox + snapshot)
6.1 Outbox — POST /sync/ops
{
"operations": [
{
"operation_id": 42, // = id client (pending_operations.id)
"ref_type": "resource", // "resource" | "share" | "share_link"
"ref_id": 7, // id local de la ligne share/share_link (sinon null)
"resource_id": "…32-hex", // ressource cible
"resource_type": "folder", // "folder" | "file"
"operation": "create_resource",
"payload": {}
}
]
}
operation∈create_resource | update_metadata | delete_resource | move_resource | share | revoke_share | update_share | create_link | revoke_link(cf.PendingOperationTypemobile).- Idempotence : contrainte d'unicité serveur
(device_id, operation_id). Pour chaque op : si déjà traitée → no-op (comptée comme appliquée, les doublons arrivent à cause du backoff/retry). Sinon appliquée si valide. - Ordre : les opérations sont appliquées séquentiellement, dans l'ordre du batch. Le serveur s'arrête à la première erreur non-idempotente et renvoie l'index atteint — le client reprend à cet index.
- Réponse :
2xxavec{ "applied": int, "failed": { "operation_id": int, "code": string, "message": string } | null }(applied= index de la prochaine op à envoyer). - Côté client, le
pushStatus(pending/synced/failed) des shares/share_links est dérivé de l'état des opérations de l'outbox ; dead-letter aprèsMAX_PENDING_ATTEMPTS(= 5).
6.2 Snapshot — GET /sync/permissions?after=<cached_at_ms>
- Renvoie le delta (ou l'ensemble) des permissions effectives pour le device appelant, chacune sous la forme exacte consommée par
canAccess:
type ResourcePermission = {
resource_id: string; // 32-hex
resourceType: 'folder' | 'file';
effectiveAccess: 'viewer' | 'commenter' | 'editor' | 'owner';
inherit: boolean;
ownerId: string | null; // device ownership si applicable
sharedById: string | null;
expiresAt: number | null; // ms epoch ; null = jamais
cachedAt: number; // ms epoch — horodatage du snapshot (TTL 24h)
updatedAt: number;
};
- Calcul de
effective_access(le serveur est la source de vérité) :- Rang :
viewer = 1 < commenter = 2 < editor = 3 < owner = 4. - La permission exacte sur le nœud est autoritaire (elle n'est pas annulée par son propre
inherit=false). - Les ancêtres propagent uniquement si leur relation a
inherit = true; une relation expirée (expires_atpassé) est ignorée et ne propage pas. owner_id== device appelant →owner(fallback, quel que soit le niveau remonté).- Le rang le plus élevé l'emporte ; sans relation applicable et sans ownership → la ressource n'est pas dans le snapshot.
- Rang :
- TTL / stale : après
PERMISSION_TTL_MS(= 24h) sans reseed,canAccessdowngrade en lecture seule (viewer) vers le cache.
6.3 Placements
- Le statut de placement (
sync_statusmobile :local|cloud|local-cloud) est le reflet duresource_placementsserveur (statuts V2 :local_only,synced,cloud_only,pending_upload,pending_download). La matérialisation se fait via l'outbox (create_resource→synced/pending_upload; suppression physique locale ≠ suppression serveur).
7. Codes d'erreur courants
NOT_FOUND, NOT_IMPLEMENTED (501 temporaire sur les routes non construites — état actuel : files CRUD/upload, devices, health, folders sont réels ; search, ocr/*, sync/* en queue), FILE_TOO_LARGE (413), NAME_CONFLICT (409 — même nom dans le même parent, cf. UNIQUE(parent_id, name)), NETWORK_ERROR (côté client), HTTP_<status> (fallback). Le serveur doit répondre 501 { "error": { "code": "NOT_IMPLEMENTED", "message": "…" } } sur toute route encore en queue. Statut SERVICE_UNAVAILABLE (503) si le backend n'est pas initialisé.