
图床是很多人第一次真正用上对象存储的场景:写博客要贴图,截图一多,本地路径、第三方图床的失效链接、配额限制轮番找上门。把图床后端换成自己的 S3 兼容存储,一次配好之后上传是 Ctrl+V 的事,链接在自己手里,不担心哪天服务关停。
这篇文章走完从 PicGo 配置到链接输出的完整链路,以 RustFS 为后端,其他 S3 兼容存储的配置项基本一致。
PicGo 在整个链路里的位置
PicGo 的定位是"把图片上传变成创作流程里无缝的一步",官方 README 的原话:
Whether you're writing a blog post, taking notes, or authoring developer docs, PicGo helps you upload images in one step and automatically copies the resulting link.
它在整条链路里负责三件事:监听剪贴板或文件拖拽、调 S3 接口上传、把可访问的 URL 复制回剪贴板。写 Markdown 时的典型流程变成:截图(或复制图片)、按快捷键、粘贴链接,三步完成,不再有"先打开图床网站、上传、复制链接"这个中断创作节奏的循环。
它支持的图床分两层:内置的(七牛、腾讯云 COS、又拍云、阿里云 OSS、GitHub、SM.MS、Imgur)和插件扩展的。README 里明确一句:PicGo itself will no longer add new third-party Image hosts by default,新图床要靠插件生态,插件列表里包括 AWS S3、Cloudflare R2、MinIO 等。
S3 插件的选型是 wayjam/picgo-plugin-s3,README 里写明支持 Amazon S3 与兼容 S3 API 的云存储(例如 Backblaze B2),支持 PicGo GUI,也支持 MinIO。"支持 MinIO" 意味着 path-style 访问是现成的,对接 RustFS 这类同样默认 path-style 的存储时不用额外折腾。
安装两条路:GUI 用户在插件市场搜 s3 直接下载;Core 用户 picgo add s3。配置入口是 picgo set uploader s3。
这个插件是社区维护的,不是 PicGo 官方出品,版本组合会直接影响能不能连上。仓库的提交历史里有 fix compatibility with new ver of client-s3 这类修复,说明 PicGo 本体、client-s3 依赖、插件三者版本对不上就会出问题。后面遇到上传报错,先别急着改配置,把插件升到最新、必要时连 PicGo 本体一起升,再排别的因素。
PicGo 还有 Typora、Obsidian 等编辑器的集成方案:Typora 里直接配"上传服务"指向 PicGo 的 HTTP 接口(默认 http://127.0.0.1:9876),写文档时粘贴图片自动上传,这套组合是中文技术写作圈最常见的搭配。对接细节在 PicGo 官方文档的"配合 Typora"一节,这里不展开。
配置文件本身值得备份一份。PicGo 的配置存在用户目录下(Windows 是 %APPDATA%\picgo,macOS 与 Linux 是 ~/.picgo),换机器时把 config.json 拷过去就能直接复用整套图床设置,不用再填一遍。要注意里面存着明文密钥,备份文件别随手丢进公开仓库。
还有一点容易误会:PicGo 的相册只记录上传历史,删掉相册里的条目不会删掉远端对象,清理存储要在对象存储侧做。

S3 插件的配置项逐个看
插件 README 给的配置项列表里,对接 RustFS 需要动的是这几项:
| 配置键 | 作用 | 对接 RustFS 的值 |
|---|---|---|
accessKeyID |
S3 凭证 ID | RustFS 的 access key |
secretAccessKey |
S3 凭证密钥 | RustFS 的 secret key |
bucketName |
桶名 | 已建好的图床桶 |
endpoint |
自定义终端节点 | http://<rustfs-host>:9000 |
pathStyleAccess |
path-style 访问模式 | 必须设为 true |
uploadPath |
上传路径模板 | {year}/{month}/{fullName} 之类 |
outputURLPattern |
输出 URL 模板 | 见下节 |
acl |
上传对象的 ACL | RustFS 不做对象 ACL,见下节 |
两个容易配错的键单独说。pathStyleAccess 默认 false(virtual-hosted style),RustFS 官方文档明确写了 Path Style 是默认访问方式,bucket 名在请求路径里(http://localhost:9000/my-bucket/hello.txt),不用 DNS 配置;virtual-hosted style 需要 RUSTFS_SERVER_DOMAINS 配置了对应域名才能工作。插件设 true 才能对上 RustFS 的默认行为。
acl 默认 public-read,但 RustFS 官方兼容性矩阵里 ACL authorization 被标为 Excluded(intentionally unsupported),也就是明确不支持对象级 ACL。图床桶的公开访问要靠桶策略控制(RustFS 的 Bucket policies 是已测试覆盖状态),插件里的 acl 值在 RustFS 上不会生效。图床桶的公开读策略要在 RustFS 侧单独配。
把这些键对着 RustFS 侧看一遍,会更清楚每个值为什么这么填:

输出 URL 模板决定链接长什么样
插件上传成功后会把链接复制到剪贴板,链接的形态由 outputURLPattern 决定。README 给的示例是:
https://img.example.com/{bucket}/{path}
如果 RustFS 前面有 Nginx 或 CDN 做反向代理,模板可以写成代理域名 + 路径;如果没有代理、直接访问 RustFS,模板可以指向 http://<host>:9000/{bucket}/{path}。
这两个字段的职责要分开看,填混了是新手最常见的失败来源:endpoint 是真正发 S3 请求的那个地址,指向 RustFS 本体(http://<host>:9000);outputURLPattern 只负责把上传结果的路径拼成 Markdown 里的访问链接,不参与上传 。前面挂了 HTTPS 代理的场景下,只有链接用代理域名,endpoint 仍要写后端真实地址。把 endpoint 也填成 CDN 或代理域名的话,S3 请求发到那里找不到桶,上传直接失败。
选哪种形态要考虑链接的存活期。直接指向 http://<host>:9000 意味着以后换域名、换端口、上 HTTPS,文章里所有旧链接都得改一轮;指向一个代理域名则把这些变化挡在后面,迁移时只动 Nginx 配置。另外,如果文章最终发布在 HTTPS 站点上,图床链接是 http:// 会被浏览器拦成混合内容,这也是倾向于挂一层代理域名并配好证书的原因。
这个模板还有两个实用变量:{year} 和 {month} 常用在 uploadPath 里做目录分层,{md5} 可以用作文件名避免重名。README 给的完整配置示例:
json
{
"picgo-plugin-s3": {
"accessKeyID": "xxx",
"secretAccessKey": "xxxxx",
"bucketName": "my-bucket",
"uploadPath": "{year}/{md5}.{extName}",
"endpoint": "s3.us-west-000.backblazeb2.com",
"outputURLPattern": "https://img.example.com/{bucket}/{path}"
}
}
uploadPath 用 {year}/{md5}.{extName} 的好处是按年分目录、文件名用内容哈希避免重复上传同名文件。{fullName} 则保留原文件名,适合在意可读性的场景,但同名文件会互相覆盖。
还有三个实用配置项顺带说。proxy 支持给上传请求走 HTTP 代理,内网走代理出公网的场景用得上;urlPrefix 和 urlSuffix 在 README 里标了已废弃,用 outputURLPattern 替代,旧配置迁过来时注意别再用废弃键。
rejectUnauthorized 默认 true,只有后端是自签证书时才需要设成 false。这个开关的代价得讲清楚:设成 false 等于把整条链路的 TLS 证书校验关掉,access key、签名、图片内容全在这条连接上跑,中间节点不校验就能读走。它适合内网自签证书的个人环境,生产应该把 CA 证书装进跑 PicGo 的那台机器的信任库、保持校验开着,而不是靠关校验换取连通。
桶策略怎么配才能公开读
RustFS 的 ACL 不可用,公开读要靠桶策略。最小可用的一条 Allow 策略给 s3:GetObject:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::picgo-images/*"
}
]
}
配完策略后,上传的图片任何人都能通过 URL 读到,但写操作仍然需要签名凭证。这个"公开读、受控写"的形态就是图床的标准形态。
Principal 写成 "*" 的准确含义是互联网上任何匿名访问者,不持有任何身份就能读。这个桶、这个前缀下的对象因此全部对外可读:内部架构截图、带报错信息的调试图、还没发布的稿件配图,只要落在同一前缀下就会被外部直接下载。落笔时把公开范围收窄到专门前缀(比如 public/*),私密内容放别处,这条边界比事后清理要省事得多。
要注意的是这条策略只对列在 Resource 里的前缀生效。如果同一个桶里还存了私密文件,策略里要限定到图床用的前缀(比如 arn:aws:s3:::picgo-images/public/*),插件侧的 uploadPath 也对应加前缀,两边对上才不会把私密路径暴露出去。
RustFS 兼容性矩阵里 Bucket policies 标的是已测试覆盖状态(Put、get、delete 三种操作都验证过),这条路径官方支持。
给 PicGo 用的凭证应该单独开一对,不要复用管理员 key。最小权限的做法是只给 s3:PutObject 和 s3:GetObject,Resource 限定在图床桶的图床前缀下。这样即使配置文件泄露(前面说了里面有明文密钥),影响范围也只是"能往图床目录传图",不会波及桶里的其他数据。
对应的策略长这样,可以直接照抄替换成自己的桶名:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PicGoUploadOnly",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::picgo-images/public/*"
}
]
}
Action 里只放上传和读回,删除、列举一概不给。图床场景没有删图需求,给出去就多一条误删的通路。客户端要做前缀预览才需要 s3:ListBucket,那种情况资源级写桶本身(arn:aws:s3:::picgo-images)而不是 /*,/* 会把桶内所有前缀都圈进来。验证这个策略的办法很直接:拿这一对凭证配进 PicGo 传一张图能成,换一个不带此策略的账号去下载同一个对象应当直接被拒。
桶策略配完之后还有一层值得做:配一下 Block Public Access。AWS 官方把公开访问阻断分成四项(BlockPublicAcls、IgnorePublicAcls、BlockPublicPolicy、RestrictPublicBuckets),图床桶的推荐组合是四项里策略相关的两项都关、ACL 相关的两项都开:
BlockPublicAcls: true、IgnorePublicAcls: true:挡住"顺手加个 public-read"这类操作,图床的公开读交给桶策略,不需要 ACLBlockPublicPolicy: false:这一项开着会拒绝带公开访问的桶策略,图床用不上RestrictPublicBuckets: false:这一项开着同样会挡掉带公开策略桶上的匿名请求,图床必须留着公开读,就不能开
改的方式是调标准 S3 的 PutPublicAccessBlock,四个布尔值一次提交,不用逐项改。RustFS 兼容性矩阵里 Public access block 是已测试覆盖的能力,这层保护用得上。

连不上先看 PicGo 的日志
图床这堆配置的问题几乎都能在日志里定位,不用靠猜。GUI 打开日志面板重新传一次,原始的 S3 报错会直接指向原因:报 404 找不到桶或对象,多数是 pathStyleAccess 没设成 true,或者 endpoint 填成了代理域名;报证书相关,是 rejectUnauthorized 与 CA 信任的问题;报 AccessDenied,要么桶策略没配公开读,要么凭证权限不够(只给 s3:PutObject 不给 s3:GetObject 也会是这个结果);签名错误配上 403,通常是 access key、secret key 填错,或者密钥里有特殊字符被 shell 转义掉。Core 用户可以命令行跑 picgo upload any.jpg,输出比 GUI 面板完整,报错堆栈也在。
从第三方图床迁过来的迁移账
已经在第三方图床(SM.MS、Imgur 之类)存了大量图片的话,迁移要算三笔账:
第一笔,旧链接的存活期。迁移期间新旧图床并存,旧链接继续有效,直到确认所有文章的图片引用都改完才考虑下线旧图床。批量改引用通常靠脚本扫数据库或静态文件里的旧域名。
第二笔,图片的搬运方式。第三方图床多数不提供 S3 接口,搬运要么手动下载再上传,要么写脚本抓图片 URL 下载后走 S3 上传。图片量大的话,用 mc mirror 不可行(源端不是 S3),得写个简单脚本循环下载上传,或者借助 rclone 的 HTTP 后端。这条路有几个边界要先知道:rclone 的 HTTP 后端是只读的 ,官方文档写明它只能读取 web 服务器给出的文件列表,不能当双向同步用,而且对方服务得真的吐目录列表才吃得下;不吐列表的图床,这条路径直接走不通。频率限制、防盗链、token 校验也压在这条链路上,批量抓取容易被限流甚至封禁。规模大的话把并发压下来再分批跑(--transfers 1、--tpslimit),指望一次跑完通常会被中途掐掉。
第三笔,URL 结构是否保留。如果第三方图床的 URL 结构能映射过来(比如按文件名),可以让新链接保持相同路径,迁移脚本一次性改域名;如果结构对不上,就得全量改文章里的引用。后者工作量大但一劳永逸,前者省事但留下了对旧结构的依赖。
搬运完成一定要做一轮校验。图片迁移里最常见的失败形式是无声的:少数几张图在中间环节损坏或只传了一半,页面上看起来是断链,排查时却要一张张点开。写个脚本对新桶做全量列举,逐个 HEAD 检查返回码和 Content-Length,和源端的大小对得上才算搬完。这一步的耗时通常只有搬运的几分之一,但能提前捞出绝大多数问题。
顺手可以在这轮迁移里加两个增量:一是给图床桶配上生命周期规则,比如临时目录 7 天过期,配合"截图先扔临时目录、定稿后再挪正式目录"的工作流;二是把 outputURLPattern 直接指向未来的 CDN 域名,即使现在没有 CDN,将来接了 CDN 之后文章里的链接不用再改一轮。
工作流层面再补一个实践:PicGo 支持自定义快捷键和上传后重命名,配一个"截图 → 自动上传 → 自动复制 Markdown 格式链接"的完整链路之后,写文章时截图和贴图的手感接近本地文件。这套链路真正运转起来之后,图床后端是哪家反而变得不重要,这也是自托管的好处之一:接口是标准的 S3,后端想换随时换。
图床这套配置一旦跑通,后面往图床桶里加 CDN、加缩略图处理都是增量的事。RustFS 以 Apache 2.0 许可开源,仓库在 github.com/rustfs/rustfs,S3 寻址风格的官方说明在 administration/protocols/s3。接口是标准的 S3,后端想换随时能换,这才是自托管图床最实在的收益。