No description
  • Rust 54.1%
  • Vue 24.3%
  • TypeScript 18.8%
  • CSS 1.5%
  • Shell 0.5%
  • Other 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-25 21:27:16 +08:00
backend 删除图片时URL不再有效 2026-09-25 08:30:57 +08:00
docker-prod 更新文件结构以及优化注释说明 2026-09-19 01:25:30 +08:00
docs 删除图片时URL不再有效 2026-09-25 08:30:57 +08:00
frontend 优化图片的淡入显示效果 2026-09-25 21:27:16 +08:00
postgres-locale postgres eliminate warnings 2026-09-06 09:46:32 +08:00
.dockerignore Remove backup functionality and optimize code 2026-09-16 03:42:54 +08:00
.env.production.example 更新文件结构以及优化注释说明 2026-09-19 01:25:30 +08:00
.gitignore optimize code 2026-09-06 10:14:58 +08:00
docker-compose.dev.pg.yml 更新文件结构以及优化注释说明 2026-09-19 01:25:30 +08:00
docker-compose.dev.yml 更新文件结构以及优化注释说明 2026-09-19 01:25:30 +08:00
README.md 删除图片时URL不再有效 2026-09-25 08:30:57 +08:00

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'
# → ![photo.png](https://img.example.com/i/9f8e….png)

# 多文件 + 指定存储后端(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 该接口不接受令牌(改密码、令牌管理),或越权访问他人资源

两个容易踩的坑

  1. 上传前账号必须已有可用存储后端:自动创建本地目录属管理员能力,而令牌是非管理员, 所以全新账号会收到 当前没有可用的存储后端…。解决:网页端登录上传一次,或让管理员分配存储。
  2. 令牌是长期凭据,别写进仓库:放在环境变量或密钥管理里;泄漏就撤销重建(明文无法找回)。

配置项(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,切勿提交。