* refactor(api)!: DELETE endpoints return 204 No Content (#443) Resolves item #4 of #443 — DELETE return-shape inconsistency (8 different conventions). Standardize every DELETE on 204 No Content with an empty body, dropping the ack/result bodies: `{id,deleted}`, `{providerId,deleted}`, `{key,deleted}`, `{id,revoked}`, `{ok:true}`, the download-task tombstone, license `{deleted,cloud_unbind_error}`, and the entitlement-revoke / abort-upload-session objects. Kept (the issue's flagged special case): object-delete and empty-trash still carry a purge count — the only delete responses with information a caller can't otherwise derive. Object delete is trimmed from `{id,deleted,purged}` to just `{ purged: number | false }`; empty-trash keeps `{ purged: number }`. Backend: 15 DELETE routes → `204: { description }` + `c.body(null, 204)`; removed the now-dead `deleteDownloaderResponseSchema`. Frontend: added a `discard()` helper (the 204 counterpart to `unwrap()`); the unwrap-based delete wrappers now resolve `void`. cancelUpload/deleteObject now return `{ purged }`. The already-void wrappers (deleteShare, deleteAvatar, …) were untouched — they never read the body. OpenAPI document + Go client regenerated (go build clean). BREAKING CHANGE: all DELETE endpoints now respond 204 with no body. License unbind no longer returns `cloud_unbind_error`, so a partial cloud-unbind failure is no longer surfaced in the response body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(licensing): surface cloud-unbind failure as 502, don't swallow it as 204 The DELETE→204 sweep turned license unbind into an unconditional 204, which hid a real partial failure: when the best-effort cloud unbind throws, the local binding is cleared but the cloud side is left dangling. Reporting 204 (success) in that case swallows the error. `unbindLicense` now returns a Result — `{ ok: true }` only when the cloud unbind also succeeds, and a 502 AppError (reason `CLOUD_UNBIND_FAILED`, the cloud error in `details.metadata`) when it fails. The local binding is still cleared either way; the handler returns 204 on ok and throws the error otherwise. DELETE success is still an empty 204 — this only restores fail-fast on the one endpoint whose failure was a soft body field, never a thrown error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(spec): reconcile users.feature with #446 better-auth migration `pnpm lint:spec` (a CI gate) was red on 11 orphaned `spec/users.feature` scenarios — leftover from #446, which moved admin user management off our `/api/users/*` routes onto better-auth's admin plugin and deleted the old endpoints/tests but not their spec scenarios. Pre-existing on main; surfaced here as the only failing CI check. Reconcile the spec with reality: - Re-link the behaviors that survived (now via better-auth) to the tests that already cover them: list / admin-only(403) / disable(ban) / delete(remove) / patch-missing(act-on-missing→404) → admin-users-ba.integration.test.ts; quota-personal-org → the per-user quota test in users.integration.test.ts. - Drop scenarios for behavior that no longer exists: batch-toggle (now a client-side fan-out, no endpoint), invalid-status (ban/unban are explicit), multi-field filter (better-auth search is single-field, untested), the unauthenticated 401 guard (better-auth owns it), and the inline quota-entitlements-in-list (quota is now a per-user sub-resource). lint:spec: 413 scenarios, all covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZPan
Open-source file hosting for your S3-compatible storage.
Deploy on Cloudflare Workers or Docker. Upload directly to object storage.
English · 简体中文 · 日本語 · 한국어 · Русский · Español · Português (BR)
🎉 ZPan v2 is here. To celebrate, we're giving away ZPan Pro three ways — for early stargazers, contributors, and new stars. See v2 Launch Offers.
What is ZPan?
ZPan is a lightweight file hosting platform built on top of S3-compatible storage. Files upload directly from the client to S3 through presigned URLs, bypassing server bandwidth entirely. The server is the control plane: auth, metadata, shares, quotas, teams, WebDAV, tool integrations, and admin operations.
The product boundary is intentional: ZPan is a purpose-built S3-backed web drive, not a wrapper around every consumer cloud drive and not a full groupware suite. You bring an S3-compatible bucket; ZPan gives it a clean web UI, public sharing, image-hosting APIs, and deployment options that do not require a VPS or NAS.
Core scenarios:
- S3 web drive — Manage files, folders, previews, trash, quotas, and team workspaces on top of your own object storage
- Image hosting — Upload via PicGo, PicList, uPic, ShareX, Flameshot, or API and get a stable URL instantly
- File sharing — Publish share links with password, expiration, download limits, direct links, and save-to-drive flows
- Personal homepage — Give each user a public
/u/usernamepage for curated shared files and folder-style browsing - External access — Mount files through WebDAV and run downloader workers for remote-download workflows
Why ZPan?
S3 only, by design. ZPan does not chase every net-disk provider or build a cloud-drive nesting layer. The storage contract stays simple and durable: S3-compatible buckets such as Cloudflare R2, AWS S3, Backblaze B2, MinIO, RustFS, Tigris, and other S3-compatible services.
Cloudflare Workers first. ZPan is built around Cloudflare Workers, D1, Hono, and web-standard APIs, with Docker and other runtimes as additional deployment targets. You can run a real file-hosting control plane without owning a VPS, keeping a NAS online, or proxying uploads through a long-running server.
Direct transfer path. Uploads and downloads use presigned object-storage URLs whenever possible. That keeps server bandwidth low, avoids a central file-transfer bottleneck, and lets object storage do the heavy lifting.
Practical file workflows. ZPan includes a web file manager, public sharing, image-hosting configuration, API keys, WebDAV access, teams, quotas, remote-download tasks, file previews, and admin controls without turning into a provider-aggregation platform.
Deployable downloader workers. Remote download does not have to run inside the main ZPan instance. You can deploy the downloader together with ZPan for a simple setup, or run it separately in an environment with better network access and fewer source-site restrictions, then let ZPan import the completed files into object storage.
Product Boundaries
ZPan is a good fit when you want:
- A focused S3-backed web drive instead of a storage-provider zoo
- A self-hosted image bed and file-sharing app backed by your own bucket
- Cloudflare-native deployment without maintaining a VPS or NAS
- Browser-to-S3 transfers instead of app-server file proxying
- Tool integrations for screenshot, publishing, WebDAV, remote download, and API-driven workflows
ZPan is not trying to be:
- A real-time document co-editing suite like Nextcloud Office
- A general-purpose cloud-drive aggregator like AList
- A local server directory browser like File Browser
How ZPan Compares
Most self-hosted file projects start from either server files, desktop sync, collaboration, or many-provider aggregation. ZPan starts from S3-compatible object storage and a Cloudflare Workers-friendly control plane.
| Capability | ZPan | Cloudreve | AList | Nextcloud | Seafile | File Browser |
|---|---|---|---|---|---|---|
| S3-backed product focus | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| S3-compatible storage backend | ✅ | ✅ | ✅ | ✅ | ⚠️ | ❌ |
| Direct browser-to-object-storage path | ✅ | ⚠️ | ⚠️ | ❌ | ❌ | ❌ |
| Cloudflare Workers deployment | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| No VPS/NAS required | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| PicGo/ShareX image-hosting workflow | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Per-user public file homepage | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ❌ |
| Remote download workflow | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Separately deployable downloader/node | ✅ | ✅ | ⚠️ | ❌ | ❌ | ❌ |
| Multi net-disk aggregation | ❌ | ❌ | ✅ | ⚠️ | ❌ | ❌ |
| Server local directory as primary file root | ❌ | ⚠️ | ⚠️ | ⚠️ | ❌ | ✅ |
| Real-time document co-editing | ❌ | ❌ | ❌ | ✅ | ⚠️ | ❌ |
| Dedicated sync clients | Planned | ❌ | ❌ | ✅ | ✅ | ❌ |
| Team/workspace model | ✅ | ⚠️ | ❌ | ✅ | ✅ | ❌ |
| WebDAV access | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Share links | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Docker deployment | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Legend: ✅ first-class or core capability; ⚠️ partial, edition-dependent, or not the product's main focus; ❌ not a core capability.
Deploy
Cloudflare Workers (Recommended)
Deploy via GitHub Actions with zero server management. Free tier covers personal use.
- Fork this repository
- In your fork, go to Settings → Secrets and variables → Actions and add:
CLOUDFLARE_ACCOUNT_ID— found on the Cloudflare dashboard sidebarCLOUDFLARE_API_TOKEN— create one here with Workers Scripts:Edit, D1:Edit, and R2 Storage:Edit permissions (R2 scope is needed to auto-provision the avatar/logo bucket)
- Go to the Actions tab, select Deploy to Cloudflare Workers, and click Run workflow
After initial setup, the workflow runs automatically every time you sync your fork with the latest release.
AWS Lambda
Deploy via GitHub Actions using SAM. Lambda Function URL provides HTTPS with no API Gateway needed.
- Fork this repository
- In your fork, go to Settings → Secrets and variables → Actions and add:
TURSO_DATABASE_URLandTURSO_AUTH_TOKEN— from Turso (free, no credit card)AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION
- Go to the Actions tab, select Deploy to AWS Lambda, and click Run workflow
See docs/deploy/aws-lambda.md for full setup instructions and IAM permissions.
Docker
Quick start — pull the pre-built image and bring your own S3 storage:
curl -O https://raw.githubusercontent.com/saltbo/zpan/main/deploy/docker-compose.yml
docker compose up -d
With RustFS (self-hosted S3-compatible storage, no external dependencies):
curl -O https://raw.githubusercontent.com/saltbo/zpan/main/deploy/docker-compose.rustfs.yml
docker compose -f docker-compose.rustfs.yml up -d
After startup:
- Open the RustFS console at
http://localhost:9001(admin / admin123) and create a bucket (e.g.zpan-bucket) - Open ZPan at
http://localhost:8222, register a user (first user gets admin role) - Go to Admin → Storage and add the RustFS storage:
- Endpoint:
http://localhost:9000(must be reachable from your browser, not the Docker internal hostname) - Bucket: the bucket name you created in step 1
- Region:
us-east-1 - Access Key / Secret Key:
admin/admin123
- Endpoint:
Important: The storage endpoint must be accessible from the client browser, since files upload directly to S3 via presigned URLs. Use
http://localhost:9000for local development, or your server's public URL for production.
Documentation
- v2 Launch Offers — earn ZPan Pro for free
- Roadmap
- Contributing
v1
Looking for ZPan v1 (Go version)? See the v1 branch.
Contributing
See CONTRIBUTING.md for details.
Thank you to all the people who contributed to ZPan!
License
ZPan is under the GNU Affero General Public License v3.0. See the LICENSE file for details.
