# S3-Compatible TeleUploader + Web File Manager **Date:** 2026-07-06 **Status:** Approved Objective: Transform TeleUploader into an S3-compatible storage server (Telegram-backed) with a web file manager UI. Existing upload/download API paths remain unchanged. --- ## 1. Architecture Overview ``` S3 Client (aws-cli, rclone, s3cmd, MinIO Client) │ ▼ AWS SigV4 + XML ┌─────────────────────────────────────┐ │ S3 Protocol Dispatcher │ src/routes/s3.ts │ - Parses path, query, auth headers │ │ - Routes to S3 operation handlers │ ├─────────────────────────────────────┤ │ JSON API v1 (for Web UI) │ src/routes/web-api.ts │ - Bucket CRUD │ │ - Object list/upload/copy/delete │ ├─────────────────────────────────────┤ │ Web File Manager │ src/routes/home.ts + home.html │ - Bucket browser, upload/download │ ├─────────────────────────────────────┤ │ Database Layer │ src/db/buckets.ts │ - buckets, files (extended), │ src/db/multipart.ts │ - multipart_uploads, parts │ ├─────────────────────────────────────┤ │ Telegram Storage Layer │ src/utils/telegram.ts (existing) │ - Single Telegram channel │ │ - Files stored as Telegram docs │ └─────────────────────────────────────┘ ``` ### Component Responsibilities | Component | Responsibility | |-----------|----------------| | **S3 Protocol Dispatcher** | S3 fallback route (`$`). Parse Auth header, detect SigV4, route by method+path+query | | **S3 Operations** | ~20 operations (ListBuckets, GetObject, PutObject, ListObjectsV2, MultipartUpload, DeleteObjects, etc) | | **Auth (SigV4)** | Verify AWS SigV4 signatures, reject invalid/missing with 403 XML error | | **XML Builder** | Template functions to construct S3 XML responses. Parse incoming XML (DeleteObjects body) | | **JSON API v1** | JSON wrapper for web UI to avoid XML parsing in browser | | **Web File Manager** | Single-page app served at `/` with bucket browser, upload, delete, search | | **DB Layer** | New tables: buckets, multipart_uploads, multipart_parts. Extended files table | | **Telegram Storage** | Existing forwardToStorage/getFileInfo; unchanged | ### Routing Priority Route matching order (existing unchanged, S3 added as catch-all): | Priority | Route | Handler | |----------|-------|---------| | 1 | `/api/upload` | Existing upload handler | | 2 | `/f/:public_id` | Existing file redirect | | 3 | `/file/:public_id/info` | Existing file info | | 4 | `/health` | Existing health | | 5 | `/docs` / `/swagger.json` | Existing swagger | | 6 | `/` | Web file manager UI (NEW) | | 7 | `/api/v1/*` | JSON API for Web UI (NEW) | | 8 | `$` (catch-all) | S3 dispatcher (NEW) | The catch-all route (`$` in Bun.serve()) inspects the `Authorization` header: - Contains `AWS4-HMAC-SHA256` → handle as S3 request - Otherwise → 404 --- ## 2. Database Schema ### 2.1 buckets Table (NEW) ```sql CREATE TABLE buckets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- Bucket name constraints: lowercase, no underscore, 3-63 chars (S3 spec) ``` ### 2.2 files Table — Extended New columns added alongside existing ones: ```sql ALTER TABLE files ADD COLUMN bucket_id UUID REFERENCES buckets(id); ALTER TABLE files ADD COLUMN s3_key TEXT; -- e.g. "images/logo.png" ALTER TABLE files ADD COLUMN storage_backend TEXT DEFAULT 'telegram'; ALTER TABLE files ADD COLUMN is_deleted BOOLEAN DEFAULT false; ALTER TABLE files ADD COLUMN multipart_upload_id TEXT; CREATE UNIQUE INDEX idx_files_bucket_key ON files(bucket_id, s3_key) WHERE is_deleted = false; CREATE INDEX idx_files_bucket_prefix ON files(bucket_id, s3_key text_pattern_ops); ``` - `s3_key` is the full object key path - `is_deleted` enables soft-delete for S3 DeleteObject - `multipart_upload_id` links parts to their parent completed multipart object - `public_id` (existing) remains primary identifier for Telegram redirect - Existing non-S3 uploads have `bucket_id = NULL, s3_key = NULL` ### 2.3 multipart_uploads Table (NEW) ```sql CREATE TABLE multipart_uploads ( upload_id TEXT PRIMARY KEY, bucket_id UUID NOT NULL REFERENCES buckets(id), s3_key TEXT NOT NULL, initiated_at TIMESTAMPTZ DEFAULT now(), status TEXT DEFAULT 'in_progress' CHECK (status IN ('in_progress', 'completed', 'aborted')), initiated_by TEXT ); ``` ### 2.4 multipart_parts Table (NEW) ```sql CREATE TABLE multipart_parts ( id SERIAL PRIMARY KEY, upload_id TEXT NOT NULL REFERENCES multipart_uploads(upload_id) ON DELETE CASCADE, part_number INT NOT NULL CHECK (part_number BETWEEN 1 AND 10000), telegram_file_id TEXT NOT NULL, telegram_file_unique_id TEXT NOT NULL, storage_message_id BIGINT NOT NULL, size_bytes BIGINT NOT NULL, etag TEXT NOT NULL, -- SHA256 hash as etag created_at TIMESTAMPTZ DEFAULT now(), UNIQUE(upload_id, part_number) ); CREATE INDEX idx_multipart_parts_upload ON multipart_parts(upload_id, part_number); ``` ### 2.5 Multipart Storage Strategy Each part is stored as an independent file in the Telegram channel. The DB links them logically: - **UploadPart**: forward part to Telegram via forwardToStorage → record in multipart_parts - **CompleteMultipartUpload**: create a single `files` row with `multipart_upload_id` pointing to the parts. No new Telegram upload needed. - **GetObject for multipart object**: query parts ordered by part_number, stream sequentially via Telegram CDN URLs. Client sees a single continuous download. - **AbortMultipartUpload**: delete DB records only (Telegram orphan files are accepted as GC is not feasible) --- ## 3. S3 Protocol Layer ### 3.1 Full Endpoint Coverage (~20 endpoints) #### Bucket Operations | Method | Path | Query Params | Handler Description | |--------|------|-------------|-------------------| | GET | `/` | — | List all buckets → `` XML | | PUT | `/{bucket}` | — | Create bucket (reject if exists) → 200 | | HEAD | `/{bucket}` | — | Check bucket exists → 200 or 404 | | DELETE | `/{bucket}` | — | Delete empty bucket → 204 | #### Object Operations | Method | Path | Query / Special | Handler Description | |--------|------|----------------|-------------------| | GET | `/{bucket}/{key+}` | — | GetObject: redirect to Telegram CDN (same as existing `/f/:public_id`) | | HEAD | `/{bucket}/{key+}` | — | Return metadata headers (Content-Length, Content-Type, ETag, Last-Modified) | | PUT | `/{bucket}/{key+}` | `x-amz-copy-source`? | Without copy header: upload multipart form → Telegram. With copy header: copy existing object in DB | | DELETE | `/{bucket}/{key+}` | — | Soft-delete (is_deleted = true) → 204 | | POST | `/{bucket}/{key+}` | `?tagging` | Return 204 (no-op, tagging not implemented) | #### Object Listing | Method | Path | Query Params | Handler Description | |--------|------|-------------|-------------------| | GET | `/{bucket}` | — | ListObjects v1 → `` XML | | GET | `/{bucket}` | `?list-type=2` | ListObjectsV2 → `` XML | Supports: `prefix`, `delimiter`, `max-keys` (default 1000), `continuation-token` (v2), `marker` (v1), `encoding-type=url`. #### Batch Operations | Method | Path | Query Params | Handler Description | |--------|------|-------------|-------------------| | POST | `/{bucket}` | `?delete` | Parse XML body `...` → soft-delete each → `` XML | #### Multipart Upload | Method | Path | Query Params | Handler Description | |--------|------|-------------|-------------------| | POST | `/{bucket}/{key+}` | `?uploads` | Initiate: create multipart_uploads row → `` XML with UploadId | | PUT | `/{bucket}/{key+}` | `?partNumber=N&uploadId=X` | Upload part: stream to Telegram → record in multipart_parts → return ETag header | | POST | `/{bucket}/{key+}` | `?uploadId=X` | Complete: parse `N...` → create files row with multipart_upload_id → `` XML | | DELETE | `/{bucket}/{key+}` | `?uploadId=X` | Abort: delete records, update status → 204 | | GET | `/{bucket}/{key+}` | `?uploadId=X` | List parts → `` XML | ### 3.2 AWS Signature V4 (SigV4) Authentication process for each S3 request: ``` 1. Extract Authorization header 2. Parse credential scope (date/region/service) 3. Reconstruct CanonicalRequest 4. Compute expected signing key 5. Compare signatures 6. Match → proceed; Mismatch → 403 Forbidden with AWS XML error ``` **Canonical Request construction:** ``` \n \n \n \n \n ``` **Implementation notes:** - Only verify signature; do not validate timestamp freshness (simplified, acceptable for single-server setup behind reverse proxy) - Hard-code `region = 'us-east-1'` (irrelevant for functionality; S3 clients accept any region) - Single set of S3 credentials (`S3_ACCESS_KEY` + `S3_SECRET_KEY` in env) - Chunked transfer encoding (`aws-chunked`) — not supported initially; client must use standard PUT. Return 501 if detected. ### 3.3 XML Serialization No external XML library. Template strings for responses: ```typescript // Example: ListBuckets response const listBucketsXml = (buckets: Bucket[]) => ` ${buckets.map(b => ` ${escapeXml(b.name)} ${b.createdAt.toISOString()} `).join('')} `; ``` **XML parsing** (only for DeleteObjects body): use simple string matching / regex on `...` tags. The DeleteObjects XML is simple and well-structured enough for this without a parser library. ### 3.4 Error Responses All S3 errors return XML with HTTP status: ```xml NoSuchKey The specified key does not exist. /bucket/key ... ``` Common error codes: `NoSuchBucket`, `NoSuchKey`, `BucketAlreadyExists`, `BucketNotEmpty`, `InvalidPartOrder`, `NoSuchUpload`, `SignatureDoesNotMatch`, `AccessDenied`, `InternalError`. ### 3.5 Presigned URL Support `GET /{bucket}/{key+}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=...` For presigned GET URLs: - Verify signature (much simpler — no body hash required, querystring-based) - If valid → return redirect to Telegram CDN (same as GetObject) - If expired → 403 Only presigned GET is essential; presigned PUT is optional for v1. --- ## 4. Web File Manager UI ### 4.1 Served At Route `'/'` → `home.html` (single HTML file with embedded CSS + JS) ### 4.2 Layout (responsive) ``` ┌─────────────────────────────────────────┐ │ ☰ TeleUploader ◉ my-bucket ▼ [+] │ ← Top bar ├─────────────────────────────────────────┤ │ ─────────────────────────────────────── │ │ 🗂 images/ Jul 06 2 items │ │ 🗂 documents/ Jul 05 5 items │ │ 📄 logo.png 2.4 MB Jul 06 [⋮ ▼] │ ← Actions dropdown │ 📄 report.pdf 1.2 MB Jul 05 [⋮ ▼] │ │ 📄 photo.jpg 3.1 MB Jul 04 [⋮ ▼] │ │ ─────────────────────────────────────── │ │ ↑ Load more │ └─────────────────────────────────────────┘ │ Drag & drop upload area (footer) │ └─────────────────────────────────────────┘ ``` ### 4.3 Features | Feature | Implementation | |---------|----------------| | **Bucket selector** | Dropdown, fetches bucket list from `/api/v1/buckets` | | **Create bucket** | Prompt for name → POST `/api/v1/buckets` | | **Delete bucket** | Confirm → DELETE `/api/v1/buckets/{name}` | | **Object listing** | GET `/api/v1/buckets/{name}/objects?prefix=X&delimiter=/` | | **Folder navigation** | Breadcrumb from prefix, click to drill down | | **Upload** | Drag-drop or click → POST `/api/v1/buckets/{name}/upload` | | **Upload progress** | XMLHttpRequest.upload.onprogress | | **Download** | Direct download via GET `/api/v1/buckets/{name}/download/{key}` | | **Copy link** | Copy full S3 URL to clipboard | | **Delete** | Confirm → soft delete | | **Search** | Filter by key prefix (debounced) | | **Credentials info** | Show S3_ACCESS_KEY + S3_SECRET_KEY (from env) in a modal | | **Dark/light mode** | CSS variables, @media prefers-color-scheme | ### 4.4 Web API v1 (JSON) These endpoints enable the UI without XML: | Method | Path | Response | |--------|------|----------| | GET | `/api/v1/buckets` | `{buckets: [{id, name, createdAt, objectCount}]}` | | POST | `/api/v1/buckets` | `{id, name}` | | DELETE | `/api/v1/buckets/{name}` | `{success: true}` | | GET | `/api/v1/buckets/{name}/objects?prefix=&delimiter=/&continuationToken=` | `{objects, prefixes, isTruncated}` | | POST | `/api/v1/buckets/{name}/upload` | Multipart form → `{key, size, etag}` | | DELETE | `/api/v1/buckets/{name}/{key+}` | `{success: true}` | | POST | `/api/v1/buckets/{name}/copy` | `{sourceKey, destKey}` | ### 4.5 Download via Web UI For the web UI, download doesn't use S3 headers. Instead: - `GET /api/v1/buckets/{name}/download/{key+}` → redirects to Telegram CDN, similar to `/f/:public_id` - This bypasses S3 auth (which the browser can't do with SigV4) --- ## 5. Existing Routes — Unchanged All existing functionality remains intact: - `POST /api/upload` — multipart + JSON file upload - `GET /f/:public_id` — file redirect to Telegram CDN - `GET /file/:public_id/info` — file metadata - `GET /health` — database health - `GET /docs` + `/swagger.json` — API docs The existing upload flow does not interact with S3 bucket/keys. This is intentional: the JSON API remains for simple programmatic upload without S3 complexity. --- ## 6. Security Model | Layer | Mechanism | |-------|-----------| | **S3 API** | SigV4 signature verification. Single credential pair from env | | **Web UI** | No auth (internal tool). Relies on network-level security / reverse proxy | | **JSON API** (existing) | Rate-limited only (same as now). No additional auth | | **Rate limiting** | Applied to S3 operations per IP | --- ## 7. Implementation Order The following build sequence minimizes blocked dependencies: | Phase | Tasks | Depends On | |-------|-------|------------| | **1. DB schema** | Create `buckets`, `multipart_uploads`, `multipart_parts` tables. Migrate existing DB | Nothing | | **2. DB layer** | CRUD functions for buckets, multipart, files extended | Phase 1 | | **3. S3 Auth + XML** | SigV4 verification, XML builder templates | Nothing (parallel with 1) | | **4. S3 Bucket Ops** | ListBuckets, CreateBucket, HeadBucket, DeleteBucket | Phase 1, 3 | | **5. S3 Object Ops** | PutObject, GetObject, HeadObject, DeleteObject, CopyObject, DeleteObjects | Phase 2, 3, 4 | | **6. S3 Listing** | ListObjects, ListObjectsV2 | Phase 2, 3, 4 | | **7. S3 Multipart** | CreateMultipartUpload, UploadPart, Complete, Abort, ListParts | Phase 2, 3, 4 | | **8. S3 Dispatcher** | Wire up catch-all route with auth detection | Phase 4-7 | | **9. JSON Web API** | All `/api/v1/*` endpoints | Phase 2, 4 | | **10. Web UI** | home.html with bucket browser, upload, delete, search | Phase 9 | | **11. Tests** | S3 auth test, bucket operations test, object operations test, multipart test | Phase 4-8 | --- ## 8. Configuration (New Env Vars) ```env # S3-compatible API credentials S3_ACCESS_KEY=teleuploader-admin # Access key for SigV4 auth S3_SECRET_KEY=your-secret-key-here # Secret key for SigV4 auth S3_DEFAULT_REGION=us-east-1 # S3 region (cosmetic, affects signature scope) # Existing vars unchanged: # BOT_TOKEN, ADDITIONAL_BOT_TOKENS, STORAGE_CHANNEL_ID, BASE_URL, ... ``` --- ## 9. Testing Strategy | Test Focus | Scope | |------------|-------| | SigV4 auth | Verify signature verification, reject invalid signatures | | S3 Bucket operations | Create, list, head, delete buckets | | S3 Object operations | Put, get, head, delete objects (via Telegram) | | S3 ListObjects | Pagination, prefix, delimiter, continuation token | | S3 Multipart | Create, upload parts, complete, abort | | S3 DeleteObjects | Batch delete XML body parsing | | S3 Error responses | Correct XML error format per status code | | Web API | JSON endpoints return correct data | | Non-S3 routes | Existing routes still work (regression) | --- ## 10. Open Questions / Future 1. **Presigned URL support** — GET presigned URLs in v1; PUT presigned for v2 2. **CORS headers** — if web UI served from different origin than S3 API calls 3. **Versioning** — not supported (single version per key) 4. **ACL / Bucket policies** — not supported (single user model) 5. **Lifecycle rules** — not supported 6. **Website hosting** — not supported 7. **Default bucket** — consider creating a default bucket on first start for convenience 8. **Upload via S3 API directly to web UI** — web UI could also call S3 API directly for maximum compatibility demo 9. **S3-compatible client list** — tested clients: aws-cli, rclone, s3cmd, MinIO Client, Cyberduck