Initial commit: Hermes Agent Skills collection
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user