- Rust 54.1%
- Vue 24.3%
- TypeScript 18.8%
- CSS 1.5%
- Shell 0.5%
- Other 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| backend | ||
| docker-prod | ||
| docs | ||
| frontend | ||
| postgres-locale | ||
| .dockerignore | ||
| .env.production.example | ||
| .gitignore | ||
| docker-compose.dev.pg.yml | ||
| docker-compose.dev.yml | ||
| README.md | ||
KHimg 图床
简单、快速的图片托管服务。单文件数据库零依赖即可跑,也可换 PostgreSQL。
功能清单、API 参考、存储后端、内存防护与安全说明见
docs/features-security.md。
| 层 | 技术栈 |
|---|---|
| 后端 | Rust · Axum · SQLx(Any 驱动,SQLite / PostgreSQL 双支持) |
| 前端 | Vue 3 · TypeScript · Vite · Element Plus |
| 存储 | 本地磁盘(默认)· S3 兼容 · WebDAV(用户级多后端) |
| 认证 | 多用户 · Argon2 密码哈希 · JWT · 长期 API 令牌 |
快速开始
Docker 部署
1. 下载编排文件(二选一,存到同一目录,例如 /opt/khimg)
# SQLite(推荐,零依赖单文件)
curl -O https://git.popjm.com/rain/khimg/raw/branch/main/docker-prod/docker-compose.yml
# PostgreSQL
curl -O https://git.popjm.com/rain/khimg/raw/branch/main/docker-prod/docker-compose.pg.yml
2. 准备环境变量
curl -O https://git.popjm.com/rain/khimg/raw/branch/main/.env.production.example
cp .env.production.example .env # 至少改 JWT_SECRET(openssl rand -hex 32)
# PG 版还要在 .env 里设 POSTGRES_PASSWORD
3. 启动
sudo docker compose up -d # SQLite
sudo docker compose -f docker-compose.pg.yml up -d # PostgreSQL
4. 升级
sudo docker compose pull && sudo docker compose up -d
本地开发
方式 A:本地 Docker(不用装 Rust / Node,构建当前代码)
# 仓库根还没有 .env 时(⚠️ 已有生产 .env 就别覆盖)
cp .env.production.example .env # 至少改 JWT_SECRET;PG 版再设 POSTGRES_PASSWORD
# 启动(在仓库根目录执行)
docker compose -f docker-compose.dev.yml up -d --build # SQLite
docker compose -f docker-compose.dev.pg.yml up -d --build # PostgreSQL
# 改完代码重建;停止(down -v 会连数据卷一起删)
docker compose -f docker-compose.dev.yml up -d --build
docker compose -f docker-compose.dev.yml down
- 访问
http://localhost:<HTTP_PORT>(.env里的HTTP_PORT,默认 80;若被占用就换一个空闲端口) - 只有前端对外暴露端口,后端在 compose 网络内由 nginx 反代
- 数据落在仓库根
./data:SQLite 用sqlite/+uploads/,PG 另加postgres/(PG 编排还挂了disk1/) - 跑的是构建后的前端,没有热更新;改前端想边改边看用方式 B
方式 B:直接跑(前端热更新)
cd backend && cp .env.example .env && cargo run # 后端 → http://localhost:8080(自动迁移)
cd frontend && npm install && npm run dev # 前端 → http://localhost:5173(代理 /api /i /t)
测试
cd backend
cargo test # SQLite(默认,零依赖)
cargo clippy --all-targets -- -D warnings # 静态检查(零警告)
cargo fmt -- --check # 格式检查
./backend/scripts/test-postgres.sh # 双库:自动起一次性 PG 容器,跑完即销毁
cd backend && cargo test && ../backend/scripts/test-postgres.sh # 两库都跑一遍
cd frontend
npx vue-tsc --noEmit --noUnusedLocals --noUnusedParameters # 类型 + 未使用检查
npm run build # 生产构建
也可指定已有的测试库:KHIMG_TEST_POSTGRES_URL='postgres://...' cargo test(设置后整套用例改跑 PostgreSQL)。
每个用例创建独立的临时 SQLite 库或 PostgreSQL schema 与存储目录,不依赖外部服务、不污染开发数据。
| 文件 | 覆盖内容 |
|---|---|
src/api_tests.rs |
集成测试(复用生产路由 app::build_router):管理员产生、用户名/密码上限、登录失败、未认证访问、越权、私密图不列出但 URL 可访问、软删后原图不可访问、非 owner 不能删图、外链 HTML/Markdown 转义、回收站全流程、上传扩展名校验、缩略图生成、防盗链拦截与白名单、防盗链设置即时生效、解压炸弹拦截、SSRF 防护、存储卷列表限管理员、大页码分页不溢出、图片元信息长度上限、删除管理员后审计记录保留、API 令牌(明文一次性/哈希入库/受限权限/撤销·过期·删号失效/越权隔离/参数与数量校验) |
src/handlers/mod.rs |
SVG 净化(剥离 script / 事件属性 / javascript: 链接) |
src/hotlink.rs |
防盗链匹配(本站放行、白名单、*. 只匹配子域不含裸域、notexample.com 不误放行) |
双库跑同一套用例能暴露行为差异(占位符、布尔/整数映射、
ON CONFLICT语义等),例如批量插入 曾因 PostgreSQL 不支持某种ON CONFLICT ... DO NOTHING+DO UPDATE组合而只有 SQLite 通过。
外部程序接入(API 令牌)
给脚本、截图工具、自动化程序用的长期凭据:调用的还是现有用户级接口,只是把凭据从「账号密码换 JWT」
换成固定令牌 —— 不必保存密码,也不受 7 天过期限制。完整接口清单见 docs/features-security.md。
创建
网页右上角头像 →「API 令牌」→ 填名称、选有效期(30 / 90 / 365 天或永久)。 明文只显示一次(服务端只存 SHA-256 哈希),忘记只能撤销后重建。
调用
请求头加 Authorization: Bearer <令牌>,其余与网页端完全一致。下面用 curl 演示;
先准备好两个变量(后文统一用 -H "$AUTH" 带认证):
KEY=kh_3f9c1a2b000000000000000000000000 # 创建时那串明文,只显示一次
HOST=https://img.example.com # 换成你的实例地址
AUTH="Authorization: Bearer $KEY"
示例用
jq解析 JSON;未安装可去掉| jq …,或改用python3 -c 'import sys,json;print(json.load(sys.stdin)["images"][0]["url"])'。
上传并取外链(最常用)
# 单文件:字段名固定为 files,重复该字段即可一次上传多张
curl -sS -X POST "$HOST/api/upload" -H "$AUTH" -F 'files=@photo.png' \
| jq -r '.images[0].url'
# → https://img.example.com/i/9f8e….png
# 服务端已生成好 Markdown / HTML / BBCode,直接取用即可
curl -sS -X POST "$HOST/api/upload" -H "$AUTH" -F 'files=@photo.png' \
| jq -r '.images[0].markdown'
# → 
# 多文件 + 指定存储后端(backend_id 见 GET /api/backends)
curl -sS -X POST "$HOST/api/upload?backend_id=<id>" -H "$AUTH" \
-F 'files=@a.png' -F 'files=@b.png' | jq -r '.images[].url'
浏览与检索
# 分页 + 筛选(可组合):own_only / public_only / private_only / search / tags / mime_type
curl -sS "$HOST/api/images?page=1&page_size=20&own_only=true" -H "$AUTH"
curl -sS "$HOST/api/images?search=壁纸&tags=风景,旅行" -H "$AUTH" # tags 逗号分隔,需全部命中
# → {"items":[…],"total":12,"page":1,"page_size":20}
修改 / 删除 / 回收站
ID=<上一步 items[].id 里的值>
curl -sS -X PATCH "$HOST/api/images/$ID" -H "$AUTH" \
-H 'Content-Type: application/json' \
-d '{"title":"新标题","description":"备注","tags":["壁纸"],"is_public":false}'
# 字段均可选:只传要改的;传空字符串 / 空数组即清空
curl -sS -X DELETE "$HOST/api/images/$ID" -H "$AUTH" # 软删除(进回收站)→ 204
curl -sS -X POST "$HOST/api/recycle/$ID/restore" -H "$AUTH" # 从回收站恢复 → 204
curl -sS -X DELETE "$HOST/api/recycle/$ID/purge" -H "$AUTH" # 彻底删除(记录 + 物理文件)→ 204
curl -sS -X DELETE "$HOST/api/recycle/purge-all" -H "$AUTH" # 清空回收站 → {"purged":N}
取图(含私密图)
# 公开图与私密图都是固定 URL,直接取即可(<storage_key> 就是外链里 /i/ 后面那一段)。
# 私密图是「不列出」:列表里看不到,但 URL 即访问凭据(128 位随机 key,猜不到)。
# 注意:已软删除(回收站中)的图片原图返回 404,需先恢复。
curl -sS "$HOST/i/<storage_key>.png" -o out.png
字段上限(超限返回 400)
| 字段 | 上限 |
|---|---|
title |
200 字符 |
description |
2000 字符 |
tags |
最多 20 个,单个 ≤ 50 字符 |
能做什么、不能做什么
| 说明 | |
|---|---|
| ✅ 用户级接口 | 上传、列表、改信息、删除、回收站、统计、自有存储后端 |
| ❌ 管理接口 | 权限固定为普通用户:即使账号是管理员,也调不动 /api/admin/* |
| ❌ 修改密码 / 令牌管理 | 只接受登录态(否则泄漏的令牌可接管账号或自我增殖) |
限制与错误码
有效期 30 / 90 / 365 天或永久;每用户最多 20 个;撤销后立即失效,删号时级联删除。
| 错误码 | 含义 |
|---|---|
400 |
参数不合法:名称空或超 50 字、有效期不在 1~3650 天、令牌数已达 20 个 |
401 |
令牌无效 / 已撤销 / 已过期 / 账号已删除 |
403 |
该接口不接受令牌(改密码、令牌管理),或越权访问他人资源 |
两个容易踩的坑
- 上传前账号必须已有可用存储后端:自动创建本地目录属管理员能力,而令牌是非管理员,
所以全新账号会收到
当前没有可用的存储后端…。解决:网页端登录上传一次,或让管理员分配存储。 - 令牌是长期凭据,别写进仓库:放在环境变量或密钥管理里;泄漏就撤销重建(明文无法找回)。
配置项(backend/.env)
| 变量 | 默认值 | 说明 |
|---|---|---|
DATABASE_TYPE |
sqlite |
sqlite / postgres(生产由编排文件固定) |
DATABASE_URL |
sqlite://data/sqlite/khimg.db?mode=rwc |
连接串;Docker 部署的 PG 由后端自动拼接 |
POSTGRES_HOST POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB |
localhost 5432 postgres postgres khimg |
仅 PG 模式使用;密码生产必改 |
BIND_ADDR |
0.0.0.0:8080 |
监听地址 |
PUBLIC_BASE_URL |
http://localhost:8080 |
外链基础地址 |
MAX_UPLOAD_BYTES |
20971520(20MB) |
单文件上传上限 |
MAX_UPLOAD_REQUEST_BYTES |
104857600(100MB) |
单次请求总量上限(多选时的总和) |
THUMBNAIL_SIZE |
400 |
缩略图最大边长(px) |
THUMBNAIL_JPEG_QUALITY |
80 |
缩略图 JPEG 质量(1-100) |
MAX_IMAGE_PIXELS |
150000000(1.5 亿) |
单张最大像素数(宽×高),解码前校验;解码内存 ≈ 像素数 × 4 字节 |
UPLOAD_CONCURRENCY |
2 |
同时处理的上传数(信号量),与像素上限联动控制峰值内存 |
BACKEND_MEMORY_LIMIT |
2g |
后端容器内存硬上限,需 ≥ 下方公式算出的峰值 |
FRONTEND_MEMORY_LIMIT |
256m |
前端 nginx 容器内存上限 |
POSTGRES_MEMORY_LIMIT |
512m |
PostgreSQL 容器内存上限(仅 PG 编排) |
JWT_SECRET |
change-me-please-... |
JWT 签名密钥(必须改,≥32 字符) |
CORS_ORIGINS |
无(仅同源) | 允许跨域的前端域名,逗号分隔 |
STORAGE_DIR |
uploads |
本地存储根目录(Docker 下为 /app/uploads) |
LOCAL_STORAGE_ROOT |
/app/uploads |
本地存储允许的根目录(多挂载卷白名单) |
TRUST_PROXY_HEADERS |
true |
是否信任反代写入的 X-Real-IP/X-Forwarded-For。后端未经反代时必须设 false,否则客户端可伪造该头绕过限流 |
RUST_LOG |
khimg_server=info,tower_http=info |
日志级别 |
数据目录
所有数据放在 .env 的 DATA_ROOT(默认 ./data)下,切换数据库无需改路径,图片天然共用:
<DATA_ROOT>/
├── sqlite/ # SQLite 库文件(khimg.db)
├── postgres/ # PostgreSQL 数据目录(仅 PG 模式)
└── uploads/ # 图片存储(两种数据库共用)
各子目录可单独覆盖:SQLITE_DATA_DIR / POSTGRES_DATA_DIR / UPLOADS_DIR。
目录结构
khimg/
├── backend/ # Rust 后端(Axum + SQLx)
│ ├── src/
│ │ ├── main.rs # 入口:读配置 → 连库 → 迁移 → 启动
│ │ ├── app.rs # 路由装配(生产与测试共用同一套)
│ │ ├── config.rs # 配置读取 + 本地存储卷白名单
│ │ ├── models.rs # 数据模型与响应结构
│ │ ├── error.rs # 错误类型与 AppState
│ │ ├── auth.rs # 认证:JWT、API 令牌分流
│ │ ├── users.rs # 注册登录、用户管理、站点设置项
│ │ ├── admin.rs # 站点标题与资源(logo/favicon)、防盗链设置
│ │ ├── api_keys.rs # API 令牌:生成、校验、增删
│ │ ├── audit.rs # 管理员操作审计
│ │ ├── hotlink.rs # 防盗链规则与内存缓存
│ │ ├── storage.rs # 存储抽象(local / S3 / WebDAV)
│ │ ├── backends.rs # 用户级多存储后端
│ │ ├── ratelimit.rs # 登录 / 注册限流(带定期清理,防内存增长)
│ │ ├── cache.rs # 浏览量计数(定期刷盘)
│ │ ├── handlers/ # 上传、列表、管理、访问、回收站、统计
│ │ └── api_tests.rs # 集成测试(跑生产路由)
│ ├── migrations/ # SQL 迁移(sqlite 8 个 / postgres 27 个)
│ ├── scripts/ # test-postgres.sh:双库测试
│ └── Cargo.toml · Dockerfile · .env.example
├── frontend/ # Vue 3 + Vite + Element Plus
│ ├── src/
│ │ ├── main.ts · App.vue # 入口与主界面(导航 / 登录 / 主题)
│ │ ├── api.ts · types.ts # 接口封装与类型定义
│ │ ├── theme.ts · style.css # 主题逻辑与全局样式变量
│ │ ├── siteMeta.ts # 站点标题/图标共享状态(页头与标签页同步)
│ │ ├── router.ts # 路由
│ │ ├── views/ # HomeView / DashboardView / RecycleView
│ │ │ └── dashboard/ # 管理页子组件(统计/设置/后端/用户/弹窗)
│ │ ├── composables/ # 组合式逻辑(图片/认证/用户/后端/令牌…)
│ │ └── components/ # 上传区、图片卡片、预览、各类弹窗
│ ├── public/theme-init.js # 渲染前恢复主题/标题/图标,并禁用滚动恢复(CSP 要求外链脚本)
│ ├── nginx.conf # SPA 路由 + API 反代 + 安全头
│ ├── security-headers.conf # 安全响应头(被 nginx.conf 多处 include)
│ └── Dockerfile · index.html · vite.config.ts
├── docker-prod/ # 生产编排(用 Docker Hub 镜像)
│ ├── docker-compose.yml # SQLite
│ └── docker-compose.pg.yml # PostgreSQL
├── docker-compose.dev.yml # 本地开发(本地构建,SQLite)
├── docker-compose.dev.pg.yml # 本地开发(本地构建,PostgreSQL)
├── postgres-locale/ # 自定义 PG 镜像(Alpine + musl-locales)
├── docs/
│ ├── docker-push.md # 构建 / 打标签 / 推送镜像
│ └── features-security.md # 功能 / API 参考 / 存储 / 内存 / 安全
└── .env.production.example # 生产环境变量模板(复制成 .env 用)
运行时生成的
data/与含密钥的.env均已加入.gitignore,切勿提交。