闲言少叙,总而言之就是又折腾了 R2S,把之前用的原本 ImmortalWrt 换成了 YAOF 的 OpenWrt 系统,原因是集成的插件多,好用省心。按照惯例,记录下本次折腾过程中碰到的麻烦事。
这里选择的固件版本是23.05.5-sfs,因为这是最后一个支持 docker 的版本。为什么选择这个,自己动手安装不香吗?不香,有点麻烦,因为这个固件用的源中的软件和 ImmortalWrt 用的相比有挺多没有的。当然可以替换源,或者动手去网上找各种luci-*-all.ipk
的插件上传安装,但是说了麻烦,做减法比做加法容易,既然有集成好的用就是了,一味的追求系统版本没意义,只要用起来稳定,让人省心,那就是好的。
固件刷写注意事项:
- 刷入前:进行扩容,保证给软件安装预留充足空间,不用太多,增加 1G 足以。扩容方式;
- 刷入中:跟以前一样;
- 刷入后:启动进入系统后,不要启动 docker,磁盘分个区给 docker,这里我分了6G 空间足够用了。然后挂载分区到
/opt/docker
;
未提及的操作照旧即可。
顺便提一下这个固件内置的软件源:
src/gz openwrt_core https://mirrors.pku.edu.cn/openwrt/releases/23.05.5/targets/rockchip/armv8/packages
src/gz openwrt_base https://mirrors.pku.edu.cn/openwrt/releases/23.05.5/packages/aarch64_generic/base
src/gz openwrt_luci https://mirrors.pku.edu.cn/openwrt/releases/23.05.5/packages/aarch64_generic/luci
src/gz openwrt_packages https://mirrors.pku.edu.cn/openwrt/releases/23.05.5/packages/aarch64_generic/packages
src/gz openwrt_routing https://mirrors.pku.edu.cn/openwrt/releases/23.05.5/packages/aarch64_generic/routing
src/gz openwrt_telephony https://mirrors.pku.edu.cn/openwrt/releases/23.05.5/packages/aarch64_generic/telephony
如果要替换为 ImmortalWrt 的软件源,只需要按照上述格式,把地址换成https://downloads.immortalwrt.org/
。但是换源之后可能会有意想不到的问题,比如原固件的插件使用的依赖在原来的源中找得到,但是在新源可能找不到或者版本不可用。操作有风险,换源需谨慎。
Tip:sfs 的固件能够重置,当系统无法正常启动时,不要立马刷机,先尝试断电后插拔 TF 卡,然后重新上电长按reset
键,之后观察sys
指示灯的变化。当红灯快速闪烁表示正在重置中,慢闪表示系统正常启动中,长亮表示系统正常运行中。重置固件后虽然需要重新设置系统,但是 TF 卡上存储的文件不会丢失。
Tailscale
的作用就不必多说了,内网穿透神器。与之齐名的还有Zerotier
,为啥不用这个?因为这个设置起来费老鼻子劲了,很折腾,而且免费计划只支持最多3个网络➕10个设备,按说差不多够用了,但是感觉捉襟见肘。但Tailscale
的安装就非常方便,并且免费计划支持3个用户➕100个设备,大气多了,用不完,根本用不完😄。
在安装插件前,先到软件包中安装iptables-nft
,这个是必要的依赖。之后上luci-app-tailscale下载最新版就完事,记得下载的是luci-app-tailscale_*_all.ipk
,然后上传安装即可,启动后会自动设置好上网接口以及防火墙。入口在 OpenWrt 系统的服务
下面。
Tailscale
的使用也很简单,登录了相同账号的设备就能互相访问。这里有个关键的地方,当安装了Tailscale
的软路由为主路由模式时,在 OpenWrt 的 Tailscale 的高级设置
下把暴露子网配置好,这样内网中的所有设备即使没有安装Tailscale
也可以被访问到,在外网的设备只需要打开Tailscale
就能像在内网中一样访问。
注意:每个设备登录后是有时效的,默认有效期 3 个月,可以在控制台调整。如果你想不失效,就在控制台选中设备后点击Disable key expiry
。
再就是,当你想要他人加入你的tailnet
,有三种选择:
- 登录到 Tailscale 的控制台配置一个
Auth keys
,他人使用你这个 Auth keys 登录,即可将他的设备加入到你的tailnet
中。优点是,不用泄露你的密码;缺点是,Auth keys 最大有效期为 90 天。 - 他人正常登录其账号,你邀请对方的账号到你的
tailnet
中。邀请成功后,需要对方重新登录其账号,此时就能看到有若干选项,要求其选择一个tailnet
加入,那么选择你的即可。优点是,无有有效期限制;缺点是,占用一个用户的名额。 - 他人正常登录其账号,你在 Tailscale 的控制台选择一个设备分享给对方,对方接受要求后,你的设备就出现在了对方的
tailnet
中。
上面三种方式的不同点:
- 第 1 种,是他的设备加入到你的网络中,会占用你的设备限额,默认可以访问你账号下的所有设备,包括暴露出的子网,你也可以访问他的设备,可以编写
ACL
控制权限。用的是你的账号; - 第 2 种,是他的设备加入到你的网络中,会占用你的设备限额,并且会占用你的用户限额,默认可以访问你账号下的所有设备,包括暴露出的子网,你也可以访问他的设备,可以编写
ACL
控制权限。用的是他的账号; - 第 3 种,是你的设备加入他的网络中,会占用他的设备限额,他只能访问你分享的这个设备,即使此设备下有子网他也不能访问,但你分享的这个设备并不能访问他的网络,由于他并不在你的网络中,所以你无需也不能编写
ACL
控制权限。用的是他账号;
熟悉以上区别,选择合适的分享策略。
注意:Tailscale 通过 IPv6 直连速度就比较快,能保证正常使用,但如果以 IPv4 连接,若当前没有公网且网络 NAT 层数过多,则无法直连,就会走中转服务器,你懂的,那就非常慢了,慢到无法使用。
所以,一种方式就是家庭内网拨号时获取 IPv6 地址,运营商一般会下发,如果没有那就没得玩,而手机通过蜂窝数据上网一般也都是有 IPv6 的,两端都是 IPv6 就可以直连;另一种方式就是自建 Derp 中继服务器,但那就需要有一台具有公网 IP 的服务器,一般是买云服务器,要钱的方案不在白嫖党的选择范围。
官网,这是一个可以提供音乐分享的服务,我们可以在软路由上运行它来搭建自己的音乐私服,配合内网穿透后就可以无限制听音乐了。
注意:这仅仅是一个可以提供音乐分享的服务,它本身是没有任何音乐的,所有的音乐内容还是得自己去搞定,一劳永逸。
下载插件luci-app-navidrome并上传安装,入口在 OpenWrt 系统的服务
下面。
安装完成后先不要急着启动,去磁盘管理
分一个区用来存放音乐。分多大看个人情况,我没有其他需求,把剩余所有空间都分出来了。分区完后进行挂载,挂载路径随意,我这里是/opt/navidrome
。
在启动插件之前,设置好插件的各种目录,并确保这些目录都存在,否则启动会失败,之后正常启动浏览器访问操作即可。
Samba 协议可以用来在设备之间共享文件,Windows 和 Linux 系统都支持,不过需要做一些设置才能启用。
Linux
安装 Samba
在软件包中搜索下载shadow-useradd
、luci-app-samba4
和luci-i18n-samba4-zh-cn
,若已安装则忽略。安装完成后,入口在服务
->网络共享
。
创建 Samba 用户
useradd samba # 添加名为 samba 的用户
smbpasswd -a samba # 为用户 samba 创建 smb 服务的密码
mkdir -p /opt/navidrome/music # 创建一个目录用于共享(已存在则忽略)
# 使用户 samba 获得目录权限。这一步很重要,否则当使用 Samba 客户端连接时会提示:没有权限。
chown -R samba:samba /opt/navidrome/music # 或者执行 chmod -R 777 /opt/navidrome/music
# 如果创建的 samba 用户不想用了
smbpasswd -x samba # 删除用户 samba 的密码
userdel samba # 删除用户
# 相关命令
service samba4 start # 启动服务
service samba4 stop # 停止服务
service samba4 restart # 重启服务
service samba4 status # 服务状态
testparm -v # 配置文件检查
设置共享
在服务
->网络共享
的常规设置
下,接口选择lan
;允许旧协议可以选择性勾选,默认不用。
在共享目录
下需要设置的选项:
名称 | 路径 | 可浏览 | 允许用户 | 创建权限掩码 | 目录权限掩码 |
---|
music | /opt/navidrome/music | ✔ | samba | 0666 | 0777 |
保存并应用
,然后用命令重启 Samba 服务。
注意:当使用 ios 或 macos 进行 samba 交互时,会导致上传使用,此时需要勾选启用 macOS 兼容共享
就能正常上传了,但又会引发其他可能的问题,如在快捷指令中列出 samba 服务上的目录,就无数据返回,对此,我的解决方法是用下面 Alist 提供的接口请求即可。
Windows
打开控制面板
,在程序和功能
->启用或关闭 Windows 功能
中,勾选SMB 1.0/CIFS 文件共享支持
和SMB 直通
。
启用完成后,按Win+E
打开文件资源管理器,在地址栏输入\\192.168.0.1
双击music
目录后输入前面创建的账户/密码即可。
如此一来,就可能很方便的上传音乐了,搭配内网穿透,可以随时随地的操作。
可以使用 AList 将 smb 转接为 webdav 服务,这是最简单、方便且功能强大的方式。在软件包下搜索luci-i18n-alist-zh-cn
安装即可。之后的使用方式这里不多说,参考文档即可。
值得注意的是,AList 完全可以用过 api 的方式来操作,通过 http 的方式来请求,但默认情况下guest
用户是禁用的,这就导致我们所有的 api 操作都需要鉴权。有的时候仅仅是查看目录,此时可以将guest
用户启用即可,即使不给于任何权限,也是可以查看的。
有时候需要一个文件版本控制系统,即方便在多台主机上管理文件,也方便文件回溯。Git 协议的服务对于 R2S 来说可能比较占资源,不过 SVN 是非常轻量的。
在系统
->软件包
中搜索subversion
,可以看到三个包:subversion-client
、subversion-libs
、subversion-server
。其中 libs 包是其他两个的依赖包,如果只是作为服务端使用,安装subversion-server
即可,会自动安装subversion-libs
依赖包。安装完成后安装如下步骤来初始化:
创建 SVN 仓库
mkdir -p /srv/svn
cd /srv/svn
# 创建一个名为 repos 的仓库
svnadmin create repos
执行完后,目录结构类似:
配置 SVN 用户和权限
修改repos/conf/svnserve.conf
启用身份验证:
svnserve.conf
[general]
anon-access = none
auth-access = write
password-db = passwd
authz-db = authz
realm = R2S Repos
修改repos/conf/passwd
,添加用户名密码:
passwd
[users]
admin = admin
b560m = 123456
winmi = 123456
修改repos/conf/authz
,配置权限:
authz
[groups]
# 定义了两个组(名称随意),为组赋予了成员
admin = admin
dev = b560m,winmi
[/]
# * 表示匿名,* = r 表示匿名可以读(r)到 / 下的仓库
# 如果不想匿名看到所有就 * = ,不给任何权限
* = r
@admin = rw
# 因为仓库为 repos,所以一定要是 repos:<子路径>
[repos:/sub-store]
@dev = rw
[repos:/proxy-server]
@dev = rw
[repos:/assist-tool]
@dev = rw
通过这种配置,可以在启动一个 svn 服务的情况下,管理多个子目录,每个子目录即表示一个代码库。
重启 SVN 服务
修改/etc/config/subversion
的内容:
subversion
config subversion
option path '/srv/svn'
option port '3690'
注意,服务启动的路径在/srv/svn
,实际创建的仓库应该在其子目录下,然后重启 SVN 服务service subversion restart
。
客户端连接
注意,直接在服务器上的仓库下创建子目录是无效的,那不会被 svn 管理,正确的做法是由管理员先拉取整个仓库到本地,然后在本地创建好目录结构:
# 管理员拉取整个仓库
svn checkout svn://192.168.0.1/repos --username admin -- password admin
# 开发者拉取子目录
svn checkout svn://192.168.0.1/repos/sub-store --username winmi -- password 123456
之后可以add/commit/update/log
等常规操作了。
注意:上传到 SVN 的文件都在db
目录中,但不是原样存储,而是被压缩、编码、分块成数据库形式。如你想导出完整版本库的内容为一个普通文件夹,可以用:
svn export svn://192.168.0.1/repos
# 或者使用 dump,可以导出整个仓库,包含提交记录。用于崩溃恢复或服务器迁移
svnadmin dump /srv/svn/repos > ~/svn-repos.dump
# 然后在恢复的服务器上创建仓库
svnadmin create repos
# 接着恢复 dump
svnadmin load /srv/svn/repos < ~/svn-repos.dump
SVN 与 Git 不同点:
- svn 不像 git 那样通过一个
.gitignore
文件就可以忽略多个、多级的文件/目录,svn 只能手动设置忽略单个文件/目录,每次忽略其实是设置目录的属性,因此设置忽略的操作也是要提交的。 - svn 不像 git 在本地删除了跟踪的文件后提交即可同步删除远程文件,svn 必须要使用
svn delete
命令删除文件然后提交才可以。
当使用 svn 管理时,删除了多个文件如何批量提交呢?有两种方式:
方法一:使用如下命令:
svn status | grep '^!' | sed 's/^! *//' | while IFS= read -r file; do svn delete "$file"; done
svn commit -m "批量删除被本地手动删除的文件"
方法二:创建脚本sync-svn-delete.sh
以及 VSCode 的 task 文件.vscode/tasks.json
实现点击运行。
sync-svn-delete.sh
#!/bin/bash
# 自动同步本地被手动删除的文件到 SVN 仓库
cd trunk
echo "📁 正在扫描本地被删除的文件..."
deleted_files=$(svn status | grep '^!' | sed 's/^! *//')
if [ -z "$deleted_files" ]; then
echo "✅ 没有检测到需要同步删除的文件。"
exit 0
fi
echo "$deleted_files" | while IFS= read -r file; do
echo "➖ 删除:$file"
svn delete "$file"
done
echo "🚀 提交到 SVN..."
svn commit -m "同步删除本地手动删除的文件"
tasjs.json
{
"version": "2.0.0",
"tasks": [
{
"label": "同步 SVN 删除文件",
"type": "shell",
"command": "${workspaceFolder}/.vscode/sync-svn-delete.sh",
"problemMatcher": [],
"options": {
"shell": {
"executable": "C:\\Program Files\\Git\\bin\\bash.exe"
}
},
"presentation": {
"reveal": "always"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
当使用图形化客户端,比如 TortoiseSVN 检出时,默认会缓存首次输入的用户和密码,如果想要切换用户则需要删除C:\Users\<用户名>\AppData\Roaming\Subversion\auth
(Windows)或~/.subversion/auth
(Linux/MacOS),在下次操作 SVN 时就会要求输入用户/密码。
如果是用命令行操作可以:
svn checkout svn://192.168.0.1/repos --username admin --password admin --no-auth-cache --non-interactive --quiet
-no-auth-cache
表示不缓存密码,--non-interactive
避免交互式输入密码,适合脚本,--quiet
表示静默的不输出详情。
为了维护仓库代码的安全,应该有定时任务每天自动导出整个仓库代码,脚本如下:
点击查看代码
# -*- coding: utf-8 -*-
import sys
from pathlib import Path
import shutil
import subprocess
from datetime import datetime
import os
import re
import zipfile
TMP = '/tmp/svn-bak' # 临时文件目录
BAK = '/srv/share/代码库/svn' # 备份文件目录
LIMIT = 5 # 最大备份数量
def init():
# 删除临时目录(避免 checkout 失败)
if Path(TMP).exists():
shutil.rmtree(TMP)
# 创建目录,包括中间目录
Path(TMP).mkdir(parents=True, exist_ok=True)
Path(BAK).mkdir(parents=True, exist_ok=True)
def zip(source_dir, zip_path):
source_dir = os.path.abspath(source_dir) # 确保是绝对路径
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(source_dir):
for file in files:
abs_file_path = os.path.join(root, file)
rel_path = os.path.relpath(abs_file_path, os.path.dirname(source_dir))
zipf.write(abs_file_path, arcname=rel_path)
def main():
print(f"时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}|svn repos 仓库备份", file=sys.stderr)
cmd = [
"svn", "export", "svn://127.0.0.1/repos", f"{TMP}/repos",
"--username", "admin",
"--password", "admin",
"--non-interactive",
"--no-auth-cache",
"--quiet"
]
result = subprocess.run(cmd,capture_output=True,text=True)
if result.returncode != 0:
print('checkout failed: ' + result.stderr, file=sys.stderr)
sys.exit(-1)
# 压缩备份
zip(f"{TMP}/repos",f"{BAK}/reposbak-{datetime.now().strftime('%Y%m%d%H%M%S')}.zip")
pattern = re.compile(r"reposbak-\d{14}\.zip")
files = [f for f in os.listdir(BAK) if pattern.fullmatch(f)]
# 如果压缩包数量超过限制
if len(files) > LIMIT:
# 保留最近的时间
files.sort(reverse=True)
# 其余全部删除
for f in files[LIMIT:]:
os.remove(os.path.join(BAK, f))
# 删掉临时目录(因为不需要了)
shutil.rmtree(TMP)
# 0 2 * * * /usr/bin/python /usr/local/bin/svnrepo-backup.py >> /var/log/svnrepo-backup.log 2>&1
if __name__ == '__main__':
init()
main()