# CattoPic Self-hosted image host: upload, tags, WebP/AVIF variants, expiry, and a public random-image API. **1.0.0** runs the admin UI and the API on **one Cloudflare Worker**. Open the Worker hostname (`workers.dev` or a Custom Domain) and the UI is there. There is no Vercel app and no second frontend deploy. Image **files** live in R2 and are served from `R2_PUBLIC_URL` (object CDN and `/cdn-cgi/image`). That is storage, not a second app. [中文](./docs/README_CN.md) · [Changelog](./CHANGELOG.md) · [API](./docs/API_EN.md) · [Deploy](./DEPLOYMENT.md) ## What 1.0.0 changes | Before | After | |--------|--------| | Next.js on Vercel + Hono Worker | One Worker: Vite React SPA (Static Assets) + Hono `/api/*` | | UI called `NEXT_PUBLIC_API_URL` | Same-origin `fetch("/api/...")` | | Docs described `GET /r2/{path}` | Image bytes from `R2_PUBLIC_URL` only | Public HTTP routes are unchanged. `GET /api/random` still returns **302** to an image URL. ## Architecture ```mermaid flowchart TB subgraph Client Browser[Browser] APIClient[API client] end subgraph Worker["Cloudflare Worker"] Assets["Static Assets
/ and /manage"] Hono["Hono /api/*"] end subgraph CF["Cloudflare"] R2[("R2")] D1[("D1")] KV[("KV")] Queue[Queues] Images[Images binding] Cron[Cron] end Browser --> Assets Browser --> Hono APIClient --> Hono Hono --> R2 Hono --> D1 Hono --> KV Hono --> Queue Hono --> Images Cron --> Hono Browser -->|"image bytes"| R2 ``` | Path | Role | |------|------| | `https:///` | Upload UI | | `https:///manage` | Gallery / tags | | `https:///api/*` | Hono API (this is the only path that invokes the Worker script) | Static HTML/JS/CSS does not invoke the Worker. `assets.run_worker_first` is `["/api/*"]` only. ### Storage keys | Variant | Key | |---------|-----| | Original | `original/{orientation}/{id}.{ext}` | | WebP | `{orientation}/webp/{id}.webp` | | AVIF | `{orientation}/avif/{id}.avif` | GIF is original-only. JPEG/PNG larger than 20MB skip the Images binding; variant URLs may use `/cdn-cgi/image`. Advertised max upload is 70MB. ## Features - JPEG, PNG, GIF, WebP, AVIF upload - Stored WebP/AVIF for files within the Images `.input()` limit (20MB) - Tags, batch tag edits, expiry - Public `GET /api/random` with `orientation`, `tags`, `exclude`, `format` - ZIP upload in the browser (extract, then concurrent `POST /api/upload/single`) - Dark mode management UI ## Bindings Configured in `wrangler.jsonc`: | Binding | Resource | |---------|----------| | `R2_BUCKET` | R2 bucket `cattopic` | | `DB` | D1 `CattoPic-D1` | | `CACHE_KV` | KV `cattopic-kv` | | `DELETE_QUEUE` | Queue `cattopic-delete-queue` | | `IMAGES` | Cloudflare Images | | `ASSETS` | Static Assets (the UI) | `USE_QUEUE` is `true`: R2 deletes go through the queue. Forks can set it to `false` for synchronous deletes. ## Commands Requires Node.js 24+ and [pnpm](https://pnpm.io/). ```bash git clone https://github.com/Yuri-NagaSaki/CattoPic.git cd CattoPic pnpm install pnpm dev # http://localhost:5173 (UI + API) pnpm build pnpm run deploy # vite build && wrangler deploy pnpm typecheck pnpm test ``` ## Deploy ```bash pnpm wrangler login pnpm wrangler d1 migrations apply CattoPic-D1 --remote pnpm wrangler d1 execute CattoPic-D1 --remote --command " INSERT OR IGNORE INTO api_keys (key, created_at) VALUES ('your-secure-api-key', datetime('now')); " pnpm run deploy ``` Then open `https://cattopic-worker..workers.dev` (or your Custom Domain). Paste the API key in the UI. ```bash curl -I "https://cattopic-worker..workers.dev/api/random" ``` Enable public access on the R2 bucket and set `vars.R2_PUBLIC_URL` in `wrangler.jsonc`. Forks: copy `wrangler.example.jsonc` to `wrangler.jsonc` and fill D1/KV ids. GitHub Actions uses `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`. Config is the committed `wrangler.jsonc`. Full steps: [DEPLOYMENT.md](./DEPLOYMENT.md). ## API (short) Public: `GET /api/random` (302 to an image URL). Query: `orientation`, `tags`, `exclude`, `format`. Follow redirects with `curl -L` if you want the file. Everything else needs `Authorization: Bearer `. | Method | Path | |--------|------| | POST | `/api/upload/single` | | GET/PUT/DELETE | `/api/images`, `/api/images/:id` | | GET/POST/PUT/DELETE | `/api/tags`, `/api/tags/:name` | | POST | `/api/tags/batch` | Full reference: [docs/API_EN.md](./docs/API_EN.md) / [docs/API.md](./docs/API.md). ## License [GPL-3.0](./LICENSE)