- Add POST /api/documents/upload multipart endpoint with target_backend form field
- Cloud backends (google_drive, onedrive, nextcloud, webdav) use direct put_object()
- MinIO path generates presigned PUT URL (unchanged flow)
- Cloud uploads do NOT touch quota (D-11: separate backend)
- GET /api/documents/{id}/content now uses get_storage_backend_for_document
- CloudConnectionError from any cloud op raises HTTPException(503) with safe message
- target_backend validated against _CLOUD_PROVIDERS allowlist (T-05-06-01)
- Import CloudConnectionError with fallback stub for envs without google-auth deps
- Add _parse_range() helper: validates Range header bounds, raises 416 on invalid
- Add stream_document_content endpoint with get_regular_user dep (admin → 403)
- Access check: owner OR Share.recipient_id; neither → 404
- Bytes fetched via get_object() only — presigned_get_url() never called
- Range requests return 206 + Content-Range header
- Add pdf_open_mode column to User ORM model (migration 0004 already applied)
- Use HTTP_416_RANGE_NOT_SATISFIABLE (non-deprecated constant)
- backend/api/folders.py: POST /api/folders (create), GET /api/folders (list),
GET /api/folders/{id} (breadcrumb), PATCH /api/folders/{id} (rename),
DELETE /api/folders/{id} (cascade-delete + atomic quota decrement),
PATCH /api/documents/{id}/folder (move document)
- All folder endpoints use get_regular_user (admin gets 403); 404 for IDOR
- IntegrityError caught -> 409 on duplicate folder name under same parent
- WITH RECURSIVE CTE for subtree collection with SQLite fallback (OperationalError)
- Atomic quota decrement with CASE WHEN pattern (SQLite compat)
- MinIO object deletion best-effort (per-object try/except)
- write_audit_log called after folder.created, folder.renamed, folder.deleted
- backend/api/documents.py: add sort, order, folder_id, q params to list_documents;
add is_shared field to each document in response using Share subquery
- backend/main.py: register folders_router and document_move_router