视频加载失败

GH-ImgBed —— 用 GitHub 存图 + Cloudflare 边缘分发,零成本自建图床

2786 字
14 分钟
GH-ImgBed —— 用 GitHub 存图 + Cloudflare 边缘分发,零成本自建图床
  1. 1GH-ImgBed —— 用 GitHub 存图 + Cloudflare 边缘分发,零成本自建图床本文
  2. 2Cloudflare 优选 IP 教程
  3. 3蓝奏云直链解析工具
  4. 4用 Cloudflare Workers + R2 搭建免费的 Serverless 云存储系统
GH-ImgBed —— 用 GitHub 存图 + Cloudflare 边缘分发,零成本自建图床

一个零服务器成本的免费图床:图片存进你自己的 GitHub 仓库,通过 Cloudflare 边缘节点缓存分发。

前端是纯静态页面,后端只有两个 Cloudflare Pages Functions。没有数据库、没有自建服务器、没有构建步骤。

为什么不用现成图床

第三方图床要么有「X 个月未访问自动删除」,要么哪天就关停了。图片存在自己仓库里,数据在自己手上,链接也不会因为服务跑路而失效。


🏗️ 整体架构#

浏览器从头到尾只跟 Cloudflare 边缘节点通信,回源由服务端完成。

POST 上传

GET /v2/xxx.png

Contents API PUT

命中缓存

未命中回源

👤 浏览器

⚙️ upload.js

Pages Function

⚙️ v2/[[vkey]].js

访问代理

🗄️ GitHub 仓库

images 分支

⚡ Cloudflare

边缘缓存

GitHub Raw

POST 上传

GET /v2/xxx.png

Contents API PUT

命中缓存

未命中回源

👤 浏览器

⚙️ upload.js

Pages Function

⚙️ v2/[[vkey]].js

访问代理

🗄️ GitHub 仓库

images 分支

⚡ Cloudflare

边缘缓存

GitHub Raw

关键点:访客所在网络能否直连 GitHub,并不影响访问 —— 回源这一步发生在服务端。


🔄 上传与访问时序#

边缘缓存GitHubCloudflare Pages Functions浏览器边缘缓存GitHubCloudflare Pages Functions浏览器上传访问alt[命中][未命中]POST /upload(multipart)校验大小 ≤ 20MB,转 base64PUT /contents/<path>(branch=images)返回 content.sha{ link, raw, path, sha }GET /v2/261010-k7f2m.pngcache.match(key)缓存副本GET raw.githubusercontent.com图片字节waitUntil(cache.put) 异步写缓存图片 + Cache-Control: immutable
边缘缓存GitHubCloudflare Pages Functions浏览器边缘缓存GitHubCloudflare Pages Functions浏览器上传访问alt[命中][未命中]POST /upload(multipart)校验大小 ≤ 20MB,转 base64PUT /contents/<path>(branch=images)返回 content.sha{ link, raw, path, sha }GET /v2/261010-k7f2m.pngcache.match(key)缓存副本GET raw.githubusercontent.com图片字节waitUntil(cache.put) 异步写缓存图片 + Cache-Control: immutable

文件名格式是 YYMMDD-5位随机.扩展名,例如 261010-k7f2m.png。5 位随机空间约 6000 万,撞名概率极低,代码里仍保留了最多 6 次的换名重试。


🚀 部署流程#

① 建 images 孤儿分支

git switch --orphan images

② 生成 Fine-grained Token

Contents: Read and write

③ Cloudflare Pages 连接仓库

④ Framework preset: None

Build command 留空

输出目录 /

⑤ 配环境变量

GITHUB_TOKEN / OWNER / REPO

⑥ Branch Preview 设为 None

⑦ 重新部署

⑧ 可选:开启人机验证

⑨ 可选:绑定自定义域名

① 建 images 孤儿分支

git switch --orphan images

② 生成 Fine-grained Token

Contents: Read and write

③ Cloudflare Pages 连接仓库

④ Framework preset: None

Build command 留空

输出目录 /

⑤ 配环境变量

GITHUB_TOKEN / OWNER / REPO

⑥ Branch Preview 设为 None

⑦ 重新部署

⑧ 可选:开启人机验证

⑨ 可选:绑定自定义域名


✨ 功能特性#

GH-ImgBed

💾 存储

存在自己的 GitHub 仓库

不会过期自动删除

返回 sha 可精确删除

仓库可私有

🔐 安全

可选人机验证

一次验证放行整批

串行上传

失败原因可见

⚡ 分发

Cache API 确定性缓存

首次访问即生效

链接与后端解耦

支持自定义域名

🖱️ 使用

点击拖拽粘贴上传

支持多选

复制链接或 Markdown

生成二维码

localStorage 历史

🎨 体验

深浅主题跟随系统

首屏无闪烁

小屏安全区适配

44px 触控目标

GH-ImgBed

💾 存储

存在自己的 GitHub 仓库

不会过期自动删除

返回 sha 可精确删除

仓库可私有

🔐 安全

可选人机验证

一次验证放行整批

串行上传

失败原因可见

⚡ 分发

Cache API 确定性缓存

首次访问即生效

链接与后端解耦

支持自定义域名

🖱️ 使用

点击拖拽粘贴上传

支持多选

复制链接或 Markdown

生成二维码

localStorage 历史

🎨 体验

深浅主题跟随系统

首屏无闪烁

小屏安全区适配

44px 触控目标


⚙️ 环境变量#

变量名说明必填
GITHUB_TOKENFine-grained 令牌,仅对存图仓库开放 Contents 读写✅
GITHUB_OWNER存图仓库的所有者用户名✅
GITHUB_REPO存图仓库名✅
GITHUB_PATH仓库内子目录,留空则存根目录(链接更短)可选
IMG_CDN设为 jsdelivr 可改走 jsDelivr 回源可选
TURNSTILE_SECRETTurnstile 的 Secret Key(不是 Site Key)可选
TURNSTILE_HOSTNAMES实际访问用的完整主机名,逗号分隔启用验证时必填
两个容易踩的坑

1. 分支名固定为 images,不能自定义。

访问代理不校验路径,如果图片与代码同分支,代码文件可能被构造路径读到。固定孤儿分支是安全隔离的前提。

2. 务必把 Branch Preview 设为 None。

否则每次上传图片推送到 images 分支都会触发一次构建,很快耗尽每月 500 次的免费额度。

改完环境变量需要重新部署才生效。

启用验证前,先换掉前端的 Site Key

assets/app.js 顶部的 TURNSTILE_SITEKEY 是本项目作者的,直接部署不会生效 —— 该 widget 的 domains 白名单里没有你的域名,验证组件会渲染失败。

const TURNSTILE_SITEKEY = "0x4AAAAAAFUKaXfCncBNv71l"; // ← 换成你自己的

它与 TURNSTILE_SECRET 必须来自同一个 widget,跨 widget 混用同样会报 invalid-input-secret。


🔐 人机验证(可选)#

防止脚本批量刷上传接口,基于 Cloudflare Turnstile。只保护上传,不保护 /v2/ —— 那是出图用的,加了验证图就无法外链了。

为什么需要两处配置#

配置位置作用
widget 的 domainsTurnstile 控制台哪些域名能签发 token
TURNSTILE_HOSTNAMES环境变量只接受为你签发的 token

两处都要配。只配控制台是不够的:任何人都能自己建一个 widget(域名填他自己的),通过挑战后把 token 提交给你的接口,Cloudflare 会如实回答 success=true —— 那确实是真的 token,只是不是为你签发的。所以后端必须比对 hostname。

一次验证放行整批#

Turnstile token 是一次性的,无法在多张图之间复用。所以由后端签发凭证解决:

第 1 张 ──带 token──> siteverify ──通过──> 签发 HMAC 凭证(10 分钟)
第 2..N 张 ──带凭证──> 校验签名与有效期 ──通过──> 写入 GitHub

凭证是用 TURNSTILE_SECRET 做的 HMAC-SHA256 签名,无法伪造 —— 攻击者仍须先通过一次真实验证才能拿到。批次结束后前端重置组件,下次批量需重新验证一次。

Turnstile 验证服务上传函数浏览器Turnstile 验证服务上传函数浏览器POST 第 1 张(带 token)siteverify(token + secret)success + hostname比对 hostname 是否在白名单写入成功 + 返回凭证POST 第 2..N 张(带凭证)校验 HMAC 签名与有效期写入成功(无需再验证)
Turnstile 验证服务上传函数浏览器Turnstile 验证服务上传函数浏览器POST 第 1 张(带 token)siteverify(token + secret)success + hostname比对 hostname 是否在白名单写入成功 + 返回凭证POST 第 2..N 张(带凭证)校验 HMAC 签名与有效期写入成功(无需再验证)

未配置 TURNSTILE_SECRET 时行为不变(完全放行),可以随时启用或停用 —— 万一验证出问题,删掉这个变量就能立刻恢复。

Secret Key 和 Site Key 长得几乎一样

两串都以 0x4AAAAAAA 开头,光看开头分辨不出来,是本项目最容易填错的地方。Site Key 已写在 index.html 里(公开无妨),Secret Key 只能进环境变量。填错会报 invalid-input-secret,若已混淆,直接在控制台重新生成一个最省事。

上传报「人机验证未通过」怎么排查#

错误提示里会带上具体原因,对照这张表定位:

提示原因处理
invalid-input-secretSecret Key 填错,或误填成了 Site Key重新复制该 widget 的 Secret Key
主机名不匹配:本站接受 X,实际为 Y访问域名与 TURNSTILE_HOSTNAMES 不一致按提示里的 Y 填进去
timeout-or-duplicatetoken 已被用过,或超过 300 秒刷新页面重新验证
invalid-input-responsetoken 无效或已过期同上
missing-input-response前端没把 token 传上来检查 widget 是否渲染成功
未收到验证凭据…收到的字段:file请求里完全没有验证字段刷新页面;若持续出现,检查脚本是否被拦截
验证组件未加载,请刷新页面重试challenges.cloudflare.com 脚本没加载成功检查网络能否访问该域名

实际踩过的坑,按概率排序:

  1. Secret Key 误填成 Site Key —— 两者长得几乎一样,最容易犯。
  2. TURNSTILE_HOSTNAMES 填了根域名(5al.top)而实际访问是子域名(img.5al.top)。
  3. 改完环境变量没有重新部署,函数里读到的还是旧值。
  4. 控制台 widget 的 domains 里没加实际访问用的域名。

📁 目录结构#

index.html 页面结构
assets/app.css 样式:设计令牌、深浅主题、响应式
assets/app.js 逻辑:上传、列表、复制、二维码、主题
└─ 含 TURNSTILE_SITEKEY,启用验证前须换成自己的
assets/fonts.css Material Icons 字体声明(本地)
assets/fonts/*.woff2 Material Icons 字体文件
functions/upload.js 上传:写入 GitHub
functions/v2/[[vkey]].js 访问:回源 + 边缘缓存
scripts/verify_upload.py 部署前自检脚本

图标字体是本地自托管的,不引用 fonts.googleapis.com(该域名在中国大陆不可访问)。字体名须与 mdui 期望的完全一致,且需开启 font-feature-settings: 'liga',否则连字不生效、图标会显示为文字。

assets/app.js 里的 CONCURRENCY 控制同时上传的数量,默认 1(串行)。多选图片时若并发过高,Cloudflare Functions 需同时做多次 base64 编码,可能超出免费额度的 CPU / 内存限制而被中断。除非确认额度充足,否则不建议调大。


✅ 部署前自检#

Terminal window
export GITHUB_TOKEN='github_pat_xxx'
export GITHUB_OWNER='你的用户名'
export GITHUB_REPO='你的仓库'
python3 scripts/verify_upload.py

脚本会依次检查令牌有效性、仓库可访问性、写入权限、链接可访问性,并自动清理测试文件。


🔧 日常维护#

删除单张图片#

每次上传的响应里带有 sha,浏览器已存在 localStorage。用它可精确删除:

Terminal window
curl -X DELETE \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/<owner>/<repo>/contents/<path> \
-d '{"message":"delete","sha":"<上传时返回的 sha>","branch":"images"}'

也可以在 GitHub 网页上直接删除文件。

删完不会立刻消失

边缘缓存最长保留 1 年。删除后如需立即生效,到 Cloudflare 后台手动清除缓存(Caching → Configuration → Purge Everything)。

更换令牌#

令牌到期后重新生成一个,更新 Pages 的 GITHUB_TOKEN 环境变量并重新部署。

仓库满了#

GitHub 建议单仓库控制在 1GB 以内。超了就新建仓库、建 images 孤儿分支、改 GITHUB_REPO 环境变量、重新部署。旧仓库的链接会失效,如需保留可保留旧仓库不动、只切换新图。


❓ 常见问题#

上传返回 403 — 令牌缺 Contents: Read and write,或 Repository access 没包含该仓库。

上传返回 422 — 通常是同名文件冲突,代码会自动换名重试 6 次;若仍失败,检查分支是否存在。

多选上传时部分失败,提示「网络错误」 — 同时上传太多图片会让 Functions 并发做多次 base64 编码,可能超出免费额度的 CPU / 内存限制而被中断。默认已串行(CONCURRENCY = 1),若仍失败就一次少选几张。

图片上传了但打不开 — 确认 images 分支存在且文件确实写入。访问代理回源的是 raw.githubusercontent.com/<owner>/<repo>/images/<path>。

刷新后上传历史没了 — 历史存在浏览器 localStorage,按域名隔离。换了域名后旧记录读不到,这是浏览器行为,不是故障。

图标显示为 cloud_upload 这样的英文 — Material Icons 字体没加载。确认 assets/fonts/*.woff2 能访问,且字体名与 mdui 期望的一致。


⚠️ 已知限制#

限制说明
单文件 20MB代码中已做校验
访问路径未做白名单依赖 images 孤儿分支隔离,不要往该分支放非图片文件
GitHub API 限额5000 次/小时,个人图床足够
Cloudflare 免费额度请求 10 万/天,构建 500 次/月
仓库体积建议控制在 1GB 以内
国内访问Cloudflare 在中国大陆无边缘节点,访客可能被路由到境外。可设 IMG_CDN=jsdelivr 改走 jsDelivr 回源
外部依赖启用人机验证后会加载 challenges.cloudflare.com 脚本(验证的必要条件)。其余资源全部本地

🛠️ 技术栈#

部分说明
页面原生 HTML + JavaScript,无框架、无构建
组件mdui 2(Web Components,CDN 引入)
图标Material Icons 字体(本地自托管)
二维码qrcode(CDN,按需动态加载)
人机验证Cloudflare Turnstile(可选,仅保护上传接口)
后端Cloudflare Pages Functions

📄 许可#

MIT License — 可自由使用、修改和分发


xingmihai
/
gh-imgbed
GH图床 —— 基于 GitHub 存储与 Cloudflare 边缘缓存的免费图床
—
—
MIT
JavaScript

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
GH-ImgBed —— 用 GitHub 存图 + Cloudflare 边缘分发,零成本自建图床
https://www.5al.top/posts/202610100235/
作者
星觅海
发布于
2026-10-09
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
星觅海
记录生活与技术
公告
欢迎来到星觅海的小站,这里记录技术与日常。
分类
标签
最新动态
站点统计
文章
8
分类
7
标签
11
总字数
7,827
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录