上传
用 HTTP 接口、URL 转存或管理后台上传图片,以及格式与大小限制、EXIF 处理、幂等键、查重关系与写入语义。
上传入口
| 入口 | 认证 | 适合 |
|---|---|---|
POST /api/upload(multipart 表单) | 上传 Token | 脚本、PicGo / PicList 自定义图床 |
POST /api/upload/url(JSON) | 上传 Token | 把别处的图片地址交给服务端抓取入库 |
| 管理后台「图片」页的「上传图片」 | 登录会话 | 拖拽、点选、粘贴剪贴板、链接转存 |
S3 协议 /s3/… | 由上传 Token 派生的 S3 凭证 | 带 S3 上传能力的客户端,见 上传客户端 |
四个入口走同一条上传管道:格式校验、像素预算、配额预检、EXIF 处理、写入渠道、入库。 差别只在字节从哪来,以及能带哪些选项:S3 入口不能逐次指定有效期,不做 EXIF 处理,也不支持幂等键。
上传 Token 在后台「上传 Token」页签发,见 管理后台。
用接口上传
curl -H "Authorization: Bearer imgy_xxxxxxxx" \
-F "file=@photo.jpg" \
https://img.example.com/api/upload带上全部可选项:
curl -H "Authorization: Bearer imgy_xxxxxxxx" \
-H "Idempotency-Key: 7d0c6a2e-photo-0001" \
-F "file=@photo.jpg" \
-F "ttl=604800" \
-F "exif_policy=strip" \
https://img.example.com/api/upload| 表单字段 | 必填 | 说明 |
|---|---|---|
file | 是 | 图片文件。文件名只作元数据(后台展示与搜索),去掉控制字符后截断到 255 字节。只取第一个带文件名的 file 段 |
ttl | 否 | 有效期,整数秒(0 到 3153600000)或 never。0 与 never 都表示永久;省略时按「用户默认 > 全局默认」,见 生命周期与查重 |
exif_policy | 否 | strip 或 passthrough;省略时用全局默认(出厂 passthrough),见下文 EXIF 处理 |
ttl 与 exif_policy 只从 multipart 表单字段读取,写在 URL 查询参数里不生效;每个字段最多 1 KiB,超出返回 400。表单里的其他字段被忽略。
请求头 Idempotency-Key 可选,见 幂等键。
成功返回 200:
{
"id": "R0RjUyP1UW",
"url": "https://img.example.com/i/R0RjUyP1UW"
}id是 10 位短 ID,字符取自0-9A-Za-z。url恒为绝对地址:设置了「对外站点地址」(base_url)就用它;没设置时按本次请求推导,协议看连接是否 TLS,域名取Host头。 经信任的反向代理转发时改看X-Forwarded-Proto与X-Forwarded-Host(信任范围由环境变量IMGYOUNG_TRUSTED_PROXIES决定)。 反代或 CDN 对外的域名与请求里的域名不一致时,必须配置base_url,见 配置 与 反向代理。
失败时返回 {"error": "…"},状态码见 状态码速查。完整接口定义见 接口参考:数据面。
幂等键
网络不稳时客户端可能重发同一次上传。请求带 Idempotency-Key 头,就能避免重发产生第二张图:
- 键按用户隔离,不同用户用同一个键互不影响,也就无法借幂等键探测或取到别人的图。
- 保留期内,同一用户带同一个键的请求直接返回第一次成功上传的
id与url,不再写入渠道。保留期由idempotency_key_ttl_hours决定,出厂 24 小时,从第一次成功时起算。 - 命中后不比较本次请求的内容:同一个键换了文件,返回的仍是第一次的那张图。每张不同的图要用不同的键。
- 只有成功的上传会记下键;失败的请求可以带同一个键放心重试。
- 并发发出的同键请求只有一个真正入库,其余返回同一结果。
- 超过保留期的键由清理扫描回收;图片被删除时,指向它的键一并删除。此后再用这个键会被当作新上传。
/api/upload、/api/upload/url 都支持幂等键;S3 入口没有。
URL 转存
给一个图片地址,服务端替你下载后入库,常用来把散在其他图床的图收回来。抓回的字节与直接上传走同一条上传管道,格式白名单、像素预算、EXIF 处理与配额照常生效;服务端不会把远端文件直接拷进渠道。
curl -H "Authorization: Bearer imgy_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/photo.png", "ttl": "never"}' \
https://img.example.com/api/upload/url请求体是 JSON 对象,只允许以下字段(出现其他字段返回 400),大小不超过 8 KiB:
| 字段 | 必填 | 说明 |
|---|---|---|
url | 是 | 图片地址,仅 http / https |
file_name | 否 | 文件名;省略时取地址路径的最后一段,取不到时为 fetched |
exif_policy | 否 | 同表单上传 |
ttl | 否 | 同表单上传;数字 3600 与字符串 "3600"、"never" 都接受 |
返回体与 Idempotency-Key 的用法和表单上传相同。抓回的字节按内容判定格式,远端声明的 Content-Type 不参与判断。
抓取限制
这个入口相当于让服务端替请求方发起外部请求,所以抓取有一组固定约束,防止被借来探测你的内网:
| 约束 | 规则 | 不满足时 |
|---|---|---|
| 协议 | 只允许 http / https,重定向的每一跳同样检查 | 400 |
| 目标地址 | 只允许公网地址。判定发生在每次建立连接前,针对实际解析出的 IP,所以 DNS 重绑定与重定向到内网都会被拦下 | 400 |
| 重定向 | 最多 5 次 | 502 |
| 大小 | 与单图上限相同(max_upload_mb)。远端声明的 Content-Length 超限直接拒绝;未声明时读到超限即停止 | 413 |
| 时长 | 整次抓取最长 30 秒;其中建立 TCP 连接、TLS 握手、等待响应头各不超过 10 秒 | 502 |
| 远端状态 | 只接受 2xx | 502 |
被拒绝的地址段:
- IPv4:
0.0.0.0/8、10.0.0.0/8、100.64.0.0/10、127.0.0.0/8、169.254.0.0/16(含云厂商元数据地址169.254.169.254)、172.16.0.0/12、192.0.0.0/24、192.168.0.0/16、198.18.0.0/15、224.0.0.0/4、240.0.0.0/4。 - IPv6:
::/128、::1/128、::/96、64:ff9b::/96、64:ff9b:1::/48、2002::/16、fc00::/7、fe80::/10、ff00::/8。IPv4 映射地址(如::ffff:127.0.0.1)按其中的 IPv4 判定。
另外几点:
- 抓取直连目标,不使用
HTTP_PROXY之类的代理环境变量。 - 请求不带
Referer,User-Agent是 Go 标准库的默认值。开了防盗链或屏蔽爬虫的源站可能拒绝,表现为 502。 - 抓取期间占用一个上传并发名额(见 格式与上限)。
在管理后台上传
在「图片」页点「上传图片」,弹窗里有两个标签:
- 「本地文件」:点击选择或把图片拖进虚线框,可多选;弹窗打开期间按 Ctrl / ⌘ + V 可直接粘贴剪贴板里的图片。文件每次最多两个同时上传,其余排队。
- 「链接转存」:填图片地址后点「转存」,等同于 URL 转存。
弹窗下方的「有效期」(默认 / 永久 / 自定义小时数)与「EXIF」(默认 / 剥离 / 保留)对之后加入的文件生效,「默认」即不传该字段。 关闭弹窗不会中断进行中的上传;每张上传成功后列表里显示链接,可一键复制。
后台上传归属当前登录账号,不需要 Token,图片详情里的「来源」显示为「后台上传 / 来源已删除」。它与 Token 上传共用频控与并发闸。
格式与上限
只接受 JPEG、PNG、GIF、WebP。格式按文件内容(magic bytes)判定,并与解码器识别出的格式交叉核对,扩展名与客户端声明的类型一概不看。
- 不接受 SVG(可内嵌脚本)。
- 动画 GIF 可以上传,原样保存,访问时始终输出原图,动画保留。
- 动画 WebP 不支持,上传返回 400。
| 限制 | 配置键 | 出厂值 | 超出时 |
|---|---|---|---|
| 单图大小 | max_upload_mb | 50 MB | 413。只算文件本身,大小恰好等于上限的文件可以上传;整个请求体另以「单图上限 + 64 KiB」为界,超出同样 413 |
| 像素预算 | max_pixels_mp | 50 百万像素(宽 × 高) | 422。只读图片头判断,超限不进入完整解码 |
| 上传并发 | max_concurrent_uploads | 8 | 429,响应头 Retry-After: 1 |
| 上传频率 | upload_rate_per_min | 0(不启用) | 429,响应头 Retry-After: 60 |
| 存储配额 | 每个用户单独设置 | 不限 | 403 |
- 并发闸全局只有一份,数据面上传、URL 转存、后台上传与全部 S3 请求(包括 GET / HEAD / DELETE)共用。它限制的是同时在处理的请求数;上传内容不论大小都只在内存里处理、不写临时文件,所以这个数决定了上传的峰值内存。满了立即返回 429,不排队。
- 像素预算挡的是解码后的内存:它由分辨率决定,与文件大小无关,一张 2000 万像素的图解码成 RGBA 约 80 MB。所以先只读图片头判断,超限的图不进入解码。
- 频控按用户计,同一用户经 Token、后台与 S3 PutObject 的上传共享额度。算法是令牌桶:可以一次性连续上传 N 张(N 为每分钟额度),之后每 60/N 秒恢复一张。额度保存在进程内存里,重启即重置。
- 配额按引用计全额:每条图片记录都按其物理文件的完整大小计入归属用户,后台查重合并不会减少已用量。预检用上传时的原始字节数。
以上配置都在「设置 → 运行配置」修改,保存即生效,见 配置。用户配额在「用户」页设置。
状态码速查
| 状态码 | 含义 |
|---|---|
| 200 | 成功(幂等键命中也是 200) |
| 400 | 缺少 file 字段或请求体无法读取;图片内容无法解析(含动画 WebP);ttl 或 exif_policy 取值非法或超过 1 KiB;URL 转存的请求体非法、协议不是 http/https 或目标不是公网地址 |
| 401 | Token 缺失、无效、已吊销、已过期,或所属用户被禁用 |
| 403 | Token 没有上传权限,或存储配额已满 |
| 413 | 文件超过单图大小上限,或请求体超过「单图上限 + 64 KiB」(URL 转存时为远端内容超限) |
| 415 | 不是支持的图片格式 |
| 422 | 分辨率超过像素预算 |
| 429 | 上传并发已满,或该用户上传过于频繁;按 Retry-After 稍后重试 |
| 502 | URL 转存:远端返回非 2xx、重定向过多、超时或连接失败 |
| 503 | 没有健康渠道可写,或同步写入的份数达不到最少同步副本数 |
EXIF 处理
| 档位 | 行为 |
|---|---|
passthrough | 原样保存,不动任何元数据。全局出厂默认 |
strip | 剥离元数据后保存,范围见下表 |
出厂默认是 passthrough:存储商能看到原图的问题由渠道存储加密解决;要不要对拿到链接的访客抹掉元数据,是上传者自己的选择,所以默认不改动原图。
本次上传的档位按「表单字段(或 JSON 字段)> 全局默认 exif_policy」决定。S3 上传不做 EXIF 处理,一律按原字节保存:S3 客户端按大小与 ETag 核对上传结果,保存的字节必须与上传的一致。
spoof(伪造元数据)尚未开放,传入返回 400,也不能设为全局默认。
strip 是字节级操作,不重新编码、不改动任何像素:
| 格式 | 剥离 | 保留 |
|---|---|---|
| JPEG | 除 APP0(JFIF)、APP2(ICC 色彩配置)、APP14(Adobe)之外的全部 APPn 段,包括 EXIF(GPS、机型、拍摄时间等)、XMP、IPTC 与相机私有段;COM 注释段;EOI 之后的附加数据(如 Motion Photo 里的视频) | 熵编码数据原样保留。原图带非默认的方向标记(Orientation)时,补回一个只含方向的最小 EXIF 段,照片仍按正确方向显示。渐进式 JPEG 各次扫描之间的段不检查 |
| PNG | eXIf、tEXt、zTXt、iTXt 块;IEND 之后的附加数据 | gAMA、iCCP 等影响颜色渲染的块 |
| WebP | EXIF 与 XMP chunk,并同步清除 VP8X 头里对应的标志位、修正 RIFF 长度;RIFF 声明长度之外的附加数据 | 像素 chunk(含动画帧) |
| GIF | 不处理 | GIF 没有承载 EXIF / XMP 的标准容器 |
- 图片的 SHA-256(后台显示的哈希、查重的依据)按剥离后实际保存的字节计算。
passthrough保存的原图里的 GPS 等信息,任何拿到链接的人都能取到原始字节读出来。 按需处理 输出的衍生图是重新编码的,不带元数据,但原图本身仍可访问。
为什么在上传时处理
EXIF 档位在上传那一刻生效,保存的就是处理后的字节;访问时不再按当前设置重新处理原图。
原因是访问层的缓存建立在「同一个 /i/ 链接的字节永不改变」之上:响应带 Cache-Control: immutable,浏览器与 CDN 可以放心缓存一年。
如果改成访问时处理,全局档位一改,同一个链接输出的字节就变了:CDN 上的缓存要整体清除,而访客浏览器里已经缓存的旧字节无法让它失效。
访问时处理的主要好处,是原图可以原样留在渠道、只在出口净化;存储商侧的隐私已由渠道存储加密解决,上传时处理加上默认 passthrough 已经覆盖了这层需要,不值得为它放弃不可变缓存。
同一张图多次上传
上传路径上不做查重:同一个文件上传两次,得到两个短 ID,渠道里也会先写入两份。
这是有意的:上传路径上没有「秒传」之类的分支,每次上传都完整走一遍校验、写渠道与入库,出问题的面最小。查重只用上传时顺手算好的哈希, 在后台是纯数据库操作,不必重新解码图片。
后台查重循环(出厂每 300 秒一轮,dedup_scan_seconds)会把实际保存字节完全相同的物理文件自动合并为一份,多余的那份按删除宽限期清理。合并后:
- 每次上传仍有自己的短 ID、有效期、访问计数与归属用户,所有链接继续可用。
- 配额按引用计全额,合并不减少已用量。
用不同 EXIF 档位上传的同一张照片,保存的字节不同,不会被自动合并;它们的感知哈希接近,会进入「疑似重复」等管理员裁决。 细节见 生命周期与查重。客户端重试造成的重复,用 幂等键 从源头避免。
写入语义
一次上传在返回之前,按优先级从高到低依次写入健康渠道,直到写成功的份数达到 min_success(「最少同步副本」,出厂 1):
- 写入失败或超时(每次写入最长 10 分钟)就顺延到下一个健康渠道;第一个写成功的渠道就是这张图的主渠道。
- 份数凑够后写入元数据库并返回。其余渠道记为待补齐,由后台补齐任务异步复制,不拖慢本次上传。
- 健康渠道不足、或全部尝试后份数仍不够,返回 503。已经写进渠道的对象成为孤儿,由对账任务在 48 小时宽限期后回收。
min_success 大于 1 时,一次上传要等多个渠道都写完才返回,换来返回时就已有多份副本;健康渠道数少于该值时上传一律 503。
渠道的优先级、健康探测与补齐见 存储渠道。