A personal cloud drive built on Cloudflare's free tier (Workers + D1 + R2), featuring a Vue 3 SPA frontend and a TypeScript/Hono Worker backend. All API requests are authenticated by Cloudflare Access at the edge — the Worker itself does not handle authentication.
- File & folder management: list, create, rename, move, copy, delete (soft delete with trash)
- Upload with drag-and-drop, upload queue with progress, chunked parallel upload (≥50MB automatically chunked into 5MB parts, 4 concurrent streams)
- File deduplication: client-side SHA-256 hashing, instant upload for files already on server (≤50MB)
- Download single files or from shared folders
- Image & text preview
- Share links: create expiring public share links with optional password protection for files or folders, browse shared folders
- Trash management: restore, permanent delete, empty trash, batch operations
- Search files by name
- Clipboard operations (copy/cut/paste), multi-select, batch actions
- Context menu, keyboard shortcuts (Ctrl+A, Delete, F2, Enter, Ctrl+C/X/V)
- Sorting by name/size/type/date, pagination
- Dark/light mode, multi-language UI (Chinese, English, Japanese, Russian)
- Mobile responsive
| Layer | Technology |
|---|---|
| Frontend | Vue 3 (Composition API, <script setup>) + TypeScript |
| UI | Naive UI |
| State | Pinia |
| Backend runtime | Cloudflare Workers |
| Backend framework | Hono |
| Database | Cloudflare D1 (SQLite) |
| Storage | Cloudflare R2 |
| Authentication | Cloudflare Access (Zero Trust) |
| CLI tooling | Wrangler |
| Package manager | pnpm |
| Code quality | ESLint + Prettier + TypeScript strict |
Browser (Vue 3 SPA)
│
│ HTTPS (through Cloudflare Access)
│
▼
Cloudflare Access (Zero Trust edge auth — unauthenticated requests blocked here)
│
│ Only authenticated requests reach the Worker
│
▼
Cloudflare Worker (Hono API)
│
├── R2 Bucket (file content blobs)
│
└── D1 Database (metadata: files, folders, share links)
- Zero auth logic in Worker — Cloudflare Access handles all authentication at the edge. The Worker trusts every request it receives as an Owner operation.
- D1 for metadata only — file content never stored in D1; only file records, folder hierarchy, hash index, and share links.
- R2 for blobs — files stored at
uploads/{uuid}/{fileName}via R2 native multipart upload. No server-side data assembly. - File dedup scope: global (any non-trashed file with the same hash), client SHA-256 computed for files ≤50MB.
- Direct multipart upload (>50MB) sends parallel parts from the browser directly to the R2 S3-compatible API. The Worker only signs short-lived part URLs, validates completion, and writes metadata to D1.
- Public share routes (
/s/*,/api/v1/s/*) must bypass Cloudflare Access via a Bypass policy with path selectors./assets/*is also needed because SPA static resources load from that path.
├── frontend/ # Vue 3 SPA
│ ├── src/
│ │ ├── api/ # API client modules
│ │ ├── components/ # Reusable UI components
│ │ ├── composables/ # Vue composables (useUpload)
│ │ ├── i18n/ # i18n translation resources
│ │ ├── layouts/ # Layout components
│ │ ├── pages/ # Route pages
│ │ ├── router/ # Vue Router config
│ │ ├── stores/ # Pinia stores
│ │ ├── types/ # Shared type definitions
│ │ ├── utils/ # Utility functions
│ │ ├── App.vue
│ │ └── main.ts
│ ├── index.html
│ └── vite.config.ts
│
├── worker/ # Cloudflare Worker backend
│ ├── src/
│ │ ├── routes/ # Route handlers (files, trash, shares)
│ │ ├── services/ # Business logic
│ │ ├── repositories/ # Data access (D1 + R2)
│ │ ├── types/ # Worker types & env bindings
│ │ └── index.ts # Worker entry + scheduled cron
│ ├── migrations/ # D1 migrations
│ └── wrangler.jsonc
│
├── .prettierrc
└── package.json # Root workspace
- Node.js >= 20
- pnpm >= 10
- Wrangler (included via workspace)
- A Cloudflare account with:
- An active domain on Cloudflare (full setup)
- Cloudflare Workers enabled
- D1 and R2 enabled
- Cloudflare Access (Zero Trust) enabled
git clone https://github.com/KangVin/CloudflareDrive.git && cd CloudflareDrive
pnpm install# Create D1 database
wrangler d1 create cloudflare-drive-db
# Create R2 bucket
wrangler r2 bucket create cloudflare-drive-storageUpdate worker/wrangler.jsonc with your D1 database ID.
cd worker
wrangler d1 migrations apply cloudflare-drive-db --local # local dev
wrangler d1 migrations apply cloudflare-drive-db --remote # productionCreate two Access applications in Zero Trust → Access → Applications:
- Main app —
drive.yourdomain.com→ Add an Allow policy for your email/domain - Bypass app —
drive.yourdomain.com/s/*,drive.yourdomain.com/api/v1/s/*,drive.yourdomain.com/assets/*→ Add a Bypass policy with Include → Everyone
# Terminal 1: Worker
pnpm dev:worker # starts at localhost:8787
# Terminal 2: Frontend
pnpm dev:frontend # starts at localhost:5173, proxies /api to localhost:8787# Local development
cd worker
wrangler secret put SHARED_SECRET --local
# Production
wrangler secret put SHARED_SECRETThis secret is used to sign and verify tokens for password-protected share links. Generate a random 64-character string. If not set, a hardcoded fallback is used for local dev only — you must set this in production.
Create an R2 API token with Object Read & Write permission scoped to the cloudflare-drive-storage bucket, then configure its S3 credentials:
cd worker
wrangler secret put R2_ACCOUNT_ID
wrangler secret put R2_ACCESS_KEY_ID
wrangler secret put R2_SECRET_ACCESS_KEYCopy worker/r2-cors.example.json to worker/r2-cors.json, replace the example production origin with your exact drive origin, and apply the policy:
wrangler r2 bucket cors set cloudflare-drive-storage --file r2-cors.json
wrangler r2 bucket cors list cloudflare-drive-storageThe CORS policy must allow PUT and expose ETag. Without these credentials, files larger than 50 MiB fall back to the Worker-proxied multipart path. See worker/DIRECT_UPLOAD.md for security and troubleshooting details.
# Build frontend
pnpm build:frontend
# Deploy worker (includes frontend assets via ASSETS binding)
cd worker
wrangler deployAll endpoints are prefixed with /api/v1.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/files |
List root directory |
GET |
/api/v1/files?parentId={id} |
List folder contents |
GET |
/api/v1/files/search?q= |
Search files |
GET |
/api/v1/files/by-hash?hash= |
Find file by SHA-256 hash (dedup) |
POST |
/api/v1/files/instant |
Instant upload from existing hash |
GET |
/api/v1/files/:id |
Get file/folder metadata |
POST |
/api/v1/files |
Create folder |
POST |
/api/v1/files/upload |
Upload file (≤50MB) |
POST |
/api/v1/files/upload/create |
Create multipart upload |
POST |
/api/v1/files/upload/part |
Upload a single multipart part |
POST |
/api/v1/files/upload/complete |
Complete multipart upload |
POST |
/api/v1/files/upload/direct/create |
Create a signed direct upload session |
POST |
/api/v1/files/upload/direct/part-urls |
Sign direct multipart part URLs |
POST |
/api/v1/files/upload/direct/complete |
Validate and register a direct upload |
POST |
/api/v1/files/upload/direct/abort |
Abort a direct multipart upload |
POST |
/api/v1/files/:id/copy |
Copy file/folder |
PATCH |
/api/v1/files/:id |
Rename/move file/folder |
DELETE |
/api/v1/files/:id |
Move to trash |
GET |
/api/v1/files/:id/download |
Download file |
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/trash |
List trashed files |
DELETE |
/api/v1/trash/:id |
Permanently delete |
POST |
/api/v1/trash/:id/restore |
Restore from trash |
POST |
/api/v1/trash/empty |
Empty trash |
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/shares |
List share links |
POST |
/api/v1/shares |
Create share link (supports password) |
DELETE |
/api/v1/shares/:id |
Revoke share link |
POST |
/api/v1/s/:token/verify |
Verify share password → verify_token |
GET |
/api/v1/s/:token |
Public access (file/folder) |
GET |
/api/v1/s/:token/browse/:folderId |
Browse subfolder in shared folder |
GET |
/api/v1/s/:token/download |
Public file download (supports ?vt=) |
GET |
/api/v1/s/:token/download/:fileId |
Public download from shared folder (?vt=) |
Password-protected shares require a
X-Verify-Tokenheader (or?vt=query param for downloads) obtained fromPOST /verify.
| Column | Type | Description |
|---|---|---|
id |
TEXT (UUID) | Primary key |
name |
TEXT | File or folder name |
parent_id |
TEXT | Parent folder ID (NULL = root) |
type |
TEXT | 'file' or 'folder' |
mime_type |
TEXT | MIME type (NULL for folders) |
size |
INTEGER | File size in bytes (0 for folders) |
hash |
TEXT | SHA-256 hash for dedup |
r2_key |
TEXT | R2 object key (NULL for folders) |
is_trashed |
INTEGER | 0 = active, 1 = trashed |
created_at |
TEXT | ISO 8601 timestamp |
updated_at |
TEXT | ISO 8601 timestamp |
| Column | Type | Description |
|---|---|---|
id |
TEXT (UUID) | Primary key |
file_id |
TEXT | Foreign key → files.id |
token |
TEXT | Unique share token (URL-safe) |
expires_at |
TEXT | Expiration (NULL = never) |
created_at |
TEXT | ISO 8601 timestamp |
password_hash |
TEXT | SHA-256 hash (NULL = no password) |
password_salt |
TEXT | Random UUID salt (NULL = no password) |
- Workers: 100,000 requests/day
- D1: 5 GB storage, 5M rows read/month, 100K rows written/month
- R2: 10 GB storage, 1M Class A ops/month, 10M Class B ops/month
- Cloudflare Access: up to 50 users (free plan)
A personal cloud drive typically stays well within these limits.
This project is licensed under the Apache License, Version 2.0. See the LICENSE file for details.