中文 · English
A Serverless file-sharing service running on the Cloudflare Workers edge network — no server to rent, no Docker to run, no ops to babysit. Upload with a background queue, set expiry dates, organize into folders — expired downloads return 410.
EdgeDrive = a complete private file hosting + temporary direct-link service: R2 stores files, D1 manages metadata, Cloudflare Access handles identity — all running on the Cloudflare free tier.
- Serverless: no server, no ops, global edge nodes respond locally
- Background uploads: drag & drop / batch / multipart (auto-shards >8MB, no size limit); identical content instant-upload (SHA-256 dedupe — writes a new record, transfers zero bytes)
- Folders: tree structure (icons + expand arrows, scrollable on mobile), create / rename / delete / tree-style move dialog
- Recycle bin: deletes are soft — restorable; purge cron permanently removes after 30 days (R2 + D1)
- Tags / Star / Recent: comma-tag filtering, inline star toggle, recent-uploads tab
- Expiry: three inline presets (hours / days / permanent) + bulk set + auto
410 - Share links: one file can have many links (open / password / limited / different expiry). Long URL
/dl/{path}/{name}?t={token}(readable) or short/s/{code} - Password protection: SHA-256+salt, constant-time compare, 5-strike lockout (10 min), HttpOnly cookie 30 min
- Share admin: sidebar Shares tab — list, copy, password, extend, revoke, delete, shorten
- Preview pages:
/dl/.../view?t=token— image lightbox (zoom/rotate), video Range streaming (seekable), audio, PDF, Markdown+Mermaid+code highlight, TXT; Markdown/PDF/TXT scroll inside height-limited containers - Copy preview link: copy
/view?t=tokenlanding page per file; multi-select generates one batch preview link - Batch sharing: multi-select → bulk bar "Copy link / Copy preview link" →
/dl/batch/{token}web-disk page (download all + per-file preview/download) - Selection highlight: primary-tinted rows in list view, 3px outline in grid view — visible in dark / light / Nocturne themes
- Theme system: Onyx (default dark) / Porcelain (light) / Nocturne — switch from Settings, persisted in D1, public pages follow
- Range downloads: resumable / video seeking
- Stats dashboard: R2 capacity & Class A/B, D1 reads/writes, Worker invocations (GraphQL Analytics) — responsive single-screen
- Security: multi-layer path traversal protection, XSS content-type hardening, fully parameterized SQL, real Cloudflare Access JWT verification (fail-closed)
- One-click deploy: fork → import to Cloudflare → auto-provisions D1/R2/runs migrations → first-visit Access onboarding
Click the button → connect GitHub + Cloudflare accounts → pick Worker/resource names → auto fork + build + deploy (D1/R2 created & bound automatically). Open the Worker domain and complete the Access onboarding.
Troubleshooting: "Unable to fetch repository contents"
The deploy tool validates the repo URL from your browser (frontend fetches the GitHub API directly). If it fails, your egress IP likely hit GitHub's anonymous rate limit (60 req/h per IP — common on shared/NAT networks).
- Verify: open
https://api.github.com/repos/ZUENS2020/EdgeDrivein your browser — a rate-limit error means you're affected- Fix 1: switch networks (phone hotspot) and retry — a fresh IP resets the quota
- Fix 2: wait ~1 hour (quota resets hourly)
- Fix 3: skip the button — use the manual flow below (Cloudflare's backend fetches the repo, unaffected by your IP quota)
- A Cloudflare account (free tier is fine)
- A GitHub account
-
Fork this repo (GitHub → Fork)
-
Import to Cloudflare:
- Cloudflare dashboard → Workers & Pages → Create → Pages → Connect to Git
- Pick your fork → Begin setup
- Framework preset:
Next.js; build commandnpm run build; output directory empty - Save and Deploy — first deploy auto-creates:
- D1 database (
edgedrive-db) + runs migrations - R2 bucket (
edgedrive) - Worker bindings (
DB/R2)
- D1 database (
-
First-visit Access onboarding (guided mode):
- Open your
*.workers.devdomain - Visit
/admin— no login required until Access is enabled, only the onboarding page shows - Fill in Access Team and AUD, click Enable Access
- From then on, all admin requests go through Access JWT (401 without auth)
⚠️ Complete onboarding and protect/admin*in Zero Trust immediately after deploying. The onboarding page is open until enabled; optional Worker SecretSETUP_TOKENprevents others from hijacking the setup. - Open your
-
Updates: push to
main→ auto redeploy (Cloudflare Pages Git integration)
Optional: bind a custom domain (Workers & Pages → your project → Custom domains) — direct links become
https://your.domain/dl/...
EdgeDrive uses Cloudflare Access for admin authentication (no brute-forceable passwords — only Cloudflare-account holders get in).
- Cloudflare dashboard → Zero Trust → Access → Applications → Add an application → Self-hosted
- Application domain:
- Protect only the admin (recommended —
/dlshare links stay public):your.domain/admin* - Or protect everything:
your.domain/*
- Protect only the admin (recommended —
- Add → save
| Value | Where |
|---|---|
| Access Team | Zero Trust domain prefix: the <team> in https://<team>.cloudflareaccess.com |
| AUD | Access app → Other settings tab → AUD Tag (hex string) |
⚠️ AUD is unique per application — it changes if you recreate the app or revoke tokens. Don't touch the app after configuring.
- Open
your.domain/admin→ onboarding page → fill Team + AUD → click Enable Access - The admin now only accepts Access JWTs (401 / redirected to Access login otherwise)
Access apps deny everything by default — you must add an Allow rule or you'll get 403 after logging in:
- In the app's Policies tab → Add a policy
- Action =
Allow; Include =Everyone(or specific email/group) - If you protect
/admin*and want/dlpublic:- Policy 1:
/dl*→ Allow Everyone - Policy 2:
/admin*→ Allow your email / Everyone
- Policy 1:
| Symptom | Cause | Fix |
|---|---|---|
| 403 Forbidden after login | No Allow rule (default deny-all) | Add an Allow policy |
| Admin keeps returning 401 | AUD wrong/outdated | Use the real AUD from "Other settings" |
| Hostname-level app sends cookie only, no header | CF edge behavior | EdgeDrive reads both (header + cookie) — nothing to do |
Hostname apps on workers.dev subdomains fail |
Known CF bug | Bind a custom domain for hostname apps |
Full manual (with verification steps): docs/cloudflare-access.md
Symptom: Access login succeeds but EdgeDrive keeps returning 401; or you need to change AUD after onboarding but can't reach /admin.
Fix: Access config lives in the D1 settings table — flip it back to onboarding mode from the Cloudflare dashboard (no local tools, nothing to install):
- Log in to the Cloudflare dashboard → Storage & Databases → D1 → click your database (e.g.
edgedrive-db) - Open the Console tab (the SQL query box)
- Run:
UPDATE settings SET value='0' WHERE key='access_enabled';- Visit
/admin— the onboarding page shows again → fill in the correct Team + AUD → click Enable Access (this writesaccess_enabledback to1automatically)
No
wrangler, no Node, no local installs — everything runs in the Cloudflare web console.
- Drag files into the admin, or pick files (multi-select supported)
- Files >8MB auto-multipart (8MB parts, 4 concurrent, retry on failure) — no size limit
- Uploads land in the current folder
- Inline / context menu "Expiry": hours / days / permanent / custom / expire-now
- Bulk select → bulk set expiry / make permanent / expire now
- After expiry:
/dlreturns 410 Gone; physical deletion runs in the daily purge (04:00 UTC)
- Long file link (default copy):
/dl/<path>/<filename>?t=<token>— path + name stay readable;tis the capability token - Preview:
/dl/<path>/<filename>/view?t=<token> - Short link (optional):
/s/<short_code>(6–8 char base62) → 302 to the long URL - Batch:
/dl/batch/<token>(preview) and?mode=download(auto-download) - Old tokenless
/dl/<path>/<filename>returns 404 — downloads require a share token - Revoked / expired / download-limit exhausted → 410
- Password-protected links redirect to
/share/<token>(themed + i18n). 5 wrong tries lock for 10 minutes; success sets an HttpOnly cookie for 30 minutes - Markdown, PDF, TXT scroll inside height-limited containers — never stretch the page
- Supports
Rangeheaders (resume, video seeking)
One file can have many share links at once (open, password, limited, different expiry) — they do not affect each other. Manage them under Admin → Shares.
- Deletes go to the recycle bin (restorable); daily purge permanently removes after 30 days
- Edit tags inline or via context menu; filter the list by tag
- Star favorites; "Recent" sorts by upload time desc
The browser computes SHA-256 before upload; if a file with the same hash exists, the R2 object is copied to the new key with a new D1 record — zero file bytes transferred.
Multi-select → bulk bar has two buttons (no nested dialogs):
| Button | Copied link | What opens |
|---|---|---|
| Copy link | /dl/batch/{token}?mode=download |
Web-disk list + auto-triggered per-file downloads |
| Copy preview link | /dl/batch/{token} |
Web-disk list, per-file preview / download + "Download all" |
- Each click creates a new batch (high-entropy token, 32-byte base64url); stored in
share_links - Up to 100 files; expiry = shortest expiry among selected files; all-permanent → batch is permanent
- Page lists type icon / name / size / expiry status; deleted files are skipped
- No server-side ZIP (Workers CPU limit) — "Download all" = browser clicks
<a download>every 300ms - Browsers may block gesture-less multi-downloads: the page shows "if blocked, click Download all below or allow downloads"
- Expired / revoked / exhausted batch → 410; invalid token → 404
- Single-file row action Share copies a default long link (
/dl/.../?t=token) or opens Shares to create another
- Shares sidebar: every file/batch link with copy / password / extend / revoke / delete / shorten
- Bulk bar (appears when ≥1 file selected): count, copy link, copy preview link, move, expiry, delete, clear
- Move: tree-style folder picker (root + expandable children), not a dropdown
- Selection: list rows get primary-tinted background; grid cards get 3px primary outline
- Themes: three cards in Settings (Onyx / Porcelain / Nocturne); saved → admin and public
/dlpages switch together
- Admin → Stats: R2 capacity & operations, D1 reads/writes & rows, Worker requests & errors (from GraphQL Analytics)
- Enable by filling Cloudflare Account ID + API Token in Settings → Account (token only needs
Account Analytics Read) - Free-tier usage bar is for reference; billing follows your plan
There is no password login. Better-Auth has been removed. Identity is Cloudflare Access JWT only.
| Stage | Behavior |
|---|---|
| Access not enabled | /admin is unauthenticated, shows only the onboarding page (Team / AUD → Enable Access) |
| Access enabled | requireAdmin verifies Access JWT only; unauthenticated → 401 page (no /login redirect) |
Public downloads /dl/* stay anonymous.
EdgeDrive reads the Access JWT from two places:
cf-access-jwt-assertionheader (injected by Worker-level protection — hostname-level apps may not inject it)CF_Authorizationcookie (hostname-level apps usually send only this — the cookie is the JWT itself, same verification)
Don't "fix" Access config just because you see 401 after login — first check whether it's the JWT channel (hostname-level apps sending cookie-only is normal — EdgeDrive already handles both).
| Symptom | Cause | Fix |
|---|---|---|
/admin returns 401 page (no Access login) |
Access doesn't protect that URL | Check Target path (should be admin*) |
| 401 after Access login | ① D1 AUD is stale ② JWT unreadable | ① sync D1 (UPDATE settings SET value='<newAUD>' WHERE key='cf_access_aud') ② confirm dual-channel build |
| 403 Forbidden (Access page) | Policy denies | Add Allow rule (your email / Everyone) |
| Bypass policy | No JWT injected and effectively unprotected | Use Allow instead |
AUD changes: recreating the Access app / clicking "revoke tokens" regenerates AUD — you must sync D1 or verification fails.
| Name | Type | Required | Description |
|---|---|---|---|
CF_API_TOKEN |
Secret | Optional | For usage stats (takes priority over the token entered in Settings) |
SETUP_TOKEN |
Secret | Optional | Protects first-time onboarding. Unset = open; set = onboarding requires this token |
Site config, cron tokens, Access Team/AUD all live in D1 by default — deploy with zero configuration.
npm install
npx wrangler d1 create edgedrive-db # create a local D1 (or let the deploy flow create it)
npm run devRequires the D1 / R2 bindings in wrangler.jsonc. Typecheck: npm run typecheck; tests: npm test.
npm test # Vitest: sanitize / JWT / expiry / Access guard / share links / batch / themes / LIKE
npm run typecheck
npm run build # generates D1 bootstrap SQL (incl. share_links) + OpenNext WorkerGitHub Actions runs tests + typecheck on every push.
migrations/ D1 migrations (→ schema_version 14)
src/
app/
admin/ Admin (files / shares / stats / settings)
api/
share/ CRUD + password verify + short codes (Access on admin routes)
batch/ POST create batch share (Access protected)
files/ list, upload, MPU, batch expiry/delete, copy, check, admin download
cron/purge/ expired files + leftover batch_links cleanup
dl/
[...path]/ tokenized single-file download / /view preview
batch/[token]/ batch share page (public)
s/[code]/ short-link 302
share/[token]/ public password page
components/admin/ FileManager, ShareManager, FolderTree, PickFolderDialog, theme settings, stats
lib/
share.ts unified share_links CRUD / access / password / short codes
batch.ts batch helpers on share_links
batch-page.ts batch page HTML
themes.ts Onyx / Porcelain / Nocturne
store.ts files / folders / D1
scripts/ cf-build / cf-deploy / wrangler shim
- Next.js 16 (App Router) + OpenNext → Cloudflare Workers
- Cloudflare D1 (SQLite metadata + config +
share_links) - Cloudflare R2 (object storage, free 10GB + zero egress)
- Refine + MUI (admin hooks / tables / dialogs)
- Cloudflare Access (JWT auth)
- Vitest (tests)
GNU Affero General Public License v3.0 (AGPL-3.0) — network copyleft: modified versions served over a network must be released under the same license with full source.