夸克网盘里存了一批电影,平时只能在手机上点开看。盒子放在电视旁边、接着 1.8T 外接硬盘,硬件条件其实比手机好得多——问题是网盘内容在云端,盒子并不会「自动」拥有它。想让它像本地文件一样被播放器、被媒体库读取,需要在宿主机上把 WebDAV 挂载成一个目录。
设备是 RK3588(ztl A588,Ubuntu 24.04 arm64,主机名 dcztl)。OpenAlist 原本就以容器方式跑着,这次没有修改它的容器、Compose、存储和端口,也没有重建或重启该容器。新增的只是一层宿主机侧的只读挂载。
目标形态
- WebDAV 服务:
http://127.0.0.1:8081/dav/ - 网盘源目录:
/夸克网盘/电影/要看的电影 - 宿主机挂载目录:
/mnt/alist-quark-movies - 挂载方式:只读,目录 0755 / 文件 0444,
allow-other,宿主机其他普通用户可读
WebDAV 与网页共用 8081 端口,没有另开 5245。镜像暴露 5245 不代表真的有 WebDAV 服务在监听它,别照着镜像的端口表去排查。
为什么不用 Docker,而是用原生 systemd
这是本次部署唯一需要提前想清楚的设计选择。挂载网盘这件事,有两条路:在容器里挂,或者在宿主机上挂。
我选了后者,用一个原生 systemd 单元 alist-quark-movies.service 来管理 rclone 的 FUSE 挂载。原因是这样能避开一堆容器化的麻烦:不需要给容器特权,不用折腾共享挂载传播,也不会把「容器消费路径」的问题引入进来。它本质上是宿主机文件系统的一次客户端挂载,而不是新增一个网络服务。原 OpenAlist 继续由 Docker Compose 管理,两边互不干扰。
完整的单元文件如下(系统里的路径是 /etc/systemd/system/alist-quark-movies.service,源码副本在 /root/AI/alist-webdav/alist-quark-movies.service)。ExecStartPre 先确认凭据文件可读;Type=notify 表示等 FUSE 挂载真正建立才算启动成功;ExecStop 用 fusermount3 -u 卸载;CPUAffinity=0 1 2 3、MemoryMax=512M、LimitNOFILE=65535 就是上一节表格里那组资源约束:
[Unit]
Description=OpenAlist Quark movies read-only WebDAV mount
After=network.target docker.service
StartLimitIntervalSec=0
[Service]
Type=notify
ExecStartPre=/usr/bin/test -r /etc/rclone-alist/rclone.conf
ExecStart=/usr/bin/rclone mount "alist:/夸克网盘/电影/要看的电影" /mnt/alist-quark-movies --config=/etc/rclone-alist/rclone.conf --read-only --allow-other --default-permissions --dir-perms=0755 --file-perms=0444 --umask=022 --vfs-cache-mode=off --buffer-size=8M --vfs-read-chunk-size=8M --vfs-read-chunk-size-limit=32M --dir-cache-time=5m --poll-interval=0 --contimeout=10s --timeout=30s --retries=2 --low-level-retries=2 --log-level=NOTICE
ExecStop=/usr/bin/fusermount3 -u /mnt/alist-quark-movies
SuccessExitStatus=143
Restart=on-failure
RestartSec=15s
TimeoutStartSec=45s
TimeoutStopSec=30s
CPUAffinity=0 1 2 3
LimitNOFILE=65535
MemoryMax=512M
NoNewPrivileges=true
UMask=0077
[Install]
WantedBy=multi-user.target
这个挂载没有新增 USB bind、也不写外盘;OpenAlist 原有的 USB 依赖仍由 usb-storage-guard 负责。它的暂停/自动恢复设置没有被改动,也没有给 Docker 或开机流程添加 USB 挂载依赖。
环境与前提
| 项 | 值 |
|---|---|
| 挂载工具 | Ubuntu 安全更新仓库中的 rclone 1.60.1+dfsg-3ubuntu0.24.04.6(命令显示 v1.60.1-DEV) |
| FUSE | FUSE3 3.14.0(既有安装) |
| 服务单元 | alist-quark-movies.service,enabled / active,异常退出自动重启 |
| 资源约束 | CPU0–3(现场 MIDR 0x412fd050,即 Cortex-A55 小核)、MemoryMax512MiB、LimitNOFILE65535 |
| 凭据文件 | /etc/rclone-alist/rclone.conf,目录 700、文件 root 600 |
这道部署还有一个前提:OpenAlist 侧必须已经开启并可用 WebDAV,地址就是上面的回环地址。挂载前先确认它本身能用,否则后面所有报错都会指向错误的方向。
缓存与只读策略
直接照抄网盘客户端的默认参数会带来一个问题:它可能把整部电影缓存到本地磁盘,几十 GB 的空间就这么被吃掉了。所以这里做了明确取舍:
- VFS cache 关闭:不落盘缓存整部电影;
- 每打开一个文件,内存缓冲 8 MiB,按 8 至 32 MiB 的范围分段读取;
- 目录缓存 5 分钟,不主动轮询云端。
这意味着它不是离线副本。断网、或者 OpenAlist 不可用时,目录列表和文件读取都会失败;网络恢复后再次访问会重新发起请求。别把它当成一份备份。
只读这一点也是刻意设计的:允许别处通过这个挂载上传、删除或修改网盘内容,风险太大。挂载以只读方式提供内容,写入路径不开放。
使用与管理
在设备终端里的常用命令:
ls -lah /mnt/alist-quark-movies
findmnt -T /mnt/alist-quark-movies
systemctl status alist-quark-movies.service --no-pager
journalctl -u alist-quark-movies.service -n 30 --no-pager
systemctl restart alist-quark-movies.service
/root/AI/manage-services.sh服务管理菜单里新增了一项「夸克电影 WebDAV 挂载」,可以独立停止/关闭自启,或启动/开启自启。这两类操作都不会停止 OpenAlist,也不会删除电影。
这一项菜单来自下面这份清单条目(/root/AI/alist-webdav/catalog-entry.json)。还原脚本只把它合并进本机服务清单,不覆盖整份清单:
{
"id": "alist-webdav",
"name": "夸克电影 WebDAV 挂载",
"kind": "systemd",
"units": [
"alist-quark-movies.service"
],
"warning": "停止将卸载 /mnt/alist-quark-movies,中断网盘媒体读取;不停止 OpenAlist、不删除网盘文件。"
}
挂载目录可以直接作为 Jellyfin 原生服务的媒体库路径。不过本次没有添加媒体库,也没有更改 SMB 或任何既有媒体目录——留给你自己决定怎么组织片库。
Docker 内的应用默认看不到这个本地挂载。需要额外评估只读 bind,以及挂载传播和服务启动顺序;不要直接把一个空目录 bind 进容器然后宣称它能读——那只会得到一个空的媒体库。
卸载时的注意点
挂载上还有正在打开的文件时,卸载可能返回 busy。请先停掉播放器或扫描任务,不要强制卸载。这一点不是理论担忧:测试期间就因为目录正在被扫描,真实遇到过一次 busy,确认没有占用后才正常卸载恢复。
两种故障要分清
Type=notify只表示 FUSE 挂载建立成功,不代表上游网盘可用;- 已配置有限的 HTTP 连接 10 秒、请求 30 秒以及重试,但整个应用调用可能跨多个请求,所以不存在严格的 30 秒端到端上限;
- 目录内容更新最多受 5 分钟缓存影响,可以在没有客户端占用时重启挂载来刷新。
凭据管理
配置在 /etc/rclone-alist/rclone.conf,目录 700、文件 root 600。要特别强调一点:rclone 配置里的 pass 只是混淆,不是加密。所以这个配置和它的就地备份都不能分享,不能进入 Git,也不能放进普通归档包。
这个配置的结构如下(user 与 pass 是文件里真实存在的字段,这里用占位符代替真实值):
[alist]
type = webdav
url = http://127.0.0.1:8081/dav/
vendor = other
user = <WebDAV 用户名>
pass = <rclone obscure 生成的口令>
pass 不能直接填明文,要用 rclone obscure 把口令从标准输入转换后写入——下面内联的 setup-credentials.sh 里用的就是 printf '%s' "$password" | rclone obscure -,用连字符让 rclone 读 STDIN,这样口令不会出现在命令行参数和 shell 历史里。type 固定是 webdav,vendor 用 other;[alist] 这个段名就是服务单元 ExecStart 里 alist: 对应的远程名,url 指向回环的 /dav/。
需要更换账号密码时:
/root/AI/alist-webdav/setup-credentials.sh
systemctl restart alist-quark-movies.service脚本采用交互输入密码,不把密码放进命令行参数,修改前会保留同目录的私密备份。不要把该目录同步进公开代码或备份包。
脚本全文如下。它只接受本机回环 HTTP 或 HTTPS 地址,用户名与口令都交互输入;写入前会给原配置做同目录的私密备份,用 umask 077 加临时文件 mv -T 落盘,最终权限 root 600。脚本里有两处 <WebDAV 用户名> 占位符——一处是交互提示里的默认值、一处是直接回车时的兜底值,原来写在那里的实际默认用户名已脱敏,使用前请按自己的账号填回:
#!/usr/bin/env bash
# 交互式保存凭据,不把口令放进命令参数或普通归档。
set -euo pipefail
[[ $EUID -eq 0 ]] || { echo '请使用 root 执行。' >&2; exit 1; }
command -v rclone >/dev/null
config_dir=/etc/rclone-alist
config_file="$config_dir/rclone.conf"
read -r -p 'WebDAV 地址 [http://127.0.0.1:8081/dav/]: ' url
url=${url:-http://127.0.0.1:8081/dav/}
[[ "$url" == http://127.0.0.1:8081/dav/ || "$url" == https://* ]] || { echo '仅接受本机回环 HTTP 或 HTTPS 地址。' >&2; exit 1; }
read -r -p 'WebDAV 用户名 [<WebDAV 用户名>]: ' user
user=${user:-<WebDAV 用户名>}
[[ "$url$user" != *$'\n'* && "$url$user" != *$'\r'* ]] || exit 1
read -r -s -p 'WebDAV 密码: ' password
printf '\n'
[[ -n "$password" ]] || { echo '密码不能为空。' >&2; exit 1; }
umask 077
install -d -m 700 "$config_dir"
if [[ -e "$config_file" ]]; then
[[ -f "$config_file" && ! -L "$config_file" ]] || exit 1
cp -a "$config_file" "$config_file.bak.$(date +%Y%m%d-%H%M%S)"
fi
obscured=$(printf '%s' "$password" | rclone obscure -)
unset password
tmp=$(mktemp "$config_dir/.rclone.conf.XXXXXX")
trap 'rm -f -- "$tmp"' EXIT
printf '[alist]\ntype = webdav\nurl = %s\nvendor = other\nuser = %s\npass = %s\n' "$url" "$user" "$obscured" > "$tmp"
chmod 600 "$tmp"
# 固定目标经上方显式校验,不允许覆盖符号链接。
mv -T -- "$tmp" "$config_file"
echo '凭据已保存(root 600);rclone 混淆并非加密,请勿分享该文件或其备份。'
echo '如服务已经运行,请执行 systemctl restart alist-quark-movies.service。'
协议选择上:HTTP 仅用于
127.0.0.1回环;如果要用远程地址,必须是 HTTPS。
验证记录
- 不认证访问
/dav/返回 401;提供账号后 PROPFIND 成功,中文目录显示正确。 - FUSE 真实挂载,属性为
ro、nosuid、nodev;65 个顶层条目可正常列出。 - 字幕文件读取 4096 字节成功;电影 MKV 读取开头 4096 字节、以及 1 MiB 位置跳转读取 4096 字节均成功。
- 普通用户
nobody可以读取元数据;只读挂载拒绝新建文件,测试未向云端创建任何内容。 - 服务重启后仍为 enabled / active,目录可读,全部线程落在 CPU0–3。
- 配置权限 root 600,systemd 语法校验通过,管理器 21 项隔离测试回归通过。
明确的未验证项也要写清楚:没有整机重启,没有让 OpenAlist 真实停服或拔盘,没有做长时间/完整 4K 播放、多并发吞吐或真实网络断开验证。本次只是分段读取成功,不等于完整播放通过。
过程中的状态变化
这个部署不是一步到位的,中间经历过两次调整,记录在这里避免误解:
- 曾按要求取消挂载:执行
systemctl disable --now alist-quark-movies.service后服务 inactive/dead、MainPID=0、开机自启关闭,内核挂载表里已无/mnt/alist-quark-movies。OpenAlist 容器仍运行,端口和云端文件未改,服务文件、root 私密凭据、空挂载目录及归档都保留了下来。 - 随后改为只挂载指定子目录:复用同一个挂载点,仍为只读,服务恢复 active/enabled;仅确认子目录列表可读,当前是 1 个 ISO 文件,没有读取影片内容、播放、扫描或再次重启服务。CPU0–3 与原缓存设置保持不变。
因此原「取消挂载」记录属于历史状态,归档中的服务单元也已更新为子目录版本,不再挂载整个电影目录。
归档与还原
归档在 /root/AI/backup/alist-webdav/,包含 README、restore.sh、files/ 绝对路径镜像和 MANIFEST.sha256;源码有独立 Git 仓库 /root/AI/alist-webdav。归档中不含实际的 rclone.conf、密码、Token、电影或业务数据库。
还原顺序是先恢复 OpenAlist、原存储和 USB 守护,确认 8081 的 /dav/ 认证可用,然后:
cd /root/AI/backup/alist-webdav
sha256sum -c MANIFEST.sha256
./restore.sh
./restore.sh --apply
/root/AI/alist-webdav/setup-credentials.sh
systemctl enable --now alist-quark-movies.service注意几点:还原脚本不会自动启动服务,只合并自己的菜单项、不覆盖整个服务清单;遇到已有且不同的配置会拒绝覆盖,须人工审阅;默认预演不会安装依赖或写入系统。另外,重装前请把归档复制到设备之外——放在 eMMC 上的归档不构成异地备份。
需要回滚时:先退出播放器,然后 systemctl disable --now alist-quark-movies.service;挂载目录和云端电影保留,凭据按实际安全需求处理,不递归删除数据。