img-hosting 完整教學
用一個 Worker、一顆 API_KEY、Cloudflare R2 + D1 + Access 做一個只有自己能寫、任何人能讀的 Imgur-shaped 圖床。 多送你:bash CLI 跟 Claude Code skill 各一份。
1. 專案定位
1.1 一句話介紹
img-hosting = 私人版 Imgur,跑在 Cloudflare 邊緣。對外 API 對齊 Imgur,所以你既有的客戶端、瀏覽器擴充、AI agent 都能直接接;對內全套都你自己的:圖檔在你的 R2 bucket、metadata 在你的 D1 database、寫入只認你的 API_KEY 或 Cloudflare Access 帳號。
1.2 為什麼出現?
作者 林學廷 (htlin222) 是醫師 / 工程師,常需要在文件、簡報、blog 中插圖。市面選項痛點:
- Imgur free tier — 公開、廣告、有政策變動風險
- GitHub README image asset — 不能直接 hot-link(要 fork repo / 上 issue)
- Self-hosted MinIO / 自架 server — VPS 成本 + 維運
- S3 + CloudFront — AWS 帳單心電圖
img-hosting 走 Cloudflare Workers + R2 + D1 全 serverless 路線:
- 零起跳成本:Workers free tier 10 萬 req/day、R2 10GB 存儲、D1 5GB 任你用
- 5 分鐘部署:
make install+wrangler deploy - 無冷啟動:邊緣 V8 isolate
- 可選 OAuth gate:用 Cloudflare Access 把 web UI 包進 Zero Trust,不用 Auth0 / Cognito
1.3 三個介面對應三個使用情境
flowchart TD
A[使用者瀏覽器] -->|"OAuth Access
JWT"| B[Worker /
web UI]
C[CLI / agent] -->|"Bearer
API_KEY"| D[Worker /3/image]
E[任何人] -->|public read| F[Worker /i/:id.png]
B --> G[R2 bucket]
D --> G
F --> G
B --> H[D1 metadata]
D --> H
F --> H
1.4 適合誰?
| 角色 | 用法 |
|---|---|
| 個人寫作者 / 部落客 | 取代 Imgur 公開上傳,建私人圖床貼 markdown |
| AI agent 使用者 | 裝 Claude Code skill,agent 自己上傳產生圖、把 URL 嵌回文件 |
| 小團隊 / 醫療研究 | Cloudflare Access + Google/GitHub OAuth,限制 email domain |
| Cloudflare 學習者 | 完整示範 Workers + R2 + D1 + Access + Image Transform 五合一 |
1.5 數字快照(2026-05-19)
- 📦 1 個 repo / 4 stars / 1 fork(誕生 1 日,極早期)
- 🧪 20 vitest tests passing
- 📜 TypeScript 1,207 LOC(13 個 .ts source files)
- 🔒 MIT 授權,含商用
- 📊 雙語 README(English + 繁體中文)
2. 安裝指南
2.1 前置需求
- Cloudflare 帳號(免費 plan OK)
- Node ≥ 20 + pnpm (
brew install pnpm或corepack enable) curl(CLI 使用,macOS / Linux 內建)- 自有網域 + 託管到 Cloudflare(僅在想開 Access OAuth 時必要;只用 API + workers.dev 子網域則可省)
2.2 7 步安裝流程
flowchart LR
A[1. clone + install] --> B[2. CF resources
create R2 + D1]
B --> C[3. set API_KEY
wrangler secret]
C --> D[4. first deploy]
D --> E[5. install CLI + skill]
E --> F[6. optional
custom domain + Access]
F --> G[7. local dev]
2.3 Step 1:clone + 安裝
1git clone https://github.com/htlin222/img-hosting.git
2cd img-hosting
3make install # 等於 pnpm install
4cp wrangler.toml.example wrangler.toml
5cp .dev.vars.example .dev.vars
6cp .env.example .env
2.4 Step 2:建 R2 + D1
1# R2 bucket
2pnpm wrangler r2 bucket create img-hosting
3
4# D1 database
5pnpm wrangler d1 create img-hosting
6# 印出 database_id - 複製貼到 wrangler.toml 的 database_id = "" 處
7
8# Schema apply
9pnpm wrangler d1 execute IMG_DB --remote --file=./schema.sql
10# 或 make db-remote
2.5 Step 3:設 API_KEY
1# 隨機產 64 位 hex
2openssl rand -hex 32 | wrangler secret put API_KEY
3# 同一把 key 也寫到本地 .dev.vars 與 .env,給 CLI / dev server 用
2.6 Step 4:首次 deploy
1make deploy
2# 拿到 https://img-hosting.<your-subdomain>.workers.dev
3# 測試
4curl https://img-hosting.<your-subdomain>.workers.dev/healthz
5# {"ok":true}
6curl -X POST https://img-hosting.<your-subdomain>.workers.dev/3/image
7# {"error":"unauthorized"} ← 401 預期
2.7 Step 5:裝 CLI + Claude skill
1make install-cli # symlink img-hosting/bin/img-hosting -> ~/bin/img-hosting
2make install-skill # symlink img-hosting/ -> ~/.claude/skills/img-hosting
3
4# 確認
5img-hosting --help
6img-hosting upload some.png
7# https://img-hosting.<sub>.workers.dev/i/Dwql3jX.png
2.8 Step 6(選用):自有網域 + Cloudflare Access
1# 1. 在 wrangler.toml 設 PUBLIC_BASE_URL = "https://i.yourdomain.com"
2# 2. CF dashboard: Workers > Custom Domains 加上 i.yourdomain.com
3# 3. CF dashboard: Zero Trust > Access > Applications 建一個 self-hosted app
4# - hostname: upload-image.yourdomain.com
5# - policy: emails ending @yourdomain.com
6# 4. 拿到 AUD tag,填到 wrangler.toml [vars]
7# ACCESS_TEAM = "your-team"
8# ACCESS_AUD = "<aud-uuid>"
9# 5. make deploy
之後瀏覽器打 upload-image.yourdomain.com 會跳 Google / GitHub / email magic link 登入,web UI 才開放上傳。
2.9 Step 7:本地 dev
1make dev # wrangler dev on :8787
2# 或
3make test # vitest run(20 個 test 全綠才 deploy)
3. 核心架構解析
3.1 三層架構
flowchart TB
subgraph Edge[Cloudflare Edge]
UI[ui.ts
單頁 HTML]
IMG[images.ts
/3/image POST/DELETE]
ACC[account.ts
/3/account/* GET]
SRV[serve.ts
/i/:id.ext GET]
AUTH[auth.ts
Bearer + Access JWT]
RL[ratelimit.ts
RL binding]
SNIFF[sniff.ts
magic byte detect]
end
UI --> AUTH
IMG --> AUTH
ACC --> AUTH
AUTH --> RL
IMG --> SNIFF
SRV --> R2
IMG --> R2[R2 bucket
IMG_BUCKET]
IMG --> D1[D1 database
IMG_DB]
SRV --> D1
ACC --> D1
3.2 13 個 source file
| File | LOC | 職責 |
|---|---|---|
index.ts | 49 | Hono app 組裝,/healthz + /whoami + route mount |
auth.ts | 64 | Bearer + Access dual auth;timing-safe equal |
access.ts | 104 | Cloudflare Access JWT verify (JWKS) |
images.ts | 205 | POST/DELETE /3/image 與 raw / multipart / base64 解析 |
account.ts | 77 | GET /3/account/me/images(list / count / by-id) |
serve.ts | 88 | GET /i/:id.ext 公開讀 + image transform |
ui.ts | 398 | 內嵌 HTML / CSS / JS(drag-drop + dark mode + paste from clipboard) |
sniff.ts | 97 | Magic byte 判 mime(PNG/JPEG/GIF/WebP),擋偽造副檔名 |
ratelimit.ts | 25 | per-key 或 per-IP 速率限制 |
resize.ts | 48 | 解析 ?w= ?h= ?fit= ?q= → Cloudflare Image Transformations |
ids.ts | 21 | 短 ID 產生 + delete-hash + SHA-256 |
response.ts | 16 | ok / fail helpers(對齊 Imgur shape) |
env.ts | 15 | Env 型別宣告 |
3.3 三條 auth 路徑
sequenceDiagram
participant U as User
participant W as Worker
rect rgba(200,255,200,0.3)
Note over U,W: 路徑 A: Web UI (preferred)
U->>W: GET upload-image.example.com
W->>U: Redirect to Access OAuth
U->>W: Returns with Cf-Access-Jwt-Assertion
W->>W: verifyAccessJwt(jwt, team, aud)
W->>U: 200 + identity (email/sub)
end
rect rgba(200,200,255,0.3)
Note over U,W: 路徑 B: CLI / agent
U->>W: POST /3/image\nAuthorization: Bearer
W->>W: timingSafeEqual(token, expected)
W->>U: 200 + Imgur-shape response
end
rect rgba(255,220,200,0.3)
Note over U,W: 路徑 C: Public read
U->>W: GET /i/Dwql3jX.png
W->>W: D1 lookup + R2 get
W->>U: image bytes + cache headers
end
3.4 D1 schema 一覽
1CREATE TABLE images (
2 id TEXT PRIMARY KEY, -- 短 ID, Imgur-shape (7 字元)
3 deletehash TEXT UNIQUE NOT NULL, -- 刪除用 token, 不外露
4 owner TEXT NOT NULL, -- 'me' 或 Access identity sub
5 filename, title, description,
6 mime TEXT NOT NULL, -- e.g. 'image/png'
7 ext TEXT NOT NULL,
8 size, width, height, -- bytes 與像素
9 sha256 TEXT NOT NULL, -- dedup 用
10 created_at INTEGER NOT NULL,
11 deleted_at INTEGER -- soft delete
12);
13CREATE INDEX idx_images_owner_created
14 ON images(owner, created_at DESC)
15 WHERE deleted_at IS NULL;
16CREATE INDEX idx_images_sha
17 ON images(sha256);
SHA-256 index 允許未來 dedup(同檔案不重複佔 R2 空間,邏輯目前未實作但 schema 預留)。
4. Helper Scripts 詳細用法
4.1 Makefile 入口
1make help # 列所有 target
2make install # pnpm install
3make typecheck # tsc --noEmit
4make test # vitest run(20 個 test)
5make dev # wrangler dev :8787
6make build # typecheck(wrangler 自動 bundle)
7make deploy # wrangler deploy
8make db-local # 套 schema 到本地 d1
9make db-remote # 套 schema 到 production d1
10make install-cli # ln -s img-hosting/bin/img-hosting ~/bin/
11make install-skill # ln -s img-hosting/ ~/.claude/skills/
4.2 REST API(Imgur-shape)
1# 上傳 (raw)
2curl -X POST https://your-worker/3/image \
3 -H "Authorization: Bearer $API_KEY" \
4 -H "Content-Type: image/png" \
5 --data-binary @photo.png
6
7# 上傳 (multipart)
8curl -X POST https://your-worker/3/image \
9 -H "Authorization: Bearer $API_KEY" \
10 -F image=@photo.png -F title="awesome"
11
12# 上傳 (JSON + base64)
13curl -X POST https://your-worker/3/image \
14 -H "Authorization: Bearer $API_KEY" \
15 -H "Content-Type: application/json" \
16 -d '{"image":"iVBORw...","name":"photo.png"}'
17
18# 列出
19curl https://your-worker/3/account/me/images \
20 -H "Authorization: Bearer $API_KEY"
21
22# 計數
23curl https://your-worker/3/account/me/images/count \
24 -H "Authorization: Bearer $API_KEY"
25
26# 取單一
27curl https://your-worker/3/image/Dwql3jX \
28 -H "Authorization: Bearer $API_KEY"
29
30# 刪除(用 deletehash 不是 id)
31curl -X DELETE https://your-worker/3/image/<deletehash> \
32 -H "Authorization: Bearer $API_KEY"
33
34# 公開讀
35curl -O https://your-worker/i/Dwql3jX.png
36curl -O 'https://your-worker/i/Dwql3jX.png?w=800&fit=cover&q=80' # 動態 resize
4.3 CLI 用法
1img-hosting upload photo.png # 印 URL
2img-hosting upload photo.png --json # 印完整 JSON
3img-hosting md photo.png #  markdown
4img-hosting md photo.png --alt "demo screenshot"
5img-hosting html photo.png # <img src="url" />
6img-hosting list # 我的上傳
7img-hosting count
8img-hosting get Dwql3jX
9img-hosting delete <deletehash>
10img-hosting whoami
11img-hosting upload - < photo.png # stdin 路徑
CLI 會依序找:環境變數 → $IMG_HOSTING_ENV → skill 同目錄 .env → ~/.config/img-hosting/.env → cwd/.env。
4.4 Claude Code skill
把 img-hosting/ symlink 到 ~/.claude/skills/img-hosting/ 後,Claude Code 看到使用者要「上傳 / 分享 / 嵌入圖片」就觸發 skill。SKILL.md 規範 agent:
- 必跑 CLI(
img-hosting upload),不要自己組 HTTP - 不要 echo bearer token
- 沒給路徑時先問使用者
5. 應用場景
5.1 場景 A:個人 markdown 寫作
1img-hosting md ~/Pictures/diagram.png --alt "system architecture"
2# 
直接複製貼 markdown 文件、Obsidian、Notion blog。
5.2 場景 B:AI agent 自動截圖 + 嵌入
1User: 幫我抓 PaperZilla 首頁截圖,產一份 markdown report
2Claude:
31. opencli-browser → screenshot.png
42. img-hosting upload screenshot.png → URL
53. 把  嵌進 report.md
5.3 場景 C:研究室 / 小團隊 OAuth 限制
11. Cloudflare Access app rule:
2 Include: emails ending @apotek.tw
32. 同事打開 upload-image.apotek.tw → Google 登入
43. 上傳 → web UI 顯示縮圖
54. R2 圖檔依 owner=<sub from JWT> 區分
無需 user 表 / password 表,全靠 IdP(Google Workspace / Okta / GitHub)。
5.4 場景 D:Markdown 文件圖檔遷移
1# 把舊文件裡的 imgur URL 全部抓下重傳到自己圖床
2for url in $(grep -oE 'https://i.imgur.com/[a-zA-Z0-9]+\.(png|jpg|gif)' notes/*.md); do
3 tmp=$(mktemp /tmp/XXXX.png)
4 curl -sL "$url" -o "$tmp"
5 new_url=$(img-hosting upload "$tmp")
6 sed -i "s|$url|$new_url|g" notes/*.md
7 rm "$tmp"
8done
6. 資安掃描報告
掃描方法:
grep -rE "eval\(|exec\(|new Function|child_process|sk-[A-Za-z0-9]{20}|password=" src/+ 檢視 .gitignore + .dev.vars.example + auth 模組程式碼。
6.1 結論:🟢 LOW RISK
對一個 1 日大的小專案而言,作者在 secrets / auth / 上傳防護 都做到該做的事。可放心 self-host。
6.2 細項
| 風險點 | 等級 | 說明 |
|---|---|---|
| API key timing attack | 🟢 LOW | auth.ts 用 timing-safe equal(自實作的 constant-time loop),不會 leak token 長度差異 |
| Hard-coded secret | 🟢 LOW | grep sk-...|password= 無發現;.dev.vars.example 與 .env.example 都明標 “replace-with…” |
.gitignore 完整性 | 🟢 LOW | 排除 .dev.vars / .env / wrangler.toml(含真實 D1 database_id),只 commit *.example |
| 任意指令執行 | 🟢 LOW | 唯一 exec( 出現在 auth.ts 的 regex match (/^Bearer\s+(.+)$/i.exec());non-issue |
| 上傳大小 | 🟢 LOW | MAX_BYTES = 20 * 1024 * 1024(20 MiB cap),對齊 Imgur free tier |
| MIME 偽造 | 🟢 LOW | sniff.ts 用 magic bytes 判型,不信任 Content-Type header — 擋 polyglot file |
| Rate limiting | 🟢 LOW | Cloudflare RL binding:60 req / 60s per key 或 per IP;測試環境 fail open(已標註) |
| Path traversal | 🟢 LOW | 短 ID 路徑(7 字元 alphanum),不接 user 傳檔名 |
| Access JWT verify | 🟢 LOW | access.ts 走 JWKS fetch 真正驗 RS256 簽名(不是只看 header) |
| 公開讀無 auth | ⚠️ BY-DESIGN | /i/:id.ext 任何人能讀 — 這是「Imgur-shape」的核心特性;想擋要自己加 Access on /i/* |
| D1 database_id 外洩 | 🟢 LOW | wrangler.toml 在 .gitignore;只 commit wrangler.toml.example(空白 database_id = "") |
| SHA-256 / deletehash | 🟢 LOW | ids.ts 用 crypto API 產生;deletehash 走獨立欄位,不從 id 推導 |
| CORS | 🟡 MEDIUM | README 與 source 未明確設 CORS allowlist — 若 web UI / API 跨域呼叫,需注意(Cloudflare 預設不限) |
| Logging 機敏 | 🟢 LOW | Access JWT 驗失敗只記 message,不記原始 token |
6.3 建議使用者注意
- ✅ 小團隊內部用:可直上 production,記得
wrangler secret put API_KEY用強隨機(openssl rand -hex 32) - ⚠️ 若處理敏感影像:考慮把
/i/:id.ext也加上 Access policy(修src/serve.ts),預設是公開讀 - ⚠️ 若給多個使用者:目前
OWNER = 'me'hard-code 在images.ts;要做多租戶要改成從 Access identity 派生 - ✅ CI/CD:跑
make test(20 個 test)作為部署 gate
7. FAQ
Q1:為什麼用 Cloudflare 而不是 AWS S3 + Lambda? A:成本 + 冷啟動。CF Workers free tier 10 萬 req/day、無冷啟動;R2 沒有 egress 費用,S3 出流量會被收錢。對個人 / 小團隊圖床完美。
Q2:圖會被搜尋引擎抓到嗎?
A:/i/:id.ext URL 是隨機短 ID(7 字元 base62 ≈ 3.5 兆組合)。沒有 listing,所以不會被爬到,但只要有人拿到 URL 就能看(unlisted 而非 private)。要全私請加 Access。
Q3:可以開分享連結但設過期嗎?
A:目前不支援。可以自己加 D1 欄位 expires_at 然後在 serve.ts 判斷。
Q4:圖檔上限為什麼是 20 MiB?
A:對齊 Imgur free tier,也是 Cloudflare Workers single-request body 推薦上限。要更大可改 images.ts 的 MAX_BYTES,但要注意 Worker 50 ms CPU time 限制(free plan)。
Q5:CLI 如何在多台機器分享 config?
A:把 .env 放 ~/.config/img-hosting/.env,CLI 會自動找。或設 IMG_HOSTING_ENV=/path/to/custom。
Q6:跟 Imgur 真的 100% 相容嗎?
A:API shape 對齊(fields 名稱、回 JSON 結構),但 auth header 是 Bearer(Imgur 是 Client-ID),rate limit 邏輯也不同。能用大多 Imgur 客戶端,但需要改 auth scheme。
Q7:D1 / R2 滿了會怎樣?
A:CF 會丟錯,Worker 回 500。建議定期跑 img-hosting list 看占用,或加個 cron 刪 N 天前的軟刪檔(schema 已有 deleted_at)。
8. 進階技巧
8.1 動態 resize(省頻寬)
1# 原始
2https://i.yourdomain.com/i/Dwql3jX.png
3# 800px 寬,q=80 jpeg
4https://i.yourdomain.com/i/Dwql3jX.png?w=800&q=80
5# 縮圖 thumbnail
6https://i.yourdomain.com/i/Dwql3jX.png?w=200&h=200&fit=cover
需在 CF dashboard 開「Transform images」(Speed > Optimization > Image Optimization)。
8.2 自訂 PUBLIC_BASE_URL
1# wrangler.toml
2[vars]
3PUBLIC_BASE_URL = "https://i.yourdomain.com"
設了之後 API 回傳的 link 欄位會用這個 base,方便 markdown 直接複製。
8.3 OWNER 從 Access JWT 派生(多租戶)
images.ts line 11 const OWNER = 'me'; 改成從 c.get('identity') 拿 sub / email,每個使用者只看自己的圖。要記得改 account.ts 的 query 條件。
8.4 整合進你的 Obsidian
Obsidian Image Auto Upload Plugin → custom uploader:
1{
2 url: "https://i.yourdomain.com/3/image",
3 method: "POST",
4 headers: { "Authorization": "Bearer YOUR_API_KEY" }
5}
8.5 用 Wrangler tail debug
1pnpm wrangler tail --format=pretty
2# 即時看 console.log / 錯誤 / 請求 metadata
9. 整合進其他工作流
9.1 與本專案的關係
flowchart LR
A[docling / paper-search
產生圖檔] --> B[img-hosting upload]
B --> C[hosted URL]
C --> D[quarkdown / kami
嵌入 HTML/PDF]
C --> E[ai-save
知識 md]
把 img-hosting 當「圖檔 URL 來源」整進現有 markdown workflow:docling 抽圖 → upload → 嵌回 .md → quarkdown 排版 → 出 HTML。
9.2 與 Claude Code skill 生態的位置
| skill | 職責 |
|---|---|
| agent-skills (tech-leads-club) | 跨 14 種 agent skill registry |
| academic-research-skills (Imbad0202) | 學術 deep stack plugin |
| img-hosting (htlin222) | 單一功能 skill — agent 上傳本地圖 → URL |
kami / quarkdown (本專案) | 文件排版層,img-hosting 是上游圖檔來源 |
img-hosting 是 single-purpose skill 的好範例:SKILL.md < 100 行,trigger 條件明確(PNG/JPEG/GIF/WebP 上傳)。
9.3 替代方案比較
| 方案 | 成本 | 隱私 | 易用性 |
|---|---|---|---|
| img-hosting (本專案) | $0–5/mo (CF free) | 自有 R2 + Access | ★★★★ (img-hosting upload) |
| Imgur free | $0 + 廣告 | 公開 | ★★★ (web only) |
| AWS S3 + CF | $5–20/mo | 自有 bucket | ★★ (要自寫 CLI) |
| GitHub Gist + asset | $0 | repo 公開 | ★ (要 fork) |
| MinIO self-hosted | VPS ~$5/mo | 完全自有 | ★ (要維運) |
10. 重點摘要 Checklist
- Cloudflare 全家桶:Workers + R2 + D1 + Access + Image Transforms 五合一
- 三介面:web UI(OAuth)+ REST API(Bearer)+ CLI / Claude skill
- Imgur-shape API:對齊
/3/image//3/account/*//i/:id.ext - secrets 全分離:
.dev.vars/wrangler secret/.env都在 .gitignore - timing-safe auth + magic-byte sniff + rate limit 三道防護
- 20 vitest test passing,CI/CD gate 完整
- MIT 授權,含商用
- 零起跳成本(CF free tier 個人用足夠)
- 資安 🟢 LOW(小團隊內部可直上 production)
- 5–10 分鐘部署(
make install+wrangler deploy)
11. 進一步閱讀
- GitHub repo:https://github.com/htlin222/img-hosting
- 繁體中文 README:README.zh-TW.md
- 作者個人首頁:Lin Hsieh-Ting
- Cloudflare Workers 文件:https://developers.cloudflare.com/workers/
- Cloudflare R2:https://developers.cloudflare.com/r2/
- Cloudflare D1:https://developers.cloudflare.com/d1/
- Cloudflare Access:https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/
- Hono framework:https://hono.dev/
- Imgur API 規格:https://apidocs.imgur.com/
Captured:2026-05-19 by gh-tutorial-qd workflow(depth=full / lang=zh-tw / qd_preset=report / security=on)。
Comments