# Phase 3: Server Integration (REST) ## Objective Attach drive operations to `backend/src/server/web.py` using standard web routes only. This supersedes the WebSocket-based protocol from `drives-report.md`: no WS actions (`list-drives`, `list-files`, `upload-file`), no chunked transfer. Upstream transfer is a single `multipart/form-data` POST per file. Assumes `drive_service` from the earlier phases provides `init_drives`, `upload_file`, `get_drive_file_metadata` and `resolve_drive_file_for_serving`. ## Routes | Route | Method | Description | |-------|--------|-------------| | `/drives` | GET | List drives of the authenticated user. | | `/drives//files` | GET | List file metadata for one drive. | | `/drives//files` | POST | Upload one file (multipart, field `file`). Creates the drive if missing. | | `/files/` | GET | Raw download. | | `/files//meta` | GET | Metadata for existence checks. | ### Auth Reuse the existing `QuartAuth(server, mode="bearer")` setup: - JSON endpoints use `@login_required` and read the user id from `current_user.auth_id`. - `GET /files/` additionally accepts `?token=`, because `` links cannot set an `Authorization` header. ## Implementation ### 3.1 Imports Top of `backend/src/server/web.py`: ```python import database as db import drive_service from quart import Quart, request, send_from_directory, send_file from quart_auth import QuartAuth, AuthUser, login_user, login_required, current_user ``` ### 3.2 Route handlers Registered inside `start_server`, alongside the existing routes: ```python @server.route('/drives', methods=['GET']) @login_required async def list_drives(): drives = await db.Drive.filter(user_id=current_user.auth_id) return [{"id": d.id, "name": d.name} for d in drives] @server.route('/drives//files', methods=['GET']) @login_required async def list_files(drive_name): files = await db.DriveFile.filter(drive__user_id=current_user.auth_id, drive__name=drive_name) return [{ "id": f.id, "name": f.filename, "size": f.size_bytes, "mime": f.mime_type, "created": f.created_at.isoformat(), } for f in files] @server.route('/drives//files', methods=['POST']) @login_required async def upload_file(drive_name): # whole file in one request, no partial transmission upload = (await request.files).get('file') if upload is None: return "missing file part", 400 file_id = await drive_service.upload_file( current_user.auth_id, drive_name, upload.filename, upload.stream, upload.content_type, ) return {"fileId": file_id} @server.route('/files/', methods=['GET']) async def serve_file(file_id): # query token instead of @login_required: anchor downloads can't set headers user_id = auth_manager.load_token(request.args.get('token', '')) if user_id is None: return "unauthorized", 401 try: file_path, mime_type = await drive_service.resolve_drive_file_for_serving(file_id, user_id) return await send_file(file_path, mimetype=mime_type) except FileNotFoundError: return "not found", 404 except PermissionError: return "forbidden", 403 @server.route('/files//meta', methods=['GET']) @login_required async def get_file_meta(file_id): try: # resolve performs the ownership check before anything is returned await drive_service.resolve_drive_file_for_serving(file_id, current_user.auth_id) return await drive_service.get_drive_file_metadata(file_id) except FileNotFoundError: return "not found", 404 except PermissionError: return "forbidden", 403 ``` No changes to `server/websocket.py`. ## Frontend protocol ```javascript // listing const drives = await fetch('/drives', {headers: {Authorization: `Bearer ${token}`}}).then(r => r.json()); const files = await fetch(`/drives/${encodeURIComponent(name)}/files`, {headers: {Authorization: `Bearer ${token}`}}).then(r => r.json()); // upload: single POST, no chunking const form = new FormData(); form.append('file', blob, 'report.pdf'); await fetch(`/drives/${encodeURIComponent(driveName)}/files`, { method: 'POST', headers: {Authorization: `Bearer ${token}`}, body: form, }); // download: plain anchor // Download ``` ## Notes - Routing is unaffected by the static catch-all: werkzeug prefers the exact/typed routes above over `/`. - `/drives` is now an API route; the SPA must not mount a view at that exact path. - `drive_name` is a single URL segment (no `/`). The service layer should reject names containing path separators before they reach `os.path.join`. - The query-token on the download endpoint trades header auth for `` compatibility (token visible in history/logs). Switch to fetch + blob or cookie auth if that's unacceptable.