v0.1.7 hardens same-origin file delivery, share-root checks, password authentication, and mount publication defaults. Read the release guide before upgrading.
- Virtual root with multiple independently named storage mounts
- OneDrive Personal OAuth authorization with PKCE and encrypted refresh tokens
- Multiple OneDrive accounts, each mounted at a custom top-level path
- Multiple Google My Drive accounts or roots, each independently authorized and mounted at a custom top-level path
- S3-compatible mounts with AWS Signature Version 4
- Cloudflare R2 through either S3 credentials or the built-in Worker binding
- Public directory browsing, stable file links, downloads, and common file previews
- Revocable file and folder shares with optional passwords, expiration, and download policy
- English and Simplified Chinese interface with system, light, and dark themes stored locally
- List and grid views, breadcrumbs, sorting, search, keyboard selection, and responsive layout
- Responsive storage and appearance administration for desktop, tablet, and mobile screens
- Administrator login, upload, folder creation, rename, move, delete, and visibility controls
- Resumable OneDrive uploads and multipart S3 uploads with pause, resume, retry, cancel, and progress controls
- D1 migrations and compatibility support for legacy R2 object links
- Streamed provider responses without buffering complete files in Worker memory
| Storage | Browse | Download | Upload | Manage | Notes |
|---|---|---|---|---|---|
| OneDrive Personal | ✓ | ✓ | ✓ | ✓ | Resumable upload; personal Microsoft accounts only |
| Google My Drive | ✓ | ✓ | ✓ | ✓ | Range downloads, resumable upload, and Docs/Sheets/Slides export |
| Cloudflare R2 binding | ✓ | ✓ | ✓ | ✓ | Built-in compatibility mount; single-request upload only |
| Cloudflare R2 through S3 | ✓ | ✓ | ✓ | ✓ | Multipart upload with the R2 S3 endpoint and scoped credentials |
| Other S3-compatible storage | ✓ | ✓ | ✓ | ✓ | Multipart compatibility depends on the provider's S3 implementation |
OneDrive Personal Vault is not exposed. Microsoft Graph returns the locked vault without a usable file or folder facet, so ilist skips it instead of failing the parent directory.
Browser
|
+-- React + Vite UI (Workers Assets)
|
+-- Cloudflare Worker
+-- Native request router and session authentication
+-- Virtual filesystem and storage driver registry
+-- OneDrive Personal driver -> Microsoft Graph
+-- Google Drive driver -> Google Drive API v3
+-- S3 driver -> R2 or another S3-compatible provider
+-- D1 -> mounts, encrypted credentials, entries, sessions, shares
+-- R2 binding -> built-in compatibility storage
The Worker acts as the control plane and streams or redirects file data where possible. Provider credentials are encrypted before they are written to D1.
-
Prerequisites. Install Node.js 22.12 or newer and npm 10 or newer. Have a Cloudflare account with Workers, D1, and R2 enabled, Wrangler authenticated with
npx wrangler login, a Microsoft Entra application for OneDrive Personal, and a Google Cloud OAuth application for Google Drive. -
Clone and install.
git clone https://github.com/chius-me/ilist.git cd ilist npm install -
Create D1 and R2 resources.
npx wrangler d1 create ilist-db npx wrangler r2 bucket create ilist-files
-
Configure
wrangler.jsonc, the custom domain, and D1 migrations. Copy the D1database_idreturned by Wrangler intowrangler.jsonc, confirm the database and bucket names, and keep the configuredilist.chius.cccustom-domain route only when that hostname is in your Cloudflare zone. Then run:npx wrangler d1 migrations apply ilist-db --remote
-
Generate the administrator password hash and random keys.
npm run hash-password # ADMIN_PASSWORD_HASH; enter the password at the prompt openssl rand -base64 32 # CREDENTIAL_MASTER_KEY openssl rand -hex 32 # SESSION_SECRET
The password command rejects password arguments. On a terminal it disables character echo while reading the prompt. Automation may provide exactly one password line through non-TTY standard input; stdout contains only the generated hash, while prompts and errors use stderr.
New hashes use
pbkdf2-sha256:600000. Legacypbkdf2hashes remain valid for this release, but an administrator hash is a Cloudflare Secret and cannot be upgraded automatically. After a successful legacy login, rotate it with these exact commands:npm run hash-password npx wrangler secret put ADMIN_PASSWORD_HASH
-
Store all eight required Worker secrets. Use the generated values and the provider application values with
npx wrangler secret put:npx wrangler secret put ADMIN_PASSWORD_HASH npx wrangler secret put CREDENTIAL_MASTER_KEY npx wrangler secret put SESSION_SECRET npx wrangler secret put MICROSOFT_CLIENT_ID npx wrangler secret put MICROSOFT_CLIENT_SECRET npx wrangler secret put GOOGLE_CLIENT_ID npx wrangler secret put GOOGLE_CLIENT_SECRET npx wrangler secret put PUBLIC_ORIGIN
The canonical production value is
https://ilist.chius.cc.PUBLIC_ORIGINmust exactly match the deployed HTTPS origin and have no trailing slash. -
Run
npm run checkandnpm run deploy.npm run check npm run deploy
-
Sign in as the
ADMIN_USERNAMEvalue, which defaults toadmin. The password is the plaintext value used to generateADMIN_PASSWORD_HASH.
Open /admin/storages after signing in. Each mount has its own display name, top-level mount path, provider and encrypted credentials, public or private visibility, enabled state, and optional provider root. Deleting or disconnecting a mount removes only ilist configuration and credentials; it does not delete the provider account, bucket, drive, or stored objects.
For OneDrive Personal, follow docs/onedrive-setup.md. Use a Microsoft Entra application configured for personal Microsoft accounts only, with the Web redirect URI https://ilist.chius.cc/api/admin/oauth/onedrive/callback and delegated Graph permissions User.Read and Files.ReadWrite. Retain the existing https://ilist.chius.workers.dev/api/admin/oauth/onedrive/callback URI until the custom-domain authorization flow is verified.
For Google Drive, follow docs/google-drive-setup.md. Enable Google Drive API, create a Web OAuth client with redirect URI https://ilist.chius.cc/api/admin/oauth/google/callback, and configure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. Retain the existing https://ilist.chius.workers.dev/api/admin/oauth/google/callback URI until the custom-domain authorization flow is verified. ilist requests https://www.googleapis.com/auth/drive; keep a development consent screen in testing with explicit test users, or complete Google's production verification requirements before serving other users.
For Cloudflare R2 through S3, use:
Endpoint: https://ACCOUNT_ID.r2.cloudflarestorage.com
Region: auto
Addressing mode: path style
Bucket name: ilist-files
Access key ID: R2 API token access key ID
Secret access key: R2 API token secret access key
Use a bucket-scoped R2 API token with only the permissions ilist requires.
- Files smaller than
10 MiBuse the existing single-request upload path. - Files of exactly
10 MiBor larger use resumable upload when the current OneDrive, Google Drive, or S3 mount advertises multipart support. - Parts are uploaded sequentially in
10 MiBchunks. The queue runs at most two files concurrently. - Pause, resume, and retry preserve the opaque ilist upload session and server-confirmed parts while the page remains open. Reloading or leaving the page discards the in-memory queue; unfinished server sessions are later cleaned up, but automatic recovery after reload is not implemented.
- Provider upload URLs, OneDrive session proofs, Google resumable session URLs, and S3 upload IDs remain encrypted or server-side and are never returned to the browser.
- The built-in
R2Worker binding remains compatible with existing deployments but does not implement resumable upload; use an S3-configured R2 mount for multipart uploads. - Configure an incomplete multipart upload lifecycle rule on S3-compatible buckets so abandoned provider uploads are removed if Worker cleanup cannot reach them.
OneDrive resumable upload uses the same delegated Files.ReadWrite permission documented above. Google Drive uses the full Drive OAuth scope documented above. Apply all D1 migrations, including 0012_upload_sessions.sql, 0013_upload_terminal_leases.sql, 0014_shares.sql, 0015_auth_rate_limits.sql, and 0016_mounts_private_default.sql, before deploying v0.1.7.
Administrators can create a share from any file or folder action menu and manage existing shares at /admin/shares. A share may require a password, expire at a chosen time, block explicit downloads, or be disabled and re-enabled. Folder shares support nested browsing, list and grid views, and the same safe preview types as the main explorer.
Google Docs, Sheets, and Slides expose explicit export formats in both the main explorer and controlled shares. PDF is used for preview when available; downloads remain subject to the share's current download policy.
The raw /s/:token URL is returned only once when the share is created. D1 stores only its SHA-256 hash, so the management page cannot recover or copy an existing link. Public item IDs are share-scoped encrypted handles rather than mount or provider IDs. Password authorization uses a short-lived, HttpOnly, SameSite=Lax cookie scoped to that share path.
Every metadata, listing, preview, and file request rechecks the current password, enabled, expiration, target, and download policy. Share responses use Cache-Control: private, no-store; do not add a Cloudflare cache rule that overrides this policy. Disabling or deleting a share therefore takes effect on the next request.
Public share routes are rooted at /s/:token. Administrator automation may use GET, POST, PATCH, and DELETE under /api/admin/shares, subject to the normal administrator session and same-origin protections.
Create local secrets from the tracked template:
cp .dev.vars.example .dev.vars
npx wrangler d1 migrations apply ilist-db --local
npm run devFill in test-only values before starting Wrangler. It normally serves the application at http://localhost:8787. OAuth cannot complete against that plain-HTTP origin; use a deployed Worker or an HTTPS development hostname for the callback flow. Local D1 and R2 data are isolated from production. Never put production credentials in .dev.vars.
| Command | Purpose |
|---|---|
npm run dev |
Run the local Worker and UI |
npm run dev:web |
Run the frontend-only Vite development server |
npm run build |
Build the Vite frontend |
npm run test:worker |
Run Worker runtime tests |
npm run test:ui |
Run UI tests |
npm run test:e2e |
Run browser workflows in desktop, tablet, and mobile viewports |
npm run test:visual |
Run browser screenshot scenarios |
npm run test |
Run all tests |
npm run check |
Type-check, build, and run all tests |
npm run deploy |
Build and deploy with Wrangler |
npm run hash-password |
Prompt for and generate an administrator password hash |
npm run migrate:objects -- --local |
Import legacy object rows into the entry model locally |
npm run migrate:objects -- --remote |
Import legacy object rows in production |
.github/workflows/ci-cd.yml runs npm run check on every pull request and every push to main.
After a green check on main (or on manual workflow_dispatch), the same workflow applies pending remote D1 migrations, then deploys the Worker and Assets with Wrangler. Worker secrets set with wrangler secret put stay in Cloudflare and are not re-uploaded by CI.
Configure these repository secrets (Settings → Secrets and variables → Actions) before the first automated deploy:
| Secret | Purpose |
|---|---|
CLOUDFLARE_API_TOKEN |
API token with permission to edit Workers, Workers Routes / Custom Domains, and D1 for this account |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account ID that owns the ilist Worker, ilist-db D1 database, and ilist-files R2 bucket |
Do not put ADMIN_PASSWORD_HASH, CREDENTIAL_MASTER_KEY, OAuth client secrets, or other Worker secrets into GitHub. Keep them only as Cloudflare Worker secrets.
- Keep
CREDENTIAL_MASTER_KEYstable. Changing it without a re-encryption migration makes stored provider credentials unreadable. - Rotate any credential that has appeared in a terminal recording, screenshot, issue, or chat.
- Back up D1 before applying migrations or deploying a new release.
- Use least-privilege, bucket-scoped S3 or R2 credentials.
- Do not commit
.dev.vars, D1 exports, access tokens, client secrets, or temporary uploads. - Private mounts rely on ilist authorization; review Cloudflare logs, Access policies, and caching rules for your deployment.
- Share links are bearer credentials. Send them through an appropriate private channel and add a password for sensitive targets.
- Existing share URLs cannot be recovered from D1; create a replacement share if the original URL is lost.
- Same-origin HTML, SVG, XML, PDF, and unknown file types are sent as attachments with a sandboxed file response policy. Only a narrow, exact image/audio/video MIME allowlist is previewed inline.
- New mounts are private by default. Publishing a mount requires explicit administrator confirmation because unauthenticated visitors can browse it.
- Share item handles are checked against the live share root on every access. Moving an item outside the shared folder revokes its old handle immediately.
- Failed administrator attempts are limited before PBKDF2 by both a five-per-minute IP-wide budget and a five-per-minute IP-plus-normalized-username budget. Share-password attempts remain limited to ten per minute per IP and share. Do not put a caching rule in front of share responses.
- OneDrive and Google Drive share ancestry is checked live, but a provider-side move concurrent with a metadata or download request leaves a narrow unavoidable TOCTOU window. Restrict provider write access for sensitive public shares and disable a share when immediate revocation is required.
- Single administrator; no registration or multi-user permission model
- OneDrive Personal only; work and school tenants are not yet supported
- Google support is limited to My Drive. Shared Drives, Shared with me, and shortcut traversal are not implemented.
- No WebDAV, FTP, SFTP, SMB, or local filesystem drivers
- Resumable recovery is page-session-only; reloading the page does not restore the upload queue
- Built-in R2 binding uploads remain single-request and subject to Cloudflare request-body limits
- No cross-mount copy or move
- Shares do not support uploads, recipient accounts, access quotas, or access counters
- No offline download, archive extraction, media transcoding, or background task system
- Provider listings are fetched live; distributed directory caching is not implemented
- Built-in R2 recursive deletion is bounded and reports per-entry failures; S3-compatible and OneDrive folder deletion follow provider-specific behavior
Before changing production, export D1 and keep the export outside Git:
npx wrangler d1 export ilist-db --remote --output /tmp/ilist-db-before-multi-mount.sql
npx wrangler d1 migrations apply ilist-db --remote
npm run migrate:objects -- --remote
npm run deployThe v0.1.x releases are intended to remain compatible with legacy R2 object links. Migrations add the entry and mount model without deleting files or legacy rows. For the v0.1.7 security release, follow the release abort procedure: do not return public traffic to v0.1.6, and retain 0015 and 0016. Restore a D1 export only when data itself is damaged or a migration failed.
src/
ui/ React file explorer and admin UI
worker/
index.ts Native Worker entry point
router.ts HTTP route dispatch
file-system.ts Virtual filesystem operations
drivers/
onedrive/ Microsoft Graph driver and OAuth tokens
google/ Google Drive API driver and OAuth tokens
s3/ S3-compatible driver and SigV4 client
migrations/ D1 schema migrations
tests/worker/ Worker runtime tests
tests/ui/ React component and interaction tests
docs/ Setup and implementation documentation
- Work and school Microsoft accounts
- Google Shared Drives and shortcuts
- Cross-mount copy and move
- Additional storage drivers and background operations
Run npm run check before submitting a change. Keep provider credentials, local variables, D1 exports, and temporary uploads out of Git. Update the relevant documentation when changing storage behavior or deployment requirements.
ilist is licensed under the GPL-3.0-only license.