---
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 <a> 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.
