18 KiB
title | date | slug | featuredImage | categories | tags | series | summary | description | wikilinks | ||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
理想乡构筑手记(3):Hello,Nieve | 2025-02-21 | hello-nieve | https://img.viento.cc/cover/hello-nieve-cover.webp |
|
|
理想乡构筑手记 | 其实你一共也没有几张图片啊 | 本文介绍了作者构建名为 Nieve 的图片服务系统的过程。由于原先使用的缤纷云图床出现加载问题,博主决定迁移至又拍云作为主图床,并以 Cloudflare R2 作为备份。迁移过程中,使用了 `rclone` 工具进行图片备份和上传。为了优化 Obsidian 中的写作体验,博主弃用了原先的 Image Converter 插件,转而利用 Shell Commands 插件调用自定义脚本。这些脚本实现了图片粘贴时的自动压缩(使用 `cavif`)、重命名、相对路径计算,以及发布文章时查找图片、上传至又拍云(使用官方工具 `upx`)并替换链接的功能。整个过程涉及了多种工具,包括 Python 脚本、AppleScript、`ripgrep` 等,最终构建了一个自动化、高效的图片处理与发布流程。 | true |
诸位老友,上午好,这里是 Chlorine。
本期是「理想乡构筑手记」的第三篇,实际也是最早进行的一篇,主题是园子的图片服务系统——Nieve。本来是想放到新的一期周报里面讲的(没想到吧,园子的周报还活着),但是这部分内容实在是太琐碎了,遂单题一篇。水,你接着水。
Nieve 这个名字,也是很久以前就想好的。这个词是西班牙语,意思是「雪」。
TL ; DR
- 在 Obsidian 写作时粘贴图片前使用 Shell Commands 的自定义脚本压缩图片为 AVIF,并获取压缩后链接。
- 使用 Shell Commands 的另一个自定义脚本上传博客图片到又拍云和 Cloudflare R2
- 又拍云作为主图床,Cloudflare R2 作为对外的图片服务和备份图床
前言
作为 Vercel 上的静态博客,园子的图片当然使用的是图床。小氯使用的图床服务几经变易,从免费图床的良心之作 SMMS 到牢巴(Alibaba)的 OSS,再到缤纷云,期间甚至还写了Markdown图片管理实践去讲 Markdown 图片管理(当然那篇文章其实挺水的)。缤纷云提供了相当慷慨的免费额度,具体来说是:
- 前 50 GiB 存储
- 每月前 10*3 GB HTTP/HTTPS 流量(每日每项限 5 GB)
- S4 出口流量 10GB/月
- 内置 CDN 回源 S4 流量 10GB/月
- 内置 CDN 出口流量 10GB/月
- 每月前 10*3 万次请求(每日每项限 1 万次)
- S4 请求数 10 万次/月
- 内置 CDN 回源 S4 请求数 10 万次/月
- 内置 CDN 请求数 10 万次/月
回来。小氯使用缤纷云已经有快一年了,总体而言速度和稳定性还算可以,有了备案之后还可以用他家的 CDN,可以省一些回源流量之类的(没错,速度其实没快多少)。当然,为了避免免费服务的传统艺能——跑路。小氯也把图片在赛博活佛 Cloudflare 的 R2 上存了一份。和国内的各大对象存储以及 Amazon S3 等同行相比,Cloudflare 的免费额度简直多得不像话,一个月有 10G 的免费空间,1M 次的 A 类操作(存储和删除等),10M 次的 B 类操作(读取等),无限流量。如果不在意国内速度慢一点,那么 Cloudflare R2 堪称是对象存储中的椎间盘——为何你如此突出。
插一句话,小氯写到这里的时候,习惯性地希望使用中文,然后发现自己似乎不知道 Cloudflare 的中文名字……等等,Cloudflare 有中文名字吗?
这个还真难说。Cloudflare 的官网在调为简中后,还是叫作 Cloudflare;而 cloudflare-cn.com
使用的是「科赋锐」(注意:小氯不清楚这个网站和 Cloudflare 官方是否有关,请谨慎),说实话,这个名字……真的让小氯很不满意,就和把 Google 翻译为「谷歌」一样。小氯还没有找到一个被 Cloudflare 官方认可的中文名字,但是从社区来看,似乎有一个不错的选择:云帆。
「云」对应 cloud,由于「云计算」「云存储」这些词汇的广泛使用,所以用这个词代表相关的技术领域没什么问题;而「帆」是 flare 的音译(其原意为火焰)。这个词整体读起来还比较顺口,而且寓意也很好,「直挂云帆济沧海」。不过如果在正式的技术讨论里面,还是用 Cloudflare 为好。
回来。不过最近(其实离文章发出来已经是几个月之前了),小氯接到了一些老友的反映,博客的图片加载明显变慢了,甚至很多裂掉了(即无法加载)。直接用 URL 看一下,发现 403。奇怪的是,小氯并没有为这种情况加任何的访问限制,而图片也都好好地在那。而当我希望将新的自定义 URL 添加到 CDN 中时,也是一直提示「未备案」(实际上 ICP 和公安备案已经过了快一个月了)。这可不是什么好兆头,说明缤纷云的后端设施可能出了一些问题。此外,小氯发现自己的流量似乎也出了点差错,其用量比实际应该有的流量高。而其分布也比较均匀,不像是攻击(而且谁闲着没事去攻击小氯酱这条杂鱼啊)。
总而言之,种种因素作用吧,小氯打算换个图片服务了。
图片服务的选择
市面上的图片服务——准确来说,能直接或者间接作为图片服务的服务不胜枚举。虽然说小氯不介意花点小钱,但是如果是像流量费这种很可能让人倾家荡产的服务,小氯还是希望尽可能避免的。于是小氯开始收集各种有免费额度的服务,当然这里指的是国内的。我没备案用的是国外的图片服务,备案了用得还是国外的图片服务,那我这不是白备案了嘛。
具体过程不多说了,极其曲折。小氯甚至想过用服务器 + 一些 CDN 搭一个,但一是没有合适的服务器,二是这种方式相当不稳定。举个例子:杜老师的去不图床,可以说是博友圈最著名的自建图床了(甚至没有之一),也时常会出现许多奇奇怪怪的问题,小氯可不认为自己的运维能力和服务器集群的质量比杜老师强。所以还是老老实实地找对象存储去了。
几番搜索,小氯找到了一个看起来还可以的选择。这个家伙大家也都不陌生:又拍云(这个链接不带 AFF,放心点击)。
牢拍也算是小有名气的商家了,跑路的风险不大,而且也没有大到像套路云、凉心云那样令人讨厌的规模。此外牢拍有一个著名的 League,简单来说就是在自己网站底下挂上牢拍的 logo 可以持续领到代金券,均摊一下也就是每个月 10G 的空间和 15G 的流量,基本上够用一段时间了。而且牢拍的代金券是和账户而不是域名挂钩的,这意味着你只需要找一个备案过的域名挂一下,然后就可以随便用了。
那么……就是你了。
使用 rclone
备份图片
在转移到牢拍之前,我们当然需要把整个图片目录备份下来。这里小氯打算试一下新玩具 rclone。
rsync 咱都知道,一个有趣的文件传输(这里说的是上传、下载和同步)工具。rsync 的花样很多,甚至可以用它部署静态博客到服务器(这可能是最好的方案之一,不需要装 Gitea 等一堆东西)。rclone 大体可以理解为云存储版的 rsync,支持一大堆各种各样的云存储和云盘。
brew update && brew install rclone
先创建一下配置:
rclone config
下一步输入 n,新建一个配置,选择 S3 - 其他,把 Access Key ID 和 Secret Key ID 之类的参数扔进去就行。
配置好以后,运行:
rclone ls your-service-name:your-bucket
如果能输出你的桶目录结构那就配置成功了,可以下载了:
rclone copy your-service-name:your-bucket /your/path
完工。
又拍云的配置
申请又拍云联盟
略。注册之后在这里申请即可。一般来说审核会有 1 ~ 3 天,小氯的用了大概二十分钟,相当快。
创建存储服务
和缤纷云不同的是,牢拍的云存储自带 CDN,所以只创建一个存储服务即可。
使用 rclone
重新上传图片
USS 兼容 S3,这就意味着我们可以用我们熟悉的各种小道具去把玩 USS。这里为了方便,我们还是使用 rclone 吧。这里小氯踩了点坑,因此说得详细一些。
首先我们需要获得 USS 的 S3 兼容凭据。这可不是你那个操作员的操作凭据,需要在存储服务的控制界面 - 存储管理里面找这里:
把东西记好喽。
回到终端,创建一个 rclone 配置:
rclone config
输入 n 新建配置,名字随便起,这里我使用 test
作为演示。
下面依次跟随指示,键入以下配置。这里的数字是以 v1.69.0
为基础的,在键入前,请检查你的版本的相应配置对应于哪个数字:
Storage
:4(Amazon S3 及其兼容服务)provider
:34(其他 S3 兼容服务提供商)env_auth
:直接回车,使用默认配置即可。access_key_id
:你刚才获取的那个access_key_id
。secret_access_key
:还是刚才那个。region
:直接回车,使用默认配置即可。endpoint
:s3.api.upyun.com
location_constraint
:直接回车,使用默认配置即可。acl
:直接回车,使用默认配置即可。- 高级设置:直接回车,使用默认配置即可。
最后保存后,使用 rclone ls test:
即可测试是否成功配置。
Obsidian 配置
小氯在 Obsidian 的配置上花了很多的时间,终于找到了一个自己满意的方案。下面我把思路整理一下。
小氯的需求大概是:
- 图片重命名(语义化命名)
- 使用相对链接
- 自动压缩为 WebP 或者 AVIF
- 发布博客时上传到图床并替换博客文件的链接,而原本的文件保持不变
为了方便,我们就叫粘贴处理和发布处理好了。
粘贴处理
能实现一部分功能的 Obsidian 的图片插件有很多,例如 Paste Image Rename,Unique Attachments 等。不过小氯最喜欢的还是这个:
{{< github repo="xRyul/obsidian-image-converter" >}}
这个插件的功能非常全面,从压缩、格式转换、自定义附件位置到标注、裁剪、缩放、对齐都有。而作者也是位乐于听从社区意见的开发者,更新迭代和 Issue / PR 回复都很勤。小氯给这个插件提了个小小的 PR(改了一个拼写错误),混到了人生中第一个 Contributor 认证 (/ω\)
不过插件的功能虽多,但是小氯主要是用其中的压缩和自定义附件位置,因此整体的功能也有些冗余了。而且,其会和我常用的 Attachflow 插件冲突。至于压缩问题,Image Converter 插件在 1.3.7 版本加入了 AVIF 压缩功能,不过有一个问题:它异常耗内存,经常转换着转换着就内存不足崩掉了(虽然说 FFmpeg AVIF 这玩意本来就吃资源)。虽然可以回退到 WebP,但是总归不是最优的解决方案。
那看来只有发挥主观能动性了。那么纵观 Obsidian 的插件,能让小氯在这个意义上发挥主观能动性的插件似乎只有一个:
{{< github repo="Taitava/obsidian-shellcommands" >}}
Obsidian Shell Commands,我愿称之为 Ob 可玩性的 Top3 之一(另外两个小氯认为是 Local REST API 和 Quickadd)。这个插件的功能就是让你在 Obsidian 中调用系统的 Shell 执行命令,支持自定义变量、预输入和自动触发等。说到这里,大家应该也能想象到这个插件的可玩性有多强了。
而且,小氯本身也要用 Shell Commands,既然我们能少装一个插件,那么「如非必要,勿增实体」,自然是极好的。
首先,由于小氯的图片使用的是相对路径,因此需要先获取图片相对于当前笔记的路径。笔记路径有 {{file_path}}
变量,图片由于是我们自己规定的,因此也不难获得。问题在于路径的计算。macOS 没有现成的命令行工具计算相对路径,于是小氯写了个 Python 脚本:
#!/usr/bin/env python3
import os
import sys
import argparse
def get_relpath(base_dir, tar_file):
try:
relative_path = os.path.relpath(tar_file, base_dir)
return relative_path
except ValueError:
return None
def main():
parser = argparse.ArgumentParser(description="计算 target 相对于 base 的相对路径")
parser.add_argument("base", help="起始目录)")
parser.add_argument("target", help="目标文件")
args = parser.parse_args()
base_dir = os.path.abspath(args.base)
tar_file = os.path.abspath(args.target)
if not os.path.isabs(base_dir) or not os.path.isabs(tar_file):
print("Err: Please input absolute path.", file=sys.stderr)
sys.exit(1)
relative_path = get_relpath(base_dir, tar_file)
if relative_path is not None:
print(relative_path)
else:
print("Err: Please input correct path.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
然后赋予执行权限,移动到 ~/.local/bin
了事。
下面我们需要读取剪贴板的图片,这里由于是 macOS,我们使用最原生的 AppleScript 即可。
on run args
set outputFile to POSIX file (first item of args)
try
write (the clipboard as «class PNGf») to outputFile
return POSIX path of outputFile
on error
return "ERROR: 剪贴板中没有图片"
end try
end run
然后把图片存储到临时文件 TMPFILE="$(mktemp "/tmp/pasteboard-XXXXXX.jpg")"
中。当然不要忘了加一个 trap 自动清理。
然后我们需要获取新图片的名字。为了满足小氯的需求,我们加一个 Preaction,让我们可以自己输入文件名。
然后我们就要开始压缩了。小氯第一个想到的自然是古神 FFmpeg,不过大家也知道,FFmpeg 尊者是出了名地脾气古怪,只要你稍微不慎,就会让他老人家掀桌不干。小氯反反复复调了好几次,遇到的问题包括但是不限于:
- 压缩极慢。
- 都把输出重定向到
/dev/null
了,还是莫名其妙地冒日志。 - 好不容易能压缩了,结果透明度数据没了,告诉我 libaom-av1 不支持 YUVA 编码。
……好好好。
那我不用 FFmpeg 还不行吗?!
rustup update
cargo install cavif
这玩意也不是不能用,而且比 FFmpeg 快多了。
压缩之后,我们就可以把图片移动到对应的附件文件夹,并且向剪贴板写入我们的相对路径链接了。小氯也尝试过把数据写入剪贴板,但是 macOS 似乎不直接支持写入 AVIF。
完整版的脚本放在这里了,大家按需取用。
发布处理
由于小氯的 Hermeneutics 支持 Wikilink 和 Alert,所以我们只需要上传图片并替换即可。这里我们还是写脚本解决问题。
这个脚本比较简单,使用 ripgrep 查找图片,再通过文件的路径构建出图片路径,然后一个循环把图片送上去即可。小氯用了 Cloudflare R2 测试成功后就放心地把脚本归档了。直到小氯写理想乡构筑手记(2):Hello,Céfiro,希望调用脚本上传图片时,才发现:
你根本没在又拍云!你在哪呢?
图片并没有被上传到又拍云。于是,小氯开始了兵荒马乱的排查过程,最终在单独测试 rclone copyto
时发现了一大堆奇奇怪怪的报错,不是超时就是缺少 Key 或者各种万泉部诗人的奇怪报错。这可能是因为牢拍的存储空间不完全符合 AWS S3 的标准,所以说 rclone 没办法很好地兼容。
好好好。支持,但是不完全支持。
万般无奈之下小氯开始寻找替代方案,在把又拍云的文档翻了个底朝天之后,小氯终于找到了这个东西:
{{< github repo="upyun/upx" >}}
这个东西不能用 Homebrew 安装(或许小氯可以自己维护一个?),同时小氯的系统里已经有一个叫 upx
的家伙了(一个压缩可执行文件的工具,可以把 C++ 编译出的 ./hello-world
大小压缩一半并且使得其报错)。所以,我们采取一下自定义安装策略。
首先把东西下载下来:
wget https://collection.b0.upaiyun.com/softwares/upx/upx_0.4.8_darwin_arm64.tar.gz
然后解压缩(或者直接用命令行解压缩也行,看您方便),再移动到 PATH 里面:
sudo mv /path/to/upx /usr/local/bin/upyun
然后验证一下就可以了。
这个命令行工具不支持使用文件验证,所以我们把凭据写到环境变量里面就好。
alias upyun="/usr/local/bin/upyun"
upyun login {{_UPYUN_SERVICE_NAME}} {{_UPYUN_OPERATOR}} {{_UPYUN_SECRET}}
然后上传完成退出就好了。
这个方法非常别扭,但是没办法,这是目前小氯找到的唯一一个能跑起来的方法了。
同样的,老友们可以自取脚本。
Shell Commands 真的是太强大了,小氯写了若干个脚本,几乎完全替代了 Image Converter、Git 等插件。如果再配合一下 Local REST API 和 Obsidian URI,大概可以把目前小氯的大部分插件都替代掉了。
后记
又清掉了一篇草稿,好耶 ~(∠・ω< )⌒☆