# File Store API — muse-vm

Personal file-storage API (single Python stdlib HTTP server). Human docs: `GET /docs`.
This file: `GET /api.md` (no auth needed). Everything else about the API is below.

## Base URLs

- Primary: `https://cloud.chungsuclq.skin` (if HTTPS is not up yet, `http://cloud.chungsuclq.skin` works the same)
- Alternative: `https://muse-vm.tail588220.ts.net` (Tailscale Funnel; unreachable while the user's Cloudflare WARP is on)

`$BASE` below = either base URL.

## Authentication

- Every endpoint except `GET /health`, `GET /`, `GET /docs`, `GET /hoc`, `GET /api.md` requires a bearer token:
  `Authorization: Bearer <TOKEN>`
- File downloads also accept the token as a query param: `GET /files/<name>?token=<TOKEN>` (needed for browser media tags).
- The token is a secret that only the user holds. If you do not have it, ASK THE USER. Never guess or brute-force it.
- Missing/wrong token → `401 {"error": "unauthorized"}`.

## Folders (namespaces)

- Files live in one-level folders. The root folder is the empty string `""`; all pre-existing files are in root.
- A folder name is a single path segment (no `/` or `\`). Folders are auto-created when a file is uploaded or moved into them.
- Each separate web/app should use its OWN folder name as its namespace (e.g. `hoc`, `phim`, `truyen`) and pass it on every call:
  - `GET /files?folder=<name>`, `GET /files/<file>?folder=<name>`, `PUT /files/<file>?folder=<name>`, `DELETE /files/<file>?folder=<name>`
  - `POST /upload` with header `X-Folder: <name>` (or `?folder=<name>`)

## Endpoints

### GET /health
No auth. Liveness check.
→ `200 {"status": "ok"}`

### GET /stats
Disk + store totals (sizes in bytes; file count covers all folders).
→ `200 {"total": N, "free": N, "disk_used": N, "files": N, "files_bytes": N}`

### GET /folders
List folders; the first entry is the root (`"name": ""`).
→ `200 [{"name": "", "files": N, "bytes": N}, {"name": "phim", "files": N, "bytes": N}]`

### POST /folders
Create a folder. JSON body: `{"name": "phim"}`
→ `201 {"folder": "phim"}` · invalid name → `400`

### DELETE /folders/<name>
Delete a folder AND every file inside it (the root cannot be deleted).
→ `200 {"deleted_folder": "phim", "files": 3}`

### GET /files?folder=<name>
List files in a folder (omit `folder` for root), sorted by name. `size` in bytes, `mtime` = Unix seconds.
→ `200 [{"name": "tap1.mp4", "size": 1512345678, "mtime": 1762345678}]`

### POST /upload
Upload a file. Headers: `X-Filename: <url-encoded name>`, optional `X-Folder: <name>`. Body = raw file bytes. Max 16 GB per file.
→ `201 {"name": "tap1.mp4", "size": N, "sha256": "…"}`
Note: the file is streamed straight to its final path; an interrupted upload leaves a partial file — simply re-upload to overwrite it completely.

### PUT /files/<name>?folder=<name>
Upload/overwrite by name in the URL. Body = raw file bytes.
→ `201 {"name": "…", "size": N, "sha256": "…"}`

### GET /files/<name>?folder=<name>
Download. Supports HTTP Range (`206` + `Content-Range`), so media seeking/streaming works. Sends `Content-Disposition: attachment`. Token via header or `?token=`.
→ `200` full body, or `206` partial.

### POST /move
Move a file between folders. JSON body: `{"name": "tap1.mp4", "src": "", "dst": "phim"}` (`""` = root; target folder auto-created).
→ `200 {"moved": "tap1.mp4", "to": "phim"}` · source missing → `404` · name already exists in target → `409`

### DELETE /files/<name>?folder=<name>
Delete one file.
→ `200 {"deleted": "tap1.mp4"}`

## Web pages

- `GET /` — web GUI for humans (the browser stores the token in localStorage key `ftoken`)
- `GET /docs` — human API documentation
- `GET /api.md` — this Markdown file
- `GET /hoc` — the user's CSoNet talk-practice app (no auth)

## Status codes

`200` ok · `201` created · `206` partial content (Range) · `400` bad name/folder/body · `401` unauthorized · `404` not found · `409` move target name conflict · `413` file over 16 GB

## curl quickstart

```bash
BASE=https://cloud.chungsuclq.skin
TOKEN=<ask the user>

curl $BASE/health
curl -H "Authorization: Bearer $TOKEN" $BASE/folders
curl -H "Authorization: Bearer $TOKEN" "$BASE/files?folder=phim"
curl -X POST --data-binary @tap1.mp4 -H "Authorization: Bearer $TOKEN" \
  -H "X-Filename: tap1.mp4" -H "X-Folder: phim" $BASE/upload
curl -H "Authorization: Bearer $TOKEN" -O "$BASE/files/tap1.mp4?folder=phim"
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"tap1.mp4","src":"","dst":"phim"}' $BASE/move
curl -X DELETE -H "Authorization: Bearer $TOKEN" "$BASE/files/tap1.mp4?folder=phim"
```

## Notes for agents

- URL-encode file names and folder names everywhere (path, query, `X-Filename` header).
- To check whether a file exists, use `GET /files` metadata — do not download large files just to probe them.
- The store sits on a personal VM that occasionally restarts (a watchdog revives the API within about a minute). If a call fails with a connection error, wait ~60 seconds and retry before concluding the API is down.
- Never expose the user's token in public output, logs, or shared links.
