MoreRSS

site iconLilydjwg | 依云修改

Arch CN 发起人和核心成员之一。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Lilydjwg | 依云的 RSS 预览

通过字幕总结YouTube视频内容

2026-06-09 17:11:54

本文来自依云's Blog,转载请注明。

现在YouTube首页的视频推荐越来越差了,好多标题党、clickbait,故意把讨论的主题藏起来。经常我点进去看了好久,才发现原来并没有讨论什么我之前不知道信息,又或者讨论的话题我完全不感兴趣。再不就是把我想知道的信息藏到不知道哪个片段里。虽然我有GlobalSpeed扩展可以依内容信息密度来方便地调整播放速度,但是调太快(超过2x)就听不清啦。总之是好多视频点开看之前十分吸引人,但看完或者看一半时就想骂人、点踩退出,十分浪费时间。

正好最近在尝试Gemini API,于是灵光一现,写了个脚本,使用yt-dlp下载视频字幕,然后调用Gemini API来总结内容。

#!/usr/bin/python3

import sys
import json
import subprocess
import tempfile
from pathlib import Path

import httpx

GEMINI_KEY = 'YOUR GEMINI KEY HERE'
URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite:streamGenerateContent?alt=sse'
PROMPT = '根据以下字幕文本总结视频内容。总结结果中请不要包含任何赞助商推广信息。'

def main(url, sublang):
  with tempfile.TemporaryDirectory() as d:
    subprocess.run([
      'yt-dlp', '--sub-langs', sublang, '--write-subs', '--write-auto-subs', '--skip-download',
      url,
    ],
      cwd=d,
      check=True,
    )

    p = Path(d)
    try:
      file = tuple(p.iterdir())[0]
    except IndexError:
      sys.exit('No subtitles.')

    for _ in range(2):
      try:
        do_request(file)
        break
      except httpx.ReadError as e:
        print(e, file=sys.stderr)

def do_request(filepath):
  client = httpx.Client(http2=True)
  filename = filepath.name
  with filepath.open() as f:
    subtitles = f.read()

  parts = [{
    'text': PROMPT,
  }, {
    'text': f'文件名:{filename}\n文件内容:\n{subtitles}',
  }]

  j = {
    'contents': [{
      'parts': parts
    }],
  }
  with client.stream(
    'POST', URL,
    headers = {
      "X-goog-api-key": GEMINI_KEY,
      "Content-Type": "application/json",
    },
    json=j, timeout=120,
  ) as r:
    for line in r.iter_lines():
      if not line.startswith('data: '):
        continue

      line = line.removeprefix('data: ')

      data = json.loads(line)
      for a in data['candidates']:
        for b in a['content']['parts']:
          text = b['text']
          if not text:
            break
          print(text, end='', flush=True)

  print()

if __name__ == '__main__':
  import argparse

  parser = argparse.ArgumentParser()
  parser.add_argument('URL',
                      help='YouTube URL')
  parser.add_argument('--lang', default='en',
                      help='choose subtitles language')
  args = parser.parse_args()

  main(args.URL, args.lang)

脚本依赖Python和httpx库。当然鉴于httpx已经不再更新,你换成httpx2应该也能用。Gemini Key可以去这里生成,然后填到脚本开头。我使用的是gemini-3.1-flash-lite这个模型,因为免费版本中,它的每日请求数配额比较充足。

当然啦,视频要有CC字幕这个脚本才能用,否则会报错。默认使用英文字幕,包含自动生成的版本。如果是中文视频,可以使用--lang zh参数指定用中文字幕。

自定义系统默认中文字体

2026-05-12 00:05:05

本文来自依云's Blog,转载请注明。

一开始使用Linux系统的时候,并没有多少自由开源的中文字体。那时候几乎所有人的选择都是文泉驿正黑。我就一直用啊用了好多年,直到后来截图时被网友说该换字体了,我才知道原来文泉驿项目已经停止很久了,网站上的新闻截止于2008年——都快20年啦。

文泉驿正黑的字形比较「旧时代」——以屏幕清晰度为优先,由于当时的屏幕普遍dpi低,笔画迁就像素风格,所以比较丑。另外也有些bug,比如「撨䑾詺㘃㞈㟯㫥」这几个字会有一片漆黑的区域。

文泉驿正黑看起来是这样的。注意截图中只有除标题外的中文部分用的是文泉驿正黑字体。

文泉驿正黑渲染的网页

2014年,Google联合Adobe发布了思源黑体和Noto Sans CJK字体——这两款字体的汉字部分是相同的,区别只在于思源黑体会根据文本的区域设置来自动选择字形(通常不管使用哪个语言的字族名来指定),而Noto Sans CJK字族名只有英文版本,并且不同的字族名后缀会选择不同的地区字形。

我在2024年终于决定切换到思源黑体试试。它长这样:

思源黑体渲染的网页

图中的日文部分也是使用的思源黑体,只不过是日文字形。标题则使用的是思源宋体。读者可以在图片上点「右键」然后新建标签页打开图像,然后来回切换着对比。

可以看出,思源黑体是比文泉驿正黑好看多啦。而且思源字体有粗体版本,文泉驿正黑是没有粗体的,只有合成出来的所谓「伪粗体」。

但是,你有没有发现有什么地方不对劲?注意看大标题下方那行字,「条目」和「阅读」下边的装饰线与该行下方的分隔线有一段距离,而「大陆简体」和「工具」两处字偏低。事实上,思源黑体的问题远不止这些。思源黑体文字上方空出来的空间比下方多不少,造成行高太高、终端里文字不居中等各种问题。

思源黑体的布局

(这个图片是使用命令pango-view --dpi=1024 --font=思源黑体 --annotate=glyph,layout,baselines -t A测试中文Test -o out.png生成的。)

因此我用了几天就换回文泉驿正黑了,但是把思源黑体作为部分网页字体在用(主要用于非简体中文内容,以及通过stylus指定的特定几个网站)。

最近,oldherl说更纱黑体修了行高的问题。于是我又试了几天这个基于思源黑体的字体。它的效果是这样的:

更纱黑体渲染的网页

行高的问题确实解决了!但是——又要「但是」了——它的字怎么矮胖矮胖的?是我没看习惯的原因吗?于是我使用了几天,最后并没有习惯,反而是和思源黑体一对照,发现确实是许多字都变矮了一点。

于是我只好又换回已经用习惯了的文泉驿正黑。

诶等等!既然更纱黑体能修行高,我为什么不行?因为我不会,可是,今非昔比了呀——Gemini,给我来个脚本!

于是就有了这么个脚本——当然这个脚本是我改过的最终版本了。

#!/usr/bin/python3

from io import BytesIO

from fontTools.ttLib import TTCollection, TTFont

def adjust_font(font):
    target_ascent = 1025
    target_descent = -265
    
    # 修改 hhea 表 (macOS/Pango 渲染常用)
    font['hhea'].ascent = target_ascent
    font['hhea'].descent = target_descent
    font['hhea'].lineGap = 92

    # 修改 OS/2 表 (Windows/Linux 合规性)
    font['OS/2'].sTypoAscender = target_ascent
    font['OS/2'].sTypoDescender = target_descent
    font['OS/2'].sTypoLineGap = 92
    # usWin 参数决定了红线(剪切区域),设为相同值可消除额外间距
    font['OS/2'].usWinAscent = target_ascent
    font['OS/2'].usWinDescent = abs(target_descent)

    # 2. 修改 Font Family 名称
    name_table = font['name']
    for record in name_table.names:
        name_str = record.toUnicode()
        new_record_str = name_str.replace('Source Han', 'Lily Han') \
                .replace('思源', '百合') \
                .replace('源ノ角ゴシック', '百合ノ角ゴシック') \
                .replace('본고딕', '백합고딕') \
                .replace('SourceHan', 'LilyHan')
        # 针对不同 ID 进行替换
        # ID 1: Family Name, ID 4: Full Name, ID 6: PostScript Name 等
        if name_str != new_record_str:
            name_table.setName(new_record_str, record.nameID, record.platformID, record.platEncID, record.langID)

    # Medium as Semibold
    if font['OS/2'].usWeightClass == 500:
        # save then read to copy the font without referencing existing data structures
        buf = BytesIO()
        font.save(buf)
        buf.seek(0)
        sb_font = TTFont(buf)

        sb_font['OS/2'].usWeightClass = 600

        for record in sb_font['name'].names:
            name_str = record.toUnicode()
            if "Medium" in name_str:
                new_name = name_str.replace("Medium", "Semibold")
                record.string = new_name.encode(record.getEncoding())

        cff = sb_font['CFF '].cff

        for i in range(len(cff.fontNames)):
            if "Medium" in cff.fontNames[i]:
                cff.fontNames[i] = cff.fontNames[i].replace("Medium", "Semibold")
        
        # 修改 TopDict 内部的名称
        for topDict in cff.topDictIndex:
            for attr in ['FullName', 'FamilyName', 'Weight']:
                if val := getattr(topDict, attr):
                    if isinstance(val, str) and "Medium" in val:
                        setattr(topDict, attr, val.replace("Medium", "Semibold"))

        return sb_font

def main(input_ttc, output_ttc):
    ttc = TTCollection(input_ttc)
    new_fonts = list(ttc.fonts)
    for font in ttc.fonts:
        newfont = adjust_font(font)
        if newfont:
            new_fonts.append(newfont)
    ttc.fonts = new_fonts
    ttc.save(output_ttc)
    print(f"成功保存至: {output_ttc}")

if __name__ == '__main__':
    import nicelogger
    nicelogger.enable_pretty_logging('DEBUG')
    input_ttc = "SourceHanSans.ttc"
    output_ttc = "LilyHanSans.otc"
    main(input_ttc, output_ttc)

脚本使用fonttools这个Python库,把ascent、descent和lineGap这三个参数改成和文泉驿正黑一样的了。之所以要照着文泉驿正黑来改,是因为我的终端最大化之后,使用13pt字号差不多刚好填满35行(只空出来两行像素)。而使用别的参数,我调整字号好久,都会空出来小半到大半甚至接近一行文字的高度。

另外,这个脚本运行起来非常耗资源:它保存的时候会重新计算每一个字形,持续占用一个CPU核心长达六分多钟,内存只分配不释放,最终用掉接近20GiB。倒是生成的文件差异不大,用rsync很快就能同步回来。

最终效果图:

百合黑体渲染的网页

可惜的是思源黑体只有粗体和Medium,不像更纱黑体那样有半粗(Semibold)版本。思源黑体的粗体挺粗的。

由于我在使用思源黑体时遇到的问题都是在UI部分,而这里又不需要用到宋体,所以我没有去改思源宋体,其它地区的字形也还是用的原本的思源系列。

如果有人想试试我生成的这个字体的话,在这里下载(112 MiB)。


2026年05月13日更新:更新了脚本和字体文件。现在把Medium复制一份然后改叫Semibold了,因此有了半粗字重,在用到的地方会更接近设计者的意图,也看着更舒服。

Wayfire支持不缩放Xwayland啦

2026-04-18 21:22:54

本文来自依云's Blog,转载请注明。

Wayland协议会告诉客户端程序缩放比率应该是多少,支持的就按指定的来,不支持的Wayland compositor就负责把窗口缩放到合适的大小。但因为是客户端渲染成位图之后,再由Wayland compositor缩放,大小是合适了,但却模糊了起来。然而很不幸地,Xwayland就是那个不支持Wayland缩放机制所以被Wayland compositor强行缩放的Wayland客户端。

但其实在X的时代,各个GUI框架都发展出来了自己的一套指定缩放的机制。比如一些老的程序会认Xft.dpi这个设置,GTK可以用GDK_SCALE环境变量,Qt认QT_AUTO_SCREEN_SCALE_FACTORQT_SCREEN_SCALE_FACTORS,Wine可以在winecfg里设置,等等。我想说的是,虽然不太统一,但大家其实都有自己的HiDPI方案了。

所以,Wayland compositor如果不强行缩放Xwayland的窗口的话,那些还不原生支持Wayland的程序其实也能显示得清晰好看的。但可惜的是Xwayland始终没有合并任何解决方案,我也就只好用着打过补丁的wlroots-lily-gitxorg-xwayland-lily了。KDE很早就引入不缩放Xwayland窗口的选项、让这些使用Xwayland的程序自己处理缩放。Hyprland后来也支持了。而现在,终于轮到Wayfire支持啦~

Wayfire的这个选项叫workarounds/force_xwayland_scaling。设置为true就是程序自己处理缩放、Wayfire不管它了。一开始没处理鼠标光标,后来也修好了。xorg-xwayland-lily终于完成了它的使命~

顺便说一下,workarounds这里我还设置了另外两个选项。一个是discard_command_output=false,这样能看到Wayfire启动的程序的报错信息。另一个是auto_reload_config=false,禁用自动重新加载配置文件,不用和git打架了。

[workarounds]
discard_command_output = false
auto_reload_config = false
force_xwayland_scaling = true

使用wayvnc远程访问无头Wayfire会话

2026-04-02 13:59:32

本文来自依云's Blog,转载请注明。

显示器送去维修了,因此我的台式机变成无头设备啦。现在用它来编译软件是没啥问题的,但是我的GUI软件的窗口全部访问不了啦,编译出来的wayfire也没法调试了。本来已经存在的窗口我打算等显示器修好回来再用的,但是Vim刚出了一个公开利用方式的RCE,我怕我的GVim留在那里忘记了然后不小心中招,直接关又不记得它有没有打开什么需要处理的文件。于是想到了远程桌面。

首先想到的是群友的reframe。之前显示器还在的时候它挺好用的,但是现在显示器不在了,它就连不上去啦。读了文档才知道需要配置一个虚拟显示器,要动内核参数所以需要重启,我的窗口们就回不来啦。另外等显示器回来,要去掉虚拟显示器还要再次重启。

其实我还有另一个显示器,也就是大上的E-ink屏幕。接上去之后,我的wayfire桌面回来了,但是我的窗口并没有……lswin发现它们还在「unknown」显示器上。于是我开始研究Wayfire IPC看看怎么把它们弄回来。

花了些时候,去Matrix频道里问了一下,窗口们是用configure_view找回来了,但是发现我怎么几百个叫「input-method-popup」的窗口啊(其实这就是我要调试Wayfire的原因)。另外发现有创建无头输出的接口,也就是Wayland compositor层级的虚拟显示器啦。

墨水屏用来阅读和写代码挺好的,但是用来测试GUI软件就不太适合了,更不用提打游戏了。所以就想着既然能创建无头输出了,那我是不是可以把无头输出VNC出来、在笔记本屏幕上看?于是读了一下wayvnc的文档,发现还真可以。

创建无头输出:

import wayfire, os
addr = os.environ.get('WAYFIRE_SOCKET', os.environ['XDG_RUNTIME_DIR'] + '/wayfire-wayland-1-.socket')
sock = wayfire.WayfireSocket(addr)
sock.create_headless_output(1920, 1080)

然后记下创建出来的输出的名字(HEADLESS-开头的那个)。几经尝试,最终使用的wayvnc命令是:

wayvnc -f 60 -r -g -o HEADLESS-2 IP PORT

其中-f 60指定帧率,默认是30fps;-r指定渲染光标,否则在服务端使用鼠标时不显示光标;-g启用需要GPU的特性;-o HEADLESS-2是选择之前创建的无头输出。后边的监听地址我指定了与笔记本直连的有线网口的IP,这样不需要设置密码、也不用加密就很安全。

然后使用tigervnc连接:

LANGUAGE=en_US vncviewer -Maximize IP:PORT -PreferredEncoding H.264

vncviewer显示中文有问题,因此使用英文界面。这里指定了编码方式为H.264,希望能比默认更好一些。我原本还想用Raw来着,但是问了一下Gemini,说是1Gbps不够1080p60用,另外序列化延迟也会更高。不过wayvnc和vncviewer都没有使用VAAPI的样子,不知道是不支持还是哪里有问题。

实际占用的带宽最高为27 MiB/s。延迟挺低的,反正我感觉很不明显,玩游戏也没有问题。不过可能是因为有压缩,文字的显示不是很清晰。

哦对了,因为vncviewer是Xwayland软件,因此它自己的键盘捕获是没有用的。我使用Wayfire的shortcuts-inhibit设置让它默认捕获键盘了,需要的时候再按快捷键临时取消一下。

[shortcuts-inhibit]
break_grab = <ctrl> <alt> <shift> KEY_ESC
inhibit_by_default = app_id is "Vncviewer"

至于为什么不用别的VNC客户端,gtk-vnc慢死了,remmina界面好复杂,virt-viewer我不知道怎么叫它连远程VNC。

给论坛用上了文本嵌入模型

2025-11-11 15:14:52

本文来自依云's Blog,转载请注明。

偶然间发现Discourse论坛支持利用文本嵌入模型来生成「相关话题」列表、提供语义化搜索。于是我给Arch Linux中文论坛试过了好几个模型,记录一下经验。

文本嵌入,英文叫「text embedding」,指的是将一段文本编码成语义空间中的向量,从而可以判断不同文本的语义相关性。编码出来的向量少则512维,多的能有4096维。而判断相关性有「余弦距离」(看两个向量的夹角大小)和「负内积」(一个向量和另一个向量的转置相乘,然后取负)两种方法,我都是看模型文档和示例来决定用哪个的。至于这些向量的存储和索引,Discourse使用的是pgvector这个PostgreSQL插件。

Discourse启用这个功能之后,会在每个话题下方推荐几个「相关话题」,很适合看看是不是有人问过相同的问题。语义化搜索则需要在搜索页面点按钮来显示。在搜索框里按两下回车,就能到搜索页面了(这时候语义化搜索就会进行了,虽然用户还看不到结果),或者点搜索框右边的按钮也行。

因为论坛以中文为主,所以没多少可以抄Discourse官方文档的地方。一开始我挑了好几个来尝试,bge-m3、all-mpnet-base-v2、gte-multilingual-base等。但是没想到它们体积不大,但跑起来却很吃资源。E5-2678 v3辛辛苦苦跑了好久,结果去数据库里一看,已索引的话题数量才几个、十几个,而且不见涨……后来写了API转换代理我才知道,原来是因为Discourse会批量并发请求,并发度会高达45左右,于是很容易导致本来就慢的请求因为排队太久而超时被放弃,CPU都白算了。

最终我找到gte-base-zh这个模型,是针对中文特化的。很小,才0.1B,但这CPU跑得动它。效果也还能接受。

后来了解到最近新出的Qwen3-Embedding系列,看评分效果是最好的。又有群友愿意提供显卡算力,于是试了试。

Qwen3-Embedding提供8B、4B、0.6B三种参数规模的模型。8B很重,我的6650XT的8G显存勉强能放下它的Q4_K_M量化版本。0.6B的只有Q8_0的量化版本,我的显卡跑起来轻松不少,就是不知道为什么它占了我4G+的显存,导致剩下的显存不够原神用了。另外运行的时候如果不用systemd的CPUWeight之类的手段降一下CPU优先级,会导致我的桌面也很卡——我没找到调整GPU优先级的方法,不过调整CPU优先级也管用。

这些模型在群友提供的RYZEN AI MAX+ 395上跑得就比较惨。这台设备有算力不错的核显——至少比用Linux的Apple M2 Ultra算得更快一些,也有核显能够使用大量内存的优势,但是!amdgpu驱动会在高负载时崩溃重置!这么久过去了,amdgpu依旧不待见核显啊(不过听说Intel那边新的xe驱动也有不少bug)。不过断断续续跑了几天之后,终于把大部分话题都索引好了。

后来我还是换0.6B模型了,因为群友提供的算力并不稳定,我想要更容易替代的方案。可能Qwen3-Embedding系列模型对我的用途来说实在是太优秀了,以至于不管是0.6B还是8B,我都没发现结果有什么明显的差异。但0.6B对性能的需求低很多,甚至编译机上的7950X3D也能跑——虽然编译机没那么多时间能跑它就是了。

我还尝试过Google家的embeddinggemma-300M模型。它的MTEB评分比gte-base-zh要高,但只比gte-base-zh大一倍。但实际用下来,呃,效果差很多,基本上没啥用,可能分数都得在别的语言上了吧。遂放弃。

目前的论坛文本嵌入算力主要由群友的RYZEN AI MAX+ 395提供。在它不在线的时候,则由另一位群友提供的Apple M2 Ultra编译机兼职。哪天要是它也有事不在了,还能由x86编译机接棒。在历史话题索引完毕之后,平时的请求其实挺少的。

哦对了,最近还接触过一个叫all-MiniLM-L6-v2的模型,超级小,只有22.7M参数,是火狐新加的地址栏语义化搜索用的。但是它只支持英文,对于中文来说纯粹在增加噪音,可以在about:config里搜索places.semanticHistory.featureGate关闭之。

最后说说运行这些模型的方式。对于给sentence-transformers用的模型,可以用ghcr.io/huggingface/text-embeddings-inference:cpu-latest这个容器来运行。缺点是,它只有支持CPU和CUDA的版本。所以我更喜欢找gguf格式的模型,然后用llama.cpp来运行,可以使用Vulkan或者ROCm。不过我测试发现llama.cpp用ROCm还不如用Vulkan的来得快,而ROCm有着极其巨大的依赖库群,我就不用它了。要是乐意用ROCm的话,也可以用ollama来跑,支持动态加载和卸载模型——但这对于长期运行的服务型用途来说并不是很适合,我还得传个参数让它不要一直加载卸载。

使用 Restic 备份数据

2025-09-23 18:13:16

本文来自依云's Blog,转载请注明。

我很早就知道 Restic 这个备份工具了,但是因为我有 rsync 和 btrfs send/receive 方案所以一直没用过。某天,突然有个走 AWS s3 协议的服务器备份需求,这才把它翻了出来。

既然是 s3,首先设置环境变量 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,然后仓库的地址是 s3:域名/存储桶名。好像一个存储桶里只能初始化一个 Restic 仓库?我还不清楚,也没有能给我玩的环境,就不管了。仓库地址可以设置到环境变量 RESTIC_REPOSITORY 中,这样就不用每次用 -r 参数指定了。执行 restic init 进行初始化。Restic 只支持加密备份,所以必须设置一个备份密码——倒也可以设置为空字符串,但是加密仍旧会进行,并且需要额外的参数来允许空密码。密码也可以放在 RESTIC_PASSWORD 环境变量里。

然后就是执行备份命令了,很简单的,比如:

restic backup -x --exclude-caches --exclude='/home/*/.cache/' --exclude=/var/cache/pacman/pkg/'*' ... /

这样就备份了整个系统,并排除了一些目录。可以先加 -v --dry-run 参数测试一遍。确定好备份的参数之后,就可以设定计划、反复执行了:

[Unit]
Description=backup the system
After=network-online.target

[Service]
Type=oneshot
Environment=HOME=/root
EnvironmentFile=/etc/restic.conf
ExecStart=restic backup -x --exclude-caches ... /
[Unit]
Description=backup system daily

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
AccuracySec=6h
Persistent=true

[Install]
WantedBy=timers.target

仓库地址和各种密码之类的放环境变量文件里了。把这个文件设置为只有 root 才能读,避免别的用户看到不该看的内容。然后 systemctl enable --now sysbackup.timer 就好了。

之后可以用 restic snapshots 查看备份的数据。使用 restic forget 删除旧的备份,比如 restic forget -d 30 -w 10 -m 10 -l 5 在最近30天、10周、10个月中各保留一份,外边最新的五个。这个命令只是删除这个备份的元数据,其关联的数据不会被删除。之后还要执行 restic prune 来实际删除数据——这个过程会比较费时费力。加 -v 可以查看诸如减小了多少空间占用的数据。

Restic 的备份是打包成块、压缩、加密存储的,带去重功能。但是增量备份会和主机名和路径相同的上一个备份进行比较。一个仓库里可以备份多台机器的相同或者不同的目录,但为了有效率地增量备份,每次增量备份时的目录不能变。这最后一点给我出了个难题。

我把 Arch Linux 中文论坛(以及维基)也用 Restic 备份到 sftp 目标里了。但这俩都是重数据库应用,因此直接对着 / 执行 restic backup 不行,数据有可能因为数据库的不同文件来自于不同的时间点而损坏。该系统在 btrfs 上并且有快照,对着快照备份就好了。问题是 Restic 执意不支持设置源路径,只能从文件系统上读取。我只好用 mount 命名空间把快照弄到 / 上再执行备份。脚本在这里:https://github.com/archlinuxcn/misc_scripts/blob/master/wiki/restic-backup-snapshot。由于权限的问题,用 bwrap 是不行的。对快照再做一个固定路径的可写快照来备份倒是能把路径固定到一个指定的位置,就是不太优雅,事后删掉快照也有额外的性能消耗。

关于备份数据的大小,restic snapshots 显示的是所有文件的名义大小的总和——是压缩前的大小,并且硬链接会算多次。restic stats --mode=restore-size 则是按硬链接去重后的未压缩大小。restic stats --mode=raw-data 才是压缩后的、备份实际占用的空间。

Restic 的备份仓库是加密的,因此无法直接查看,但它有 restic mount 子命令可以把备份仓库挂载了查看内容,就是性能比较差,只适合检查和恢复特定的少量文件。恢复大量数据推荐用 restic restore。另有 restic diff 命令可以查看不同快照之间的文件差异(新增、修改、删除了哪些文件,不含内容),方便在大小出现异常时查找元凶。

最后说说配置只能使用 sftp 的用户的方法。

首先在 sshd_config 里设置 Subsystem sftp internal-sftp。因为之后要进行 chroot,访问不到外部 sftp 服务的可执行文件的。然后设置指定的用户组只能用 sftp:

Match Group sftp-users
     ChrootDirectory /srv/sftp
     X11Forwarding no
     AllowTcpForwarding no
     AllowAgentForwarding no
     ForceCommand internal-sftp -d /%u

最后创建用户,按常规设置 authorized_keys,并按之前配置的路径,在 /srv/sftp 下创建该用户与其用户名同名的存储数据的目录,记得要 chown 到该用户。注意 ChrootDirectory 的目标目录(也就是这里的 /srv/sftp)应当只能由 root 写入(就跟 /home 目录那样)。