3.9 KiB
Drives
Per-user file storage: a drive is a named container owned by a user, and each file is a database record plus a blob on disk. Everything is addressed by drive name + filename — the random file id never appears in URLs. Drives bypass the WebSocket action protocol entirely: all endpoints are plain REST (routes in backend/src/server/web.py, logic in backend/src/drives.py), so downloads can be plain <a> links.
Logical model
- Drive — user-scoped, addressed by name. Created implicitly on first upload to a new name; there is no create/rename/delete-drive endpoint.
- File — readable
filename, mime type, size,created/accessedtimestamps. - Folders are virtual — a filename may contain slashes (
docs/api/readme.md); the Drives view splits names to render a tree, but storage stays flat. - Identity —
DriveFile.idis a random hex string (secrets.token_hex(8)) that doubles as the on-disk filename, so user-facing names never touch the filesystem.
Backend interface
Download links can't set headers, so the GET/DELETE file routes additionally accept the token as a ?token= query parameter.
| Route | Behavior |
|---|---|
GET /files |
List the user's drives: [{id, name}] |
GET /files/<drive> |
List files: [{id, name, size, mime, created}] |
POST /files/<drive>/<filename> |
Multipart upload (field file) in a single request. The URL tail — <path:filename>, slashes allowed — becomes the stored filename; the part's own name is the fallback. Returns {fileId}. |
GET /files/<drive>/<filename> |
Serve the bytes with their stored mime type, or return {id, name, size, mime, created, accessed} when ?metadata is present. |
DELETE /files/<drive>/<filename> |
Delete the record, then the blob. Returns {fileId}. |
Every resolve (download, metadata, delete) stamps accessed_at.
Frontend interface
In frontend/src/backend.js:
webFetch(path, options)— generic REST helper: setsAuthorization: Bearer(waiting for login if needed), parses JSON, throws on non-2xx. Downloads bypass it.getDrives(),getDriveFiles(driveName),deleteDriveFile(driveName, filename)uploadDriveFile(driveName, file)— multipart POST of the whole file.driveFileUrl(driveName, filename)— GET URL carrying?token=, used for download anchors and for opening a file in a tab.
frontend/src/views/Drives.vue lazy-loads a drive's files on first expand, rebuilds the folder tree from the flat file list on every load (open folders live on the drive object, so they survive reloads), uploads through a file picker, and deletes with confirmation.
Implementation notes
- Models in
backend/src/database.py(migration0002_auto_20260921_1840):Drive(id, user_id, name, created_at)andDriveFile(id char(64) pk, drive_id, filename, mime_type, size_bytes, accessed_at, created_at), tabledrive_files. Ownership rides theUser → Drive → Filechain and is checked before any disk access. - Disk layout:
{data_dir}/drives/{drive_id}/{file_id}— keyed by drive id, not by user or name.init_drives(data_directory), called frommain(), creates the root. - Upload streams to disk in 8 KiB chunks and only then writes the DB record; delete removes the record first, then the blob.
Known issues
uploadDriveFilePOSTs to/files/<drive>, where no POST route exists, and drops the folder-path argumentDrives.vuepasses it — UI uploads fail, and folder-targeted upload is not wired up.- Drives are only created implicitly, and the view only uploads into existing drives, so the first drive cannot be created from the frontend.
- Uploads are meant to refuse a filename already used in the drive, but the check queries the
Drivemodel withDriveFile's fields, so it errors instead of detecting duplicates. resolve_filedereferences the drive without a None check, so a nonexistent drive yields a 500 instead of a 404.