Aria2c的图形化界面部署(以AriaNg为例)

之前提到过(详见之前这篇文章)Aria2具有JSON-RPC/XML-RPC操控接口,因此大多数图形化界面都是以此接口开发的。在这里以AriaNg为例,介绍图形化界面的部署。

AriaNg是什么

AriaNg 是一个让 aria2 更容易使用的现代 Web 前端. AriaNg 使用纯 html & javascript 开发, 所以其不需要任何编译器或运行环境. 您只要将 AriaNg 放在您的 Web 服务器里并在浏览器中打开即可使用. AriaNg 使用响应式布局, 支持各种计算机或移动设备.

引自其官方仓库 https://github.com/mayswind/AriaNg

官方并没解释Ng是什么意思,个人猜测可能是Next Generation?

AriaNg的安装

AriaNg作为一个纯HTML与Javascript语言的静态网页型应用,自然部署不是什么特别大的问题。预编译包提供标准版本(分为index.html与一系列css、js文件)和单文件版本(所有css,js,图片文件全部写进index.html)。单文件版本适合本地使用,而标准版本适合服务器部署。难点可能要数如何控制AriaNg与Aria2c的交互。

1. 配置网页服务器

此处提供一个Nginx的范例,可供局域网内使用。

server {
        listen 80;
        listen [::]:80;
        server_name _;

        location / {
                root /var/www/aria2;
                index index.html;
                try_files $uri $uri/ =404;
        }
}

此外也可以选择使用OSS+CDN托管,作为公共服务。

2. 代理Aria2c的RPC端口

如果只是在局域网里使用,只要放开aria2c默认的6800端口上的防火墙就可以了。但是如果只是在局域网里使用,那aria2c相对其他软件的优势也就体现不出来了(特指某迅雷)。因此肯定要将aria2c的RPC端口开放到公网,然后用aria2c的token保护这个端口,或者用些其他的方法。

不过好在XML-RPC也好,JSON-RPC也好,都是建立在HTTP应用层之上的。也就是说可以把它当成普通的Web服务端口,用各类网页服务器做反向代理。下面提供一个反向代理的例子:

server {
        listen 80;
        listen [::]:80;
        server_name _;

        location =/jsonrpc {
                # 这里填写正确的反向代理位置
                # 假设你内网穿透到公网服务器的6800端口上
                proxy_pass              http://localhost:6800/jsonrpc;
                proxy_redirect          off;
                proxy_set_header        X-Real-IP       $remote_addr;
                proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header        Host            $host;
                # 使Nginx支持WebSocket反向代理
                proxy_http_version 1.1;
                proxy_set_header        Upgrade         $http_upgrade;
                proxy_set_header        Connection      "upgrade";
        }

        location / {
                return 404;
        }
}

那么到这里有些同志会问,直接防火墙开放公网服务器的6800端口不就好了吗?需要注意的是,默认情况下aria2c的RPC端口是HTTP协议的,即明文传输。如果需要aria2c方面开启SSL,需要在aria2c.conf中配置SSL证书。这么配置证书,难倒是不难;就是通常免费的SSL证书有效期有限,你总是要去换证书,而换证书这个操作,要自动化得要费点力气。

使用Nginx等网页服务器配置的好处在于,当你写完HTTP反向代理的配置文件之后,并且做完域名解析,使用certbot一键就能开启SSL并将所有非SSL流量重定向到SSL上(当然视情况可能需要按三次回车键吧),证书的更新将由后台的服务自动更新。如此一来,已经没有什么好怕的了。

3. 正确设置AriaNg连接参数

当使用 certbot 将所有流量都引导至SSL之后,Aria2 RPC 的地址中协议要设置为 https 或者 wss ,端口要设置为443。通常还要设置 Aria2 RPC 密钥为aria2c.conf中的 rpc-secret 项所对应的值 。

需要注意的是HTTP下的AriaNg可以用SSL上的RPC;而HTTPS下的AriaNg,如果不使用SSL上的RPC则会被视为混合内容,通常会被浏览器阻止。

至此Aria2c的图形化界面就算完成了
■QED

Aria2c的部署

什么是Aria2c

aria2 是一个轻量级的多协议多源的命令行下载工具。它支持 HTTP/HTTPS、FTP、SFTP、BitTorrent和 Metalink。aria2可以介由内置的JSON-RPC和XML-RPC接口来操控

官网自述 https://aria2.github.io/

最大的亮点应该是可以通过JSON-RPC/XML-RPC进行交互,其XML-RPC接口可以通过Python的xml.client标准库进行交互。

安装Aria2c

对于Linux来讲,可以简单地通过apt包管理器进行安装,包名为“aria2”。版本比较新的操作系统,软件仓库里的版本会比较高,例如Ubuntu,GitHub仓库里的版本号已经达到1.34.0的时候,16.04 LTS仓库里的是1.19.0;18.04 LTS仓库里的是1.33.1。
而对于Windows而言,首先当然可以下载源码,编译出aria2c.exe,但aria2的GitHub仓库里已经有了预编译好的发行版本,怎么方便怎么弄就行。

有了aria2c可执行文件之后,Linux相对比较好配置,可以写一个systemctl服务文件,或者其他的传统艺能,比如supervisor之类的。难点在于Windows的配置,接下来主要解释Windows下的配置。

1. 启动脚本

说是脚本,其实就是一行命令:

aria2c.exe --conf=./aria2c.conf

2. 完善目录结构

可以看到启动脚本里有一个 aria2c.conf 的文件,同样可以发现GitHub上下载下来的预编译包里没有这个文件,只有一个孤零零的aria2c.exe(除去一些readme和版权声明之类的东西)。
因此首先需要新建一个名为 aria2c.conf 的配置文件,一个网上随处可见的典型的配置文件如下所示,需要注意一下高亮的部分,那些行一定要记得写:

#################
## 文件保存相关 ##
#################

# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
dir=d:/aria2/

# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
disk-cache=32M

# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none < falloc ? trunc < prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
file-allocation=falloc

# 断点续传
continue=true

#################
## 下载连接相关 ##
#################

# 最大同时下载任务数, 运行时可修改, 默认:5
#max-concurrent-downloads=5

# 同一服务器连接数, 添加时可指定, 默认:1
max-connection-per-server=5

# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
min-split-size=10M

# 单个任务最大线程数, 添加时可指定, 默认:5
#split=5

# 整体下载速度限制, 运行时可修改, 默认:0
#max-overall-download-limit=0

# 单个任务下载速度限制, 默认:0
#max-download-limit=0

# 整体上传速度限制, 运行时可修改, 默认:0
#max-overall-upload-limit=0

# 单个任务上传速度限制, 默认:0
#max-upload-limit=0

# 禁用IPv6, 默认:false
#disable-ipv6=true

# 连接超时时间, 默认:60
#timeout=60

# 最大重试次数, 设置为0表示不限制重试次数, 默认:5
#max-tries=5

# 设置重试等待的秒数, 默认:0
#retry-wait=0


#################
## 进度保存相关 ##
#################


# 从会话文件中读取下载任务
input-file=./aria2.session

# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
save-session=./aria2.session

# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=60

################
## RPC相关设置 ##
################

# 启用RPC, 默认:false
enable-rpc=true

# 允许所有来源, 默认:false
rpc-allow-origin-all=true

# 允许非外部访问, 默认:false
rpc-listen-all=true

# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
event-poll=select

# RPC监听端口, 端口被占用时可以修改, 默认:6800
#rpc-listen-port=6800

# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
rpc-secret=hello

# 设置的RPC访问用户名, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-user=<USER>

# 设置的RPC访问密码, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-passwd=<PASSWD>

# 是否启用 RPC 服务的 SSL/TLS 加密,
# 启用加密后 RPC 服务需要使用 https 或者 wss 协议连接
#rpc-secure=true

# 在 RPC 服务中启用 SSL/TLS 加密时的证书文件,
# 使用 PEM 格式时,您必须通过 --rpc-private-key 指定私钥
#rpc-certificate=/path/to/certificate.pem

# 在 RPC 服务中启用 SSL/TLS 加密时的私钥文件
#rpc-private-key=/path/to/certificate.key

###################
## BT/PT下载相关 ##
###################

# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
#follow-torrent=true

# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=51413

# 单个种子最大连接数, 默认:55
#bt-max-peers=55

# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true

# 打开IPv6 DHT功能, PT需要禁用
#enable-dht6=false

# DHT网络监听端口, 默认:6881-6999
#dht-listen-port=6881-6999

# 本地节点查找, PT需要禁用, 默认:false
#bt-enable-lpd=false

# 种子交换, PT需要禁用, 默认:true
enable-peer-exchange=true

# 每个种子限速, 对少种的PT很有用, 默认:50K
#bt-request-peer-speed-limit=50K

# 客户端伪装, PT需要
#peer-id-prefix=-TR2770-
#user-agent=Transmission/2.77

# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1

# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false

# BT校验相关, 默认:true
#bt-hash-check-seed=true

# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=false

# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=true

daemon=true

可以看到配置文件里面有一个名叫aria2.session的文件,如果不先创建一个空文件,它会提示找不到而拒绝启动,因此,需要稍微帮它一下。

3. 自启动

可以说这是最麻烦的地方,有些同志把启动脚本写成批处理文件,然后在开机自启动的文件夹(具体而言在%appdata%\Roaming\Microsoft\Windows\Start Menu\Programs\Startup)做一个启动脚本的快捷方式;当然又有些同志觉得一个黑框框挺丑的,所以给写了一个隐藏启动vbs脚本套子,比如说:

CreateObject("WScript.Shell").Run "aria2c.exe --conf-path=aria2.conf",0

也不是说这些同志的方案不好,但对于Windows服务器而言,这要求每次服务器开机时要登陆一下账户,然后才会执行自启动文件夹里的文件。因此创建一个Windows服务的显然是更好的选择,比如说:

> sc create Aria2 binpath= "path/to/aria2c.exe --conf=path/to/aria2c.conf" type= share start= auto displayname= "Aria2 下载服务" 

但难点在于直到本文撰写时,版本1.34.0,仍然会遇到无法后台运行,导致服务启动失败,而且配置文件中session文件的路径也要写绝对路径。
因此就有了一个曲线救国的方案:【任务计划程序】(一个大家总是无视其存在的东西)
配置要点如下:

  • 常规
    • 安全选项【不管用户是否登录都要运行】
  • 触发器
    • 启动时 – 在系统启动时
  • 操作
    • 启动程序
      • 程序【aria2c.exe】
      • 添加参数【–conf=aria2c.conf】
      • 起始于【aria2c.exe所在路径】
  • 设置
    • 如果任务失败,按一下频率重新启动【按需勾选】
    • 如果任务运行时间超过以下时间,停止任务【不勾选】

到此为止Aria2在Windows上的部署就已完成
■QED

Ubuntu 配置自动更新

主要编译自Ubuntu官方参考手册

https://help.ubuntu.com/lts/serverguide/automatic-updates.html.en

软件包 unattended-upgrades 可以用来自动安装软件包的更新,并且其能配置为自动更新所有软件包,或者仅安装软件包的安全性升级。首先在终端输入下列命令以安装该软件包。

sudo apt install unattended-upgrades

译注:通常而言unattended-upgrades软件包是默认安装的。

通过编辑 /etc/apt/apt.conf.d/50unattended-upgrades 文件,可以配置unattended-upgrades软件包以达到你的需求。

Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
//      "${distro_id}:${distro_codename}-updates";
//      "${distro_id}:${distro_codename}-proposed";
//      "${distro_id}:${distro_codename}-backports";
};

特定的软件包也可以通过加入黑名单来避免其自动更新。将软件包的名字加入下方列表以阻止其自动更新。

Unattended-Upgrade::Package-Blacklist {
//      "vim";
//      "libc6";
//      "libc6-dev";
//      "libc6-i686";
};

注意:双斜线“//”会被当做注释,因此任何在双斜线后的语句都将不会被计算。

译注:上两个配置文件片段都是默认情况下的配置,即自动更新主线(即“”)安全升级(即“security”)频道中需要更新的软件包,同时不阻止任何软件包的自动更新。根据实际需要可以放开对更新(即“updates”)频道的更新;而建议(即“proposed”)频道,则不建议放开。

要启用自动更新,可以编辑 /etc/apt/apt.conf.d/20auto-upgrades 文件,并设置正确的 apt 配置参数。

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

上述的配置将会每天更新软件包列表,下载并安装可用的更新。本地下载的压缩包将会每周清理一次。 根据于你的选择,在一些升级到更新一些版本的Ubuntu服务器上,上述文件也许不存在。如果不存在,创建一个这个名字的新文件也应当可行。

注意:你可以通过阅读  /etc/cron.daily/apt 脚本的头部内容来了解更多关于 apt 周期性配置的选项。

译注:虽然手册上标了个“注意”,说要去 /etc/cron.daily/apt 的头部查阅,实际上对于Ubuntu≥18.04,你得去 /usr/lib/apt/apt.systemd.daily 的头部查阅更多的配置选项。

unattended-upgrades 的运行结果将会记录到 /var/log/unattended-upgrades 目录中。

通过配置 /etc/apt/apt.conf.d/50unattended-upgrades 文件中的 Unattended-Upgrade::Mail 项将会使 unattended-upgrades 向管理员发送邮件,报告任何需要升级的,或者存在问题的软件包。

另一个有用的的软件包是 apticronapticron 会配置一个 cron 任务通过邮件来通知管理员在系统中任何有可用升级的软件包,并包含每个软件包升级变更的总结。

要安装 apticron 软件包,在终端输入:

sudo apt install apticron

当软件包安装完成后,编辑 /etc/apticron/apticron.conf 文件,设置邮箱地址以及其他选项:

EMAIL="root@example.com"

一种基于OSS的静态网站工作流

标题有点微妙对吧,涉及的东西太多了我也不太确定怎么拟标题。

架构

接下来围绕几个问题做一点讨论:

1.为什么要海外VPS

因为drone ci需要拉取docker镜像,而国内访问Docker Hub存在一些小小但却令人抓狂的问题。当然如果本地访问Docker Hub一点问题都没有,那gogs和drone ci部署在本地就行了。此外需要注意一个海外VPS是否能正确连通国内的对象储存Endpoint,否则需要配置海外对象储存Endpoint,此举可能导致费用上升(虽然5GB以内倒是费用下降了)。

2.为什么要Gogs

现在Github的私有仓库不设数量限制,当然用GitHub是非常好的,数据将会更安全(从数据可用性意义上)。但因为历史遗留问题,以及个人偏好(我比较喜欢把自己的数据放在自己枕边),选了gogs

3.为什么要对象储存

对象储存是按量计费的,通常每GB每月费用不超过1¥,相比搞一个轻量型云服务器,或者虚拟主机之类,其成本几乎可以忽略不计。另一方面对象储存允许配置为静态网站模式,为这个工作流提供了可能性。

4.为什么要内容分发网络(CDN)

注意,首先假设使用的是国内的对象储存Endpoint。
不使用CDN的话,首先需要配置SSL证书托管,否则直接用Bucket.Endpoint访问首页倒是可以用HTTPS,但浏览器行为会变成下载网页,同时流量费用是属于外网流出(一般而言应该用忙时的价格)0.5¥/GB,请求费用是0.01¥/万次
而使用CDN,首先SSL证书可以配置为自动免费证书,同时流量费用分两个部分:CDN回源流量0.15¥/GB+CDN按流量付费0.24¥/GB≤0.39¥/GB,请求费用是0.05¥/万次(自动SSL证书的代价吗😭)

假设每次请求是 x GB/req,CDN部分流量费按上界计,不难根据不等式0.5+0.01/x ≤ 0.39+0.05/x得到 x≤0.36 GB/req。亦即只要每次请求流量不大于0.36GB的话,用CDN+OSS总是比单用OSS便宜。同理用CDN流量费用下界计算,可以得到只要每次请求流量不大于0.15GB。

结论就是,静态内容变动得不勤快,只要每次请求静态内容大小不大于0.15GB,或者很勤快,那么只要不大于0.36GB,使用CDN+OSS总是优于单用OSS。

实例

The best way to learn something is to build it yourself.

沃·梓季·硕德

在此以我自己的有机化学cookbook为例。

1.Drone CI的部署

目前drone 1.0还处在RC阶段,最好还是根据文档部署,详见https://docs.drone.io/

2.Drone CI 配置文件(*配置点1)

默认drone会从代码仓库的根目录读取一个名叫 .drone.yml 文件作为持续集成的配置文件,当然这也是可以自定义的。以下是一个范例配置文件

workspace:
  base: /data
  path: .

pipeline:
  build:
    image: kitakami/mkdocs-materials-pipeline
    commands:
      - mkdocs build -s

  upload:
    image: plugins/s3
    endpoint: yourendpoint
    bucket: yourbucketname
    access_key:
      from_secret: access_key_id
    secret_key:
      from_secret: secret_access_key
    strip_prefix: site/
    source: site/**/*
    target: .
    acl: public-read

  notify:
    image: drillster/drone-email
    host: smtp.mxhichina.com
    port: 465
    username: 
      from_secret: username
    password: 
      from_secret: password
    from: some@example.com
    recipients: [ admin@example.com ]
    when:
      branch: master

几点说明:

  1. 由于我用的是Mkdocs,所以用了一个预装Mkdocs环境的docker容器,至于怎么做一个这样的容器,其实是非常简单的。可以参见https://github.com/jsjyhzy/mkdocs-materials-pipelinehttps://cloud.docker.com/u/kitakami/repository/docker/kitakami/mkdocs-materials-pipeline,看看什么叫做蓐资本主义的羊毛。
  2. upload阶段的acl: public-read务必不要漏掉,否则即使bucket设置为公共读,文件也是私有属性的(因为默认行为不是继承Bucket ACL,而是直接Private)。
  3. notify阶段的端口号记得设置,感觉他们歪果仁的邮件服务器默认端口跟我国的不一样。

3.OSS与CDN的配置(*配置点2)

网页UI它老改来改去,就不上截图了,几点说明:

  1. Bucket配置静态首页与404页后就开启了静态网站模式。在此模式下任何以斜杠结尾的请求将返回配置的静态首页。
    1. 也因为此Mkdocs用户需要注意配置文件中设置
      use_directory_urls: false 以启用经典URL模式。
  2. Bucket的权限设置为公共读,这是因为当CDN配置为私有Bucket回源时访问以斜杠结尾的URL会被当作签名访问根目录,而不返回静态首页
  3. 由于Bucket的权限为公共读,为了防止直接对Bucket的访问可以考虑设置Referer,并禁止无Referer访问,同时在CDN回源的HTTP头上追加Referer。Referer可以是任何值,比如说一段随机的16进制数。

-1.使用

将灵感注入本地的Markdown文件后,git push,不到一分钟就能收到通知邮件,静态网站就更新完成了。

Python 图片字符化

以前写过一篇关于字符化的文章,当然用的算法比较老,虽然性能很好(就纯Python实现而言),但确实效果不算特别好,简单说一下思路,就翻篇吧。

老方法

当我们考虑一个图片,用字符将每个像素块替换为一个字符,那么首先会想到的,是根据每个字符产生的平均的亮度效应,与待替换像素块的平均亮度作映射。通过生成缩略图,比如我们需要一个50×50字符数量的图,那我们就将原图缩略成50×50像素,然后将每个字符缩略成1个像素,接着我们根据亮度,映射一下就好。因为只是一个常量的映射,因此性能相对来说挺不错的。

问题

性能好当然是有代价的。最大的问题在于细节的缺失,不管原来的像素块是有一个什么样的模式在里面,缩略之后什么都没有了。

新思路

那既然只考虑平均的亮度效应不行,那我们就逐个像素比较,找出最相近的字符,那不就好了嘛。

方法论

首先,字符化之后,字符显然是黑色的,或者准确地说,是前景色。因此就有个前景色选择的预处理问题,图片中哪种颜色是前景色,这就必须要人工介入选择。当然通常情况下把黑色作为前景色处理,是不会有错的。
那么接着需要考虑的是,逐个像素比较出最相近的字符要怎么比较,或者规范的说,如何定义像素块之间的距离。相对来说,比较直观的想法就是把整个包含N个像素点的像素块拉成一个N维向量,然后比较向量间的距离,比如余弦距离等等。
最后,从方法论上讲比较简单,不过就是渲染出单个字符,然后逐个比较嘛,但工程上的实现,确实有些十分讨厌的地方,下面会接着谈。

实现

我个人的一个实现已经放在Github上了。
链接在此👉https://github.com/jsjyhzy/charalize👈
代码是有一点乱,但是因为比较短,所以也就不多做解释,应该能无文档看懂代码吧。(希望)

距离函数

一般来讲向量间的距离用闵科夫斯基距离就可以了,所谓闵科夫斯基距离就是两个向量各维上的标量差的绝对值的p次方之和的p次方根,当p取1时退化为曼哈顿距离,p取2时退化为欧几里得距离,在我的实现中取了p=2,也就是欧几里得距离。关于欧几里得距离与曼哈顿距离的效果如下图所示:

另外值得注意的是,向量间的距离,通常还去考虑余弦距离度量,但在这个向量化方法中(彩色图先转化为灰度图,然后flatten二维数组为N维向量)这么做会有一个feature(绝对不是bug噢😎)。就是通过余弦距离度量生成的字符,会非常有效地捕捉到原画的线条,或者叫像素变化剧烈的部分,而色块则会非常星际地忽视。如下图所示:

这个区别就比较大(余弦距离 左 欧几里得距离 右)

究其原因,在于色块的像素块在我这个向量化方法中,会变成全是x的N维向量。对于余弦距离而言,全是x的N维向量和全是y的N维向量,其实是一回事。但对于线条而言,哪怕只有几个像素的变化,其距离变化相较闵科夫斯基之流会更加敏感,比如对辫子处的处理上:

左 余弦距离 中 原画 右 欧几里得距离

当然可以考虑综合余弦距离和闵科夫斯基距离做成一个集成学习,但就两个和而不同的算法可能还是有点….稀薄🤣。等以后能想到其他算法了,应该就可以搞一搞集成学习。

字符与字体

目前这个新思路,虽说新,但和原来的算法一样,也只支持等宽字体,也就是说一定是横纵分割的块。因此,首先要用等宽字体。但是就算用了等宽字体,也要视所使用的字符。哪怕字体告诉你“我是等宽字体哟😃”,实际上也有可能不等宽。这就涉及到字符的半角与全角之分。如果只看ASCII字符的话,那毫无疑问没有什么半角全角,但如果纳入,比如说日文平假名,那通常可以看到2个ASCII字符才跟一个平假名等宽的景象。
因此我们需要将ASCII字符变成全角的,全角Latin字符在U+FF00到U+FF65,所以只要把半角Latin字符序号加上65248就可以了,全角空格可以直接用U+3000,或者Python3里用char(12288)来表示

根据CUE切分音频文件

首先,为什么要切成小文件:如果你像我一样将音乐储存在文件服务器上,通过映射驱动器然后在本地播放。那么切分对于文件服务器具有极大的优势,文件服务器可以有效地缓存小文件。例如使用ZFS作为储存后端,启用ARC+L2ARC后可以最大限度地利用缓存,从而获得更短的响应时间,同时也减少机械磁盘的读写。

也许大家知道ARC和L2ARC都是块级的缓存机制,切不切开来好像也没太大所谓。但大家也应该知道并不是所有协议都支持随机读取的,比如基于WebDAV的网络共享在处理大型文件上就比较尴尬了。

快速上手

首先,介绍一下我写的一个Python小工具cuecut,用pip安装后直接就可以在命令行中使用cuecut命令根据cue文件切分对应的音频文件了。
PyPI链接–>https://pypi.org/project/cuecut/
Github链接–>https://github.com/jsjyhzy/cuecut

要是对原理不是很感兴趣的话,那么把FFmpeg的可执行文件复制到PATH中的任一目录,假设cue文件的位置是/a/b.cue,希望切分出来的音频文件是flac格式,那么打开命令行,输入如下命令

cuecut /a/b.cue -c flac

就可以了。音频文件将会自动根据cue文件中的演奏者,单曲名称命名为`演奏者 – 单曲名`。当然目前还不支持配置自动命名的样式,也许以后会有吧。

原理与方法论

技术上还是在使用ffmpeg的命令行,因此难点还是主要集中在ffmpeg的命令行参数的选取上。首先一些无关痛痒的参数,诸如是否显示banner(--hide_banner)、输入文件是什么(-i FILE)、输出编码是什么(-c:a codec)、日志级别(-loglevel levelname)、元数据(-metadata xxx=yyy)就不再展开讨论了。接下来主要讨论一些比较重要的参数:

移除原有的元数据

对于有一些音频镜像文件,原作者会将cue文件作为元数据写入音频文件中,如果不移除这些元数据,可能会导致播放音频文件时出现分章。因此需要用参数 -map_metadata -1 来移除这些元数据。

切取位置

既然是切分出音轨,那么就要确定切分位置,音轨的开始位置(或者说偏移量)用 -ss where 参数描述,其中where是一个浮点数(建议只取小数点后两位),结束位置则用 -to where 参数描述。

元数据写入

既然要写入一些诸如演奏者、音轨名或者专辑名称的元数据,那么就要让FFmpeg写入ID3数据块,这一点可以用 -write_id3v1 1 参数来指定。

移除视频流

部分音频镜像会把专辑封面作为一个视频流插入文件中,在切分这样的文件时,往往第一个音轨是能够正常获得这个视频流的。然而后续的音轨都将无法获得,同时由于这个问题,还进一步导致后续音轨丢失长度信息(duration),会使部分播放器无法seek到某一具体位置。目前没有什么特别好的方法让所有音轨都正确地获取视频流,因此较为实际的方案就是扔掉所有视频流,用参数 -vn 来完成。

平台依赖的非法文件名

这个问题是可以通过修改输出文件名的模式来规避的,主要是我的cuecut中的文件名模式中存在演奏者和单曲名,因此对于一些奇怪的名字还是要加以处理的,例如名字中不能出现 \/:*?<>| 这些字符。此外在Windows平台上AUX,CON,COM[1-9],NUL,LPT[1-9],PRN都是保留名称,即使带扩展名也不能创建这类文件。

找回RAR压缩包密码

背景:某无产者(我就不点名批判一番了)分享了一个压缩包,然而有密码保护,只有密码提示“是xxx的niconico番号”。当然帖子底下有好多伸手党问密码到底是什么,不出意外,没有什么特别的发现。

首先是估算问题的规模,根据niconico在发帖时大概有2千万个视频,基本可以认为这个问题的规模时2千万个候选密码。不算特别大,取个常用对数还在10以下,那还是能搞一搞的。

那么首先是核心代码,这边就会有一点点讲究。直觉上,如果我们想要知道压缩包的密码是否正确,那么我们可以使用密码测试一下压缩包。但是实际上,这么做的速度非常慢:在AMD Ryzen5上速度只有不到48 rps(round per second),但是当我们直接尝试去使用密码直接解压时,速度可以提升到1214 rps。下面是示例代码

def do(guess, filepath):
    from zipfile import ZipFile
    from zlib import error as ZlibError
    fp = ZipFile(filepath)
    for passwd in guess:
        try:
            fp.extractall(pwd=passwd )
            print('password is %s' % passwd)
            break
        except ZlibError:
            pass
        except RuntimeError:
            pass

那么对于CPython的祖传单线程,启用多进程池就行了,示例如下

from multiprocessing.pool import Pool
if __name__ == "__main__":
    pool = Pool()
    for i in [b'some', b'password', b'here']:
        pool.apply_async(do, args=(i, '/path/to/zipfile'))
    pool.close()
    pool.join()

当然根据具体问题适当划分一下问题规模会比较好,这里就不再展开了,毕竟开一个进程是要时间的,传参是要占内存的。

小议WordPress的容器化

一般来说无状态服务比较适合进行容器化,以获得极强水平扩展能力,但是对于WordPress这种应用,进行容器化就不可避免地要遇到数据持久化的问题。数据持久化主要在体现在两个方面:wp-content文件夹和数据库。

经典的部署方案是本地创建一个数据卷,然后再加上一个容器化的数据库。那么对于容器化的终极目的——水平扩展能力——这样的经典方案是远不能达到要求的。

首先是经典方案中容器化的数据库,个人建议还是独立到其他服务器,或者干脆直接安排在宿主机上。第一、虚拟化是有开销的,像数据库这类偏重IO的应用,潜在地会降低性能;第二、容器化后的数据库并没有得到更高的水平扩展能力,同时各数据库间的数据同步将成为一个大问题(会有一个dockerd和mysqld谁先挂掉的问题,谁能保证dockerd这头鲸鱼搁浅之前能恰当地杀掉mysqld这只海豚?)

另一个就是本地数据卷的问题,个人建议数据卷直接挂载NFS共享,而不是挂载本地文件系统,这样就能省去很多wp-content目录数据同步的麻烦。挂载NFS共享还可能有一个读写分离的好处,毕竟读者只需要读操作,编者才需要写操作(然而并没有太大意义)。

因此总体而言docker-compose.yml文件看起来就会像下面这样

version: "3"
services:
   wordpress:
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:port       # <---这里是数据库的地址和端口
       WORDPRESS_DB_USER: wordpress     # <---这里是数据库用户名
       WORDPRESS_DB_PASSWORD: wordpress # <---这里是数据库密码
     volumes:
       - wp-data:/var/www/html

volumes:
   wp-data:
      driver: local
      driver_opts:
        type: nfs
        o: "addr=192.168.1.54,rw,sync" # <---这里是NFS的地址
        device: ":/wordpress-files"    # <---这里是NFS的共享路径

那么此时水平扩展只需要多开几个docker-compose,然后前端nginx恰当地反向代理、负载均衡即可(一定不要把proxy_set_header Host漏了,不然就会冒出充满血泪的Warning: Cannot modify header information - headers already sent by ...,一般的troubleshot只会提示你说可能php文件里有个空格。

内网穿透——FRP途径

环境

权限与运行环境

Server A :不作要求;不作要求

Server B :不作要求;不作要求

Server C :不做要求;不作要求

步骤

  1. 根据A、B的体系结构从FRP发行版中下载下来,A上解压出frpc(客户端),B上解压出frps(服务端)。
  2. A、B上分别配置frpc.ini和frps.ini。
  3. A、B上分别编写systemd服务文件,并启用

讨论

相对ssh隧道来说,frp途径更多的是配置文件的问题。所以从步骤上看好像很短很简单的样子,但写配置文件倒确实是有一点点麻烦的好用和配置繁琐的trade off。接下来就最常见的端口转发与http转发,对相关配置文件简要讨论。

1. frps.ini

http的转发在服务器端的配置主要体现在[common]块中的vhost*项,因此http只需要在客户端定义。对整个连接的保护,则体现在[common]块下的token项。其次dashboard*项,正如其名字所示,浏览器访问指定端口就可以查看服务端运行状态,遗憾的是只能查询

[common]

bind_port = 7000
kcp_bind_port = 7000
vhost_http_port = 7080
vhost_https_port = 7081

dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = 12345687

token = 12345687

1.5 间章 针对HTTP服务穿透的特别部署姿势

对于内网穿透来说,更多时候会变动的是外网服务器,内网服务器一般是不会变动的。有的时候发现了更便宜的服务器,考虑迁移的话,这么配置一圈还是有那么点麻烦的(又是跑到GitHub去下载release,又是建配置文件,又是配置服务的),对于仅需要HTTP穿透的需求来讲,服务端的配置文件一般是不需要变动的(尽管从FRP开发的历史进程上看,好像开始倾向于所有的配置交给客户端来设置;服务端往后可能也不负担配置任务了),于是我们可以利用Dokcer把服务端封装起来,迁移外网服务器的时候只需要在新服务器上启动镜像即可。

本人做了一点微小的工作 (*/ω\*) 👉 kitakami/frp-server

在这个镜像中,只需要通过环境变量配置Dashboard的用户名与密码和验证用的Token即可,此外切换版本也会相对比较方便。当然如果HTTP服务仅仅只有透过FRP穿透而来的话,也可以加上nginx镜像组成docker-compose来启动。

2. frpc.ini

客户端的配置,主要是以正确连接上服务器([common]块),与定义具体服务(其他块)。http的部分相对来说一看就懂,可能需要提一下的就是protocol = kcp的部分,传说可以改善客户端与服务端因延迟、丢包而造成的问题。端口转发的部分需要注意,服务端定义了一个监听端口,客端端上的远程端口就要写那个监听端口。

[common] 
server_addr = #远程主机IP 
server_port = 7000 
auth = qwerty
token = 12345687
[mc] 
local_port = 25565 
remote_port = 25565  
[drive]
type = http 
local_ip = 127.0.0.1 
local_port = 80 
use_encryption = true 
use_compression = false 
custom_domains = #自定义域名 
protocol = kcp  

2.5. 间章 http转发

有一些程序(例如gogs、minio)内置http服务器,但有一些程序(主要是php程序,例如nextcloud)需要借助外部http服务器(例如apache2、nginx)的帮助才能访问。对于同时有这两类程序的后端,http的转发方法就有两种:

  1. frp作为反向代理
  1. frp作为端口转发

个人建议第一个方案,如果要用第二个方案为什么不用ssh-tunnel。如果采纳方案1的话,比较推荐frp中段采取http协议进行加密,但不进行压缩。理由是压缩的任务,理应由http服务器来完成,盲目地对流量进行压缩,潜在地,会造成大量对图片无谓的压缩。

此外,并不是很推荐转发https,除非Server B上连nginx也配置不起(怕不是树莓派、单片机吧)。原则上讲,加证书这种事情应该由最末端的http服务器来做。因为https有一个握手环节,转发https不过是徒增延迟。

3. frp(s/c).service

为frp注册一个systemd服务,还是比较简单的。例子如下

[Unit]
Description=frps Daemon
After=syslog.target network.target
Wants=network.target

[Service]
Type=simple

# ExecStart=/usr/local/bin/frpc -c /root/frpc.ini
ExecStart=/usr/local/bin/frps -c /root/frps.ini
Restart=always
RestartSec=1min
ExecStop=/usr/bin/killall frps

[Install]
WantedBy=multi-user.target

关于mysqldump在命令行接口使用密码时的警告

数据库的备份策略通常有主从同步、数据库导出等等。对于小规模的数据库而言,例如家用文件服务器(我本来就只有一个节点,我上哪再去搞从库),使用mysqldump命令配置每日备份不失为一个更好的方案。

但在备份脚本中明确使用账户密码,mysqldump将会报警告

mysqldump: [Warning] Using a password on the command line interface can be insecure.

既然有警告,那就要去解决,其实完全不理他也是没太大关系的。问题出在在脚本中放账户与密码,确实不是很安全,亟需用一种更安全的方法登陆数据库,于是就有了--login-path开关(MySQL 5.6及以上)。当然也可以运行脚本时重定向stderr流,但这样做会把其他的错误内容一并解决掉,一刀切可能会对备份策略的可靠性造成潜在危害。

使用mysql_config_editor命令,例如mysql_config_editor set --login-path=local --host=localhost --user=db_user --password,就会生成一个名为local的登陆点(login path,直译),其账户密钥数据加密储存于$HOME/.mylogin.cnf,然后使用mysqldump时使用--login-path开关指定登陆点即可,例如mysqldump --login-path=local ...

Appendix

下面是一个我放在小服务器上的一个自动备份脚本,希望能抛砖引玉。

#!/bin/sh
for name in "$@"
do
    echo "========================="
    echo "Processing database $name"
    echo "-------------------------"
    now="$(date +'%Y_%m_%d_%H_%M_%S')"
    filename="$name"_backup_"$now".gz
    echo "File Saved as $filename"
    backupfolder="/vault/111/mysql_backup" # <=== 这里是备份保存的根目录 
    fullpathbackupfile="$backupfolder/$filename" 
    mysqldump --login-path=localbackup -x -f $1 | gzip > "$fullpathbackupfile"
    find "$backupfolder" -name "$name"_backup_* -mtime +8 -exec rm {} \;
    echo "========================="
    echo ""
done
exit 0