--- name: seafile-api description: "Seafile Cloud administration: API interaction (token auth, repos, dirs, file ops) and Docker server administration (fsck, MySQL volume recovery, startup debugging, image-version-change pitfalls). Covers Seafile 12.x–13.x MC Docker edition." tags: [file-storage, cloud, api, seafile, docker, recovery] --- # Seafile Cloud Administration This skill covers two domains: 1. **API Interaction** — token auth, repo/dir/file operations via REST API 2. **Docker Server Administration** — `seaf-fsck`, MySQL volume recovery, startup debugging, selective restore from PBS backups For Docker server administration, recovery procedures, and fsck troubleshooting, see `references/seafile-docker-recovery.md`. For backup strategy analysis (offsite/PBS/S3/native), see `references/seafile-immich-backup-strategy.md`. ## API: Authentication Seafile uses token-based API auth. **Never use cookie/session login** — it doesn't work reliably. ```bash # Get API token curl -ks -H 'Content-Type: application/json' \ -d '{"username":"user@example.com","password":"password"}' \ "https://seafile.yourdomain.com/api2/auth-token/" # → {"token":"abc123..."} ``` ## API: List Repositories ```bash curl -ks -H "Authorization: Token {token}" \ -H "X-Requested-With: XMLHttpRequest" \ "https://seafile.yourdomain.com/api2/repos/" ``` Returns array of repos with type (`"repo"` = personal, `"grepo"` = group-shared). ## API: List Directory Contents ```bash curl -ks -H "Authorization: Token {token}" \ -H "X-Requested-With: XMLHttpRequest" \ "https://seafile.yourdomain.com/api2/repos/{repo-id}/dir/?p=%2F" ``` Returns array of items: `{type:"dir"/"file", name, size, mtime, id, permission}`. ## API: Download File **Seafile 13.x (Pro/Plus) — file download via `/api2/repos/.../files/` returns 404.** This endpoint only works on older Seafile versions. **Workarounds for Seafile 13.x:** 1. Use `requests.Session()` to log in via the web UI (`/accounts/login/`) and navigate to the file page, then extract the download URL from the React SPA's network calls. 2. Use WebDAV: `curl -k -u user:token https://server/dav/path/to/file` 3. Use `seafile-cli` if available. 4. Manually download via browser/CLI and copy to the server. ```bash # Old endpoint (ONLY works on Seafile < 13.x): curl -ks -H "Authorization: Token {token}" \ -H "X-Requested-With: XMLHttpRequest" \ "https://seafile.yourdomain.com/api2/repos/{repo-id}/files/?p=%2Fpath%2Fto%2Ffile.txt" \ -o output.txt ``` For login + download via Python `requests`: ```python import requests, re, urllib.parse API = "https://seafile.yourdomain.com" REPO = "repo-id-here" FILE = "/path/to/file.pdf" session = requests.Session() session.headers.update({"User-Agent": "Mozilla/5.0 ..."}) # 1. Get CSRF token resp = session.get(API, allow_redirects=True, timeout=15) csrf = re.search(r'name="csrfmiddlewaretoken"[^>]*value="([^"]*)"', resp.text).group(1) # 2. POST login session.post(resp.url, data={ "login": "user@example.com", "password": "pw", "csrfmiddlewaretoken": csrf, "next": "/", }, allow_redirects=True, timeout=15) # 3. Access file page — the SPA loads download via JS; inspect page source for download patterns file_url = f"{API}/library/{REPO}/docs/?p={urllib.parse.quote(FILE, safe='/')}" resp = session.get(file_url, timeout=15) # Look for 'download' patterns in resp.text — no static tags exist ``` ## API: Create Directory ```bash curl -ks -X PUT -H "Authorization: Token {token}" \ -H "X-Requested-With: XMLHttpRequest" \ "https://seafile.yourdomain.com/api/v2.1/repos/{repo-id}/directory/" \ --data-urlencode "path=/new_folder" ``` ## API: Upload File **Seafile v2.1 Basic Edition (default) — ALL API upload endpoints return 404.** The following endpoints are ALL disabled: - `/api/v2.1/repos/{repo-id}/upload/` - `/api/v2.0/repos/{repo-id}/upload/` - `/api/v1/repos/{repo-id}/upload/` - `/api/repos/{repo-id}/upload/` - `/api/upload/` - `/api/v1/upload/` - `/seafhttp/upload/` **Fallback: Playwright Web UI Automation** The React web UI upload button uses selector `[class*="upload-file"]`. It sits in a dropdown, so use JS click + file chooser: ```python from playwright.sync_api import sync_playwright import json, subprocess, glob, time SEAF_URL = "https://cloud.familie-schoen.com" repo_id = "737eb5b4-731e-4bfe-8a42-8edd62b04c6e" # from /api2/repos/ TARGET_DIR = "/Amazon" creds = json.loads(subprocess.run( 'op item get "cloud.familie-schoen.com" --vault "Hermes" --format json', shell=True, capture_output=True, text=True).stdout) username, password = creds['fields'][0]['value'], creds['fields'][1]['value'] files = sorted(glob.glob("/tmp/amazon-invoices-2025/Amazon_Rechnung_*.pdf")) with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(viewport={"width": 1280, "height": 800}, ignore_https_errors=True) page = context.new_page() # Login page.goto(f"{SEAF_URL}/accounts/login/", wait_until="domcontentloaded", timeout=30000) page.fill('input[name="login"]', username) page.fill('input[name="password"]', password) page.click('button[type="submit"]') page.wait_for_url(lambda u: "login" not in u.lower(), timeout=10000) # Navigate to target directory page.goto(f"{SEAF_URL}/library/{repo_id}/docs/?p=%2F{TARGET_DIR.lstrip('/')}", wait_until="domcontentloaded", timeout=30000) time.sleep(2) # Trigger file chooser via JS click (button is in dropdown, not directly clickable) with page.expect_file_chooser() as fc: page.evaluate("""() => { document.querySelector('[class*="upload-file"]')?.click(); }""") time.sleep(1) file_chooser = fc.value file_chooser.set_files(files) time.sleep(20) # Wait for upload to complete browser.close() ``` Key details: - Selector: `[class*="upload-file"]` — matches `.sf3-font-upload-files sf3-font mr-2 dropdown-item-icon` - Must use JS click (`page.evaluate`) because the button is in a hidden dropdown (visibility: hidden → click timeout) - Use `page.expect_file_chooser()` context manager to catch the file dialog - `file_chooser.set_files(files)` accepts a list for batch upload ## API: Delete File ```bash curl -ks -X DELETE -H "Authorization: Token {token}" \ -H "X-Requested-With: XMLHttpRequest" \ "https://seafile.yourdomain.com/api/v2.1/repos/{repo-id}/files/" \ -d "p=/path/to/file" ``` ## Docker Server Administration ### Running seaf-fsck ```bash # Find the seafile-server version directory docker exec seafile bash -lc 'ls /opt/seafile/ | grep seafile-server' # → seafile-server-12.0.14 # Run fsck docker exec seafile bash -lc '/opt/seafile/seafile-server-12.0.14/seaf-fsck.sh' ``` The fsck checks consistency between MySQL metadata (Repo table, commits) and the file block storage. If MySQL tables are missing or the `seafile` DB user doesn't exist, fsck will report warnings but exit cleanly without actually checking anything. ### fsck Silent Death (OOM Under Heavy I/O) **Pitfall**: `seaf-fsck` can die silently under heavy I/O competition (e.g. concurrent Ceph recovery + RBD copy on the same OSDs). The process gets OOM-killed or I/O-starved, leaving no error in the fsck output log. The log simply stops at the last repo being checked, with no "finished" or error message. **Detection**: Don't trust the log output alone — check if the process is still alive: ```bash # Inside the CT: ps aux | grep seaf-fsck | grep -v grep | wc -l # 0 = process died, needs restart # 3+ = process running (multiple threads) ``` **Restart**: Simply re-run the fsck command. Already-checked repos are processed quickly (seconds), so progress jumps forward rapidly after restart. Large photo repos ("Fotos", "Lightroom Bilder") are the usual stalling points — they have tens of thousands of commits and millions of file blocks. **Large repos**: Repos with 24000-34000+ commits take hours per repo under normal I/O. Under I/O contention they may appear hung for 1+ hour per repo. The process is alive (state `Dl` = disk I/O wait) but barely progressing. Serializing I/O (pausing competing operations) dramatically speeds up fsck. ### Recovery from Lost MySQL Volume See `references/seafile-docker-recovery.md` for the full debugging procedure covering: - `wait_for_mysql()` infinite loop diagnosis - `init_seafile_server()` skipping schema creation - Selective restore from PBS backup (MySQL volume + conf only, skip seafile-data) - Manual DB init via `docker run` setup script (Option B) - Manual DB/User recreation and SQL schema import (Option B2) - **Repo reconstruction from commit storage** (Option C) — when DBs are fresh but file blocks are intact, scan commit JSON objects to find HEAD commits and register repos in `Repo`/`Branch`/`RepoOwner`/`RepoInfo` tables. See `references/seafile-repo-reconstruction.md` for the full procedure. ## API Pitfalls - **Wrong API paths** — `/api/v2.1/repos/{repo}/directory/` and `/api/v2.0/` return HTML error pages. Use `/api2/repos/{repo-id}/dir/` for listing. - **Missing header** — `X-Requested-With: XMLHttpRequest` is required for most API calls, or the server redirects to the login page. - **Cookie login does NOT work** — The AJAX form POST (`/seahub/ajax/login/`) renders an error page. Always use token auth. - **Web UI login returns 403 but sets cookie** — The Seafile login POST (`/accounts/login/`) may return HTTP 403 despite successfully setting the session cookie. This is expected on Seafile 13.x; subsequent requests to the same session work. - **URL encoding** — Paths must be URL-encoded for `p=` parameter (`space` → `%20`, `/` → `%2F`). - **Seafile 13.x (Pro/Plus)** — The `/api/v2.1/repos/{repo}/files/`, `/seafhttp/`, and WebDAV endpoints all return 404/400. Token auth works for repo listing (`/api2/repos/`) and dir listing (`/api2/repos/{repo}/dir/`), but file download is broken via API. Use web login + requests.Session() workaround or manual download. - **WebDAV disabled** — Seafile may have WebDAV disabled on newer versions. Check with `curl -k -o /dev/null -w '%{http_code}' -X PROPFIND "https://server/dav/"`. - **Auth'd 500 vs unauth'd 403** — If authenticated calls (`Authorization: Token ...`) return HTTP 500 (HTML "Page unavailable") while unauthenticated calls return HTTP 403 (valid JSON), the backend is in a broken state. Get a fresh token, and if all endpoints return 500, the server needs a restart.