任何枯燥的重复性工作都必须尽可能地简单,否则就会被忽视。我很确定这就是我们发明电脑的原因。备份是挺无聊的。事实上,当涉及到备份时,你要避免任何形式的刺激。所以它们必须尽可能的简单。我有一个脚本,当一个新设备被添加时,由 udev 规则触发。当插入一个磁盘时,这个脚本工作得很好。(这很好用。)我有一个 USB 集线器,里面有几个U盘,形成了一个 btrfs raid5,每当我打开U盘集线器的时候,就可以对我的 $home
进行快速备份。在某些情况下,这并不能正常工作。找一个 bash 脚本来检查是否有一个硬盘丢失了,这可不好玩。主要是因为只有合适的语言才会有 Set。我们确实有一种合适的语言。
在 linux 上,找出一个驱动器是否被插入是相当容易的。我们需要做的就是观察 /dev/disk/by-id/
中是否有新文件出现。我们也可以知道是否发现了新的分区。这个目录看起来是这样的。
$ ls -1 /dev/disk/by-id/
ata-CT120BX500SSD1_1902E16BC135
ata-CT120BX500SSD1_1902E16BC2AA
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG-part1
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG-part2
ata-TOSHIBA_HDWQ140_X83VK0GDFAYG-part3
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG-part1
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG-part2
ata-TOSHIBA_HDWQ140_Y8J9K0TZFAYG-part3
usb-SanDisk_Ultra_USB_3.0_4C530001160708110455-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001190708111070-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001220708110370-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001280708111064-0:0
wwn-0x50000398dc60029a
wwn-0x50000398dc60029a-part1
wwn-0x50000398dc60029a-part2
wwn-0x50000398dc60029a-part3
wwn-0x50000398ebb01681
wwn-0x50000398ebb01681-part1
wwn-0x50000398ebb01681-part2
wwn-0x50000398ebb01681-part3
如果我们寻找任何不以 '-part' \d+
结尾的东西,我们就得到了一个驱动器。我们也可以通过检查前缀来判断它插在哪里。
sub scan-drive-ids(--> Set) {
my Set $ret;
for '/dev/disk/by-id/'.IO.dir.grep(!*.IO.basename.match(/'part' \d+ $/)) {
$ret ∪= .basename.Str;
CATCH { default { warn .message } }
}
$ret
}
my %last-seen := scan-drive-ids;
集合没有 append
方法,我们可以用 ∪=
代替。现在我们在 %last-seen
中得到了一个可爱的 Set
,里面有已经存在的驱动器。现在我们需要等待新的文件出现,并对它们应用集合理论。
react {
whenever IO::Notification.watch-path('/dev/disk/by-id/') {
my %just-seen := scan-drive-ids;
my %new-drives := %just-seen ∖ %last-seen;
my %old-drives := %last-seen ∩ %just-seen;
my %removed-drives := %last-seen ∖ %just-seen;
%last-seen := %just-seen;
# say ‚old drives: ‘, %old-drives.keys.sort;
say ‚new drives: ‘, %new-drives.keys.sort || '∅';
say ‚removed drives: ‘, %removed-drives.keys.sort || '∅';
}
}
通过将 Set 绑定到 Associative 容器上,我们可以得到for和其他构建的行为。如果我们想在添加某些磁盘时采取行动,我们需要定义包含正确文件名的 Set。
my %usb-backup-set = Set(
<
usb-SanDisk_Ultra_USB_3.0_4C530001160708110455-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001190708111070-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001220708110370-0:0
usb-SanDisk_Ultra_USB_3.0_4C530001280708111064-0:0
>);
my %root-backup-disk = Set(<ata-TOSHIBA_DT01ACA200_8443D04GS>);
my $delayed-check := Channel.new;
my Promise $timeout-promise;
react {
whenever IO::Notification.watch-path('/dev/disk/by-id/') {
my %just-seen := scan-drive-ids;
my %new-drives := %just-seen ∖ %last-seen;
my %old-drives := %last-seen ∩ %just-seen;
my %removed-drives := %last-seen ∖ %just-seen;
%last-seen := %just-seen;
# say ‚old drives: ‘, %old-drives.keys.sort;
say ‚new drives: ‘, %new-drives.keys.sort || '∅';
say ‚removed drives: ‘, %removed-drives.keys.sort || '∅';
if %usb-backup-set ∩ %new-drives {
$timeout-promise = Promise.in(5).then: {
$delayed-check.send: True;
$timeout-promise = Nil;
} without $timeout-promise;
}
if %root-backup-disk ∩ %new-drives {
sleep 2;
backup-root-and-home-to-disk(%root-backup-disk);
}
say '';
}
whenever $delayed-check {
my %just-seen := scan-drive-ids;
if %usb-backup-set ⊆ %just-seen {
backup-home-to-usb(%usb-backup-set);
} elsif %usb-backup-set ∩ %just-seen {
warn 'drive missing in usb set: ' ~ (%usb-backup-set ∖ (%usb-backup-set ∩ %just-seen)).keys;
reset-usb-hub;
}
}
}
我使用 $delayed-check
whenever 块来处理其中一个usb棒拒绝上线的情况。usb hub 的 vendorid 和 deviceid 是硬编码的。请注意,状态和启动不能混为一谈。
sub reset-usb-hub(--> True) {
state $reset-attempt = 0;
if $reset-attempt++ {
say ‚already reset, doing nothing‘;
$reset-attempt = 0;
} else {
say ‚Resetting usb hub.‘;
my $usb_modeswitch = run <usb_modeswitch -v 0x2109 -p 0x0813 --reset-usb>;
fail ‚resetting usb hub failed‘ unless $usb_modeswitch;
}
}
整个脚本可以在这里找到。我相信 watch-path 的例子可以使用这个脚本的修改版本。如果你读了它,你可以简单地通过发现集合运算符来判断哪里使用了集合。让 Raku 成为一种面向操作符的语言是个好主意。谢谢你,Larry。
当我把我的备份脚本从 Bash 转到 Raku 的时候,我有了更多关于用适当的语言编写 shell 脚本的发现。我将在接下来的几周内在这里报告这些发现。
by gfldex.