一个面向个人/小团队的 Cloudflare R2 图床,支持登录保护、预签名直传、分组排序、批量管理。
📡 开放 API 接口 · 申领 API Key · 接口文档 · Java 示例
ZensImage 是一个基于 Next.js 15 的现代图床应用,重点是“好看 + 好用 + 可低成本长期部署”。
核心特性:
- 登录后才能上传与管理(cookie session)
- 预签名直传 R2(浏览器直传,不走服务器中转)
- 图片管理页支持搜索、标签筛选、无限滚动、批量删除
- 图片链接一键复制(URL / Markdown / HTML / BBCode)
- 按时间分组(今天/本周/更早)并支持分组独立拖拽排序
- 排序持久化到 Cloudflare KV(可回退 R2)
- 深浅主题 + 现代化仪表盘风格
- 开放 API 接口:支持 API Key 鉴权,Java / Python / Go 等任意后端直接上传图片,一键申领密钥,适合博客、CMS 等场景
- Next.js 15(App Router + Route Handlers)
- TypeScript(strict)
- Tailwind CSS v4
- shadcn/ui 风格组件
- TanStack Query v5
- zod + react-hook-form
- framer-motion
- sonner
- next-themes
- Cloudflare R2(S3 兼容)
- Cloudflare KV(排序持久化,推荐)
npm install复制 .env.example 为 .env.local,填写你的值:
cp .env.example .env.localnpm run dev打开 http://localhost:3000。
可以直接配置在 Cloudflare Pages 的环境变量里,本地
.env.local仅用于本地调试。
R2_ACCOUNT_ID:Cloudflare Account IDR2_ACCESS_KEY_ID:R2 API Token 的 Access Key IDR2_SECRET_ACCESS_KEY:R2 API Token 的 Secret KeyR2_BUCKET_NAME:R2 桶名R2_PUBLIC_DOMAIN:图片访问域名(自定义域或*.r2.dev)ADMIN_PASSWORD:登录密码SESSION_SECRET:会话签名密钥(建议 32+ 随机字符)
MAX_FILE_SIZE:单文件最大字节数(默认10485760= 10MB)MAX_FILES_PER_UPLOAD:服务端单次最多签名文件数(默认20)NEXT_PUBLIC_MAX_FILES_PER_UPLOAD:前端单次最多选择文件数(默认20)
ORDER_STORAGE_BACKEND:kv或r2(推荐kv)KV_ORDER_NAMESPACE_ID:使用 KV 时必填CF_ACCOUNT_ID:使用 KV 时必填CF_API_TOKEN:使用 KV 时必填(需 KV 读写权限)ORDER_R2_KEY:使用 R2 存排序时的对象路径(默认.imagebed/meta/order.json)
通常不需要,直接 KEY=value 即可。
只有这些情况建议加引号:
- 值里有空格
- 值里有
# - 值前后需要保留空白
- Cloudflare 控制台 ->
R2->Create bucket - 记录桶名(对应
R2_BUCKET_NAME) - 在 R2 中创建 API Token(S3 访问)并拿到:
- Access Key ID
- Secret Access Key
二选一:
- 使用 R2 Dev 域名(
https://pub-xxx.r2.dev) - 绑定你自己的域名(推荐)
把最终访问域写入:R2_PUBLIC_DOMAIN
如果你要浏览器直传(本项目默认),必须给 Bucket 配 CORS,否则会出现 No 'Access-Control-Allow-Origin'。
在 R2 Bucket 的 CORS 配置里填:
[
{
"AllowedOrigins": [
"http://localhost:3000",
"https://<your-pages-project>.pages.dev",
"https://<your-custom-domain>"
],
"AllowedMethods": ["GET", "HEAD", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]- Cloudflare 控制台 ->
Workers & Pages->KV->Create namespace - 记录 namespace id(对应
KV_ORDER_NAMESPACE_ID) - 创建 API Token,赋予该命名空间读写权限(
CF_API_TOKEN)
- Cloudflare ->
Workers & Pages->Create application->Pages->Connect to Git - 选择本仓库
- 构建配置:
- Build command:
npm run cf:build - Build output directory:
.vercel/output/static - Node.js:
20+
不要把输出目录设置为
.next。
.next/cache/webpack/*.pack可能超过 Cloudflare Pages 25 MiB 文件限制,出现:Pages only supports files up to 25 MiB in size。
Pages 项目 -> Settings -> Environment variables:
把第 4 节中的变量填进去(建议在 Production 和 Preview 都配置)。
- 点
Save and Deploy - 部署成功后访问你的 Pages 域名
- 能打开
/login并用ADMIN_PASSWORD登录 /upload能成功上传(无 CORS 报错)/gallery能看到上传文件- 拖拽排序刷新后不丢(KV/R2 持久化生效)
- 链接复制格式正常
原因:R2 CORS 没配好。
处理:按上文 5.3 配置,并确认 AllowedOrigins 包含你当前访问域。
原因:请求头包含了中文/Unicode 元数据。
处理:项目已内置编码修复;拉取最新代码并重启服务。
原因:文件夹字段前后多余 /。
处理:项目已在服务端自动清洗。
ZensImage 提供开放的图片上传 API,任何后端语言(Java / Python / Go / PHP 等)都可以直接调用,无需浏览器 session,适合博客系统、CMS、自动化脚本等场景。
方式一:网页后台申领(推荐)
- 用管理员账号登录你的图床
- 点击顶部导航栏的 「API 密钥」 按钮,或直接访问
/settings/apikeys - 填写 Key 名称(如「我的博客」),点击「创建」
- 立即复制完整 Key 保存,关闭弹窗后仅显示前缀,无法再次查看
- 可随时删除某个 Key 来撤销其访问权限
方式二:环境变量静态配置
在 Cloudflare Pages 环境变量中设置:
API_KEY=your_static_api_key
网页动态创建的 Key 和环境变量静态 Key 都有效,可同时使用。
POST /api/upload/direct
请求格式:multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file |
File | ✓ | 图片文件 |
tags |
String | 逗号分隔标签,如 blog,avatar |
|
folder |
String | 存储目录,如 posts |
响应示例:
{
"key": "2026-03-29/posts/uuid-photo.jpg",
"url": "https://your-domain.com/2026-03-29/posts/uuid-photo.jpg",
"filename": "photo.jpg",
"size": 102400,
"remaining": 19
}错误码:400 参数错误 | 401 Key 无效 | 429 频率限制
MediaType MEDIA_TYPE = MediaType.parse("image/jpeg");
RequestBody fileBody = RequestBody.create(new File("/path/to/photo.jpg"), MEDIA_TYPE);
MultipartBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", "photo.jpg", fileBody)
.addFormDataPart("tags", "blog,article")
.addFormDataPart("folder", "posts")
.build();
Request request = new Request.Builder()
.url("https://your-imagebed.com/api/upload/direct")
.header("Authorization", "Bearer " + API_KEY)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
String json = response.body().string();
// 从 json 中取 "url" 字段即为图片公开访问地址
}@Service
public class ImageBedService {
private static final String UPLOAD_URL = "https://your-imagebed.com/api/upload/direct";
private static final String API_KEY = "zib_your_api_key_here";
public String uploadImage(MultipartFile file) throws Exception {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
headers.set("Authorization", "Bearer " + API_KEY);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new MultipartInputStreamFileResource(
file.getInputStream(), file.getOriginalFilename()));
body.add("folder", "blog");
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(UPLOAD_URL, request, Map.class);
return (String) response.getBody().get("url"); // 返回公开访问地址
}
}import requests
API_KEY = "zib_your_api_key_here"
UPLOAD_URL = "https://your-imagebed.com/api/upload/direct"
def upload_image(file_path: str, folder: str = "", tags: str = "") -> str:
with open(file_path, "rb") as f:
resp = requests.post(
UPLOAD_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
data={"folder": folder, "tags": tags},
)
resp.raise_for_status()
return resp.json()["url"] # 返回公开访问地址
# 使用示例
url = upload_image("./photo.jpg", folder="blog", tags="python,demo")
print(url)curl -X POST https://your-imagebed.com/api/upload/direct \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@/path/to/photo.jpg" \
-F "tags=test" \
-F "folder=demo"app/
api/
auth/login
auth/logout
upload/presign # 浏览器预签名直传
upload/direct # 外部 API 直传(API Key 鉴权)
images
images/delete
images/order
apikeys # API Key 管理接口
login/
upload/
gallery/
settings/apikeys/ # API Key 管理页面
lib/
auth.ts
r2.ts
order-store.ts
rate-limit.ts
apikey-store.ts # API Key 存储
components/
upload-panel.tsx
gallery-panel.tsx
ui/*
npm run dev:本地开发npm run typecheck:类型检查npm run build:生产构建
可按你的需求改成 MIT / Apache-2.0 / 私有协议。