| 1 | # blobs |
| 2 | |
| 3 | Self-hosted browser, CLI, and TUI for S3-compatible blob storage. Built for Cloudflare R2 but works with any S3-compatible endpoint (AWS S3, MinIO, Backblaze B2, etc). |
| 4 | |
| 5 | Single binary, three modes: |
| 6 | |
| 7 | - `blobs server` — web UI (password-protected, session cookies) |
| 8 | - `blobs` (no args) — Yazi-style TUI for browsing buckets, copying URLs, opening files, previewing images inline |
| 9 | - `blobs <file>` — one-shot upload that prints the resulting URL to stdout |
| 10 | |
| 11 | Features: |
| 12 | |
| 13 | - Lists every bucket the credentials can see |
| 14 | - Folder/file navigation with breadcrumbs (web) or two-pane preview (TUI) |
| 15 | - Inline image previews via Kitty/Ghostty/iTerm2 graphics protocols (or `chafa` fallback) |
| 16 | - Presigned download URLs + optional permanent public URL map |
| 17 | - Upload (multi-file in web, single-file in CLI), replace, delete, create folder |
| 18 | |
| 19 | ## Quick start (server) |
| 20 | |
| 21 | ```sh |
| 22 | cp .env.example .env |
| 23 | # edit .env — set BLOBS_PASSWORD and either: |
| 24 | # S3_ENDPOINT + S3_ACCESS_KEY_ID + S3_SECRET_ACCESS_KEY (generic) |
| 25 | # R2_ACCOUNT_ID + S3_ACCESS_KEY_ID + S3_SECRET_ACCESS_KEY (R2) |
| 26 | go run . server |
| 27 | ``` |
| 28 | |
| 29 | Visit `http://127.0.0.1:3000` and log in. |
| 30 | |
| 31 | ## CLI / TUI |
| 32 | |
| 33 | The same binary also speaks directly to S3 (no server required): |
| 34 | |
| 35 | ```sh |
| 36 | blobs auth # interactive: writes ~/.config/blobs/config.toml |
| 37 | blobs # TUI — bucket picker then folder browse |
| 38 | blobs -b my-bucket # TUI — jump straight into my-bucket |
| 39 | blobs ./photo.png # upload to BLOBS_DEFAULT_BUCKET, print URL |
| 40 | blobs -b my-bucket --prefix imgs/ ./photo.png |
| 41 | ``` |
| 42 | |
| 43 | The TUI auto-detects Kitty/Ghostty/iTerm2 graphics protocols for inline image previews and falls back to `chafa` when available. Override with `BLOBS_PREVIEW={kitty|iterm|chafa|none}`. |
| 44 | |
| 45 | Key bindings in browse view: `enter`/`l` open, `h`/`esc` back, `y` copy URL (public if mapped, else presigned), `Y` force public, `K` copy S3 key, `o` open in browser, `u` upload, `d` delete, `r` refresh, `b` jump to buckets, `space` toggle preview, `?` help, `q` quit. |
| 46 | |
| 47 | ## Configuration |
| 48 | |
| 49 | See `.env.example` for the full list. Notable knobs: |
| 50 | |
| 51 | - `BLOBS_MAX_UPLOAD_MB` — single-shot upload cap (default 100MB) |
| 52 | - `BLOBS_PRESIGN_TTL_SECONDS` — presigned download URL lifetime (default 3600) |
| 53 | - `BLOBS_PUBLIC_URLS` — `bucket=url,bucket=url` map; when a file's bucket appears here, the detail page also surfaces a permanent public URL (e.g. an R2 public dev URL or custom domain) |
| 54 | |
| 55 | ## R2 setup |
| 56 | |
| 57 | 1. In the Cloudflare dashboard, create an R2 API token with read+write access to your bucket(s). |
| 58 | 2. Set in `.env`: |
| 59 | ``` |
| 60 | R2_ACCOUNT_ID=<your account id> |
| 61 | S3_ACCESS_KEY_ID=<token id> |
| 62 | S3_SECRET_ACCESS_KEY=<token secret> |
| 63 | ``` |
| 64 | 3. (Optional) Enable a public dev URL or custom domain on the bucket and add it to `BLOBS_PUBLIC_URLS`. |
| 65 | |
| 66 | ## Generic S3 setup (e.g. MinIO) |
| 67 | |
| 68 | ``` |
| 69 | S3_ENDPOINT=http://localhost:9000 |
| 70 | S3_REGION=us-east-1 |
| 71 | S3_ACCESS_KEY_ID=minioadmin |
| 72 | S3_SECRET_ACCESS_KEY=minioadmin |
| 73 | ``` |
| 74 | |
| 75 | ## Routes |
| 76 | |
| 77 | | Method | Path | Notes | |
| 78 | | --- | --- | --- | |
| 79 | | GET | `/login`, POST `/login` | password form | |
| 80 | | POST | `/logout` | clear session | |
| 81 | | GET | `/buckets` | list buckets | |
| 82 | | GET | `/b/{bucket}/browse/{prefix...}` | folder listing | |
| 83 | | GET | `/b/{bucket}/object/{key...}` | file detail | |
| 84 | | GET | `/b/{bucket}/preview/{key...}` | proxied file stream (for inline images) | |
| 85 | | POST | `/b/{bucket}/upload` | multipart file upload | |
| 86 | | POST | `/b/{bucket}/replace` | overwrite existing key | |
| 87 | | POST | `/b/{bucket}/delete` | delete by key | |
| 88 | | POST | `/b/{bucket}/mkdir` | create zero-byte `prefix/name/` marker | |