虽然现在各大云存储遍地,但是将自己的重要资料在资金允许的情况下,多异地备份几次不会有错,毕竟,电脑有价资料无价。今天写了个备份目录到backblaze的小脚本,分享记录一下

流程很简单,使用tar打包,使用zstd压缩,再使用age加密,最后上传, 成功后让pushover给自己发通知

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package main

import (
"bytes"
"flag"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"time"
)

type Config struct {
KeyID string `yaml:"key_id"`
KeySec string `yaml:"key_sec"`
Endpoint string `yaml:"endpoint"`
SrcFolderPath string `yaml:"src_folder_path"`
TarPath string `yaml:"tar_path"`
ZstdPath string `yaml:"zstd_path"`
AgePath string `yaml:"age_path"`
AgeKey string `yaml:"age_key"`
DstName string `yaml:"dst_name"`
BucketName string `yaml:"bucket_name"`
PushOverUser string `yaml:"push_over_user"`
PushOverToken string `yaml:"push_over_token"`
}

func main() {
// 打包 tar -c folder | zstd | age -r xx > folder.tar.zst.age
// 拆包 age -d -i key.txt folder.tar.zst.age | unzstd | tar -xf -
configFile := flag.String("f", "config.yaml", "Path to config file")
flag.Parse()

data, err := ioutil.ReadFile(*configFile)
if err != nil {
log.Fatalf("Error reading config file: %v", err)
}
var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Fatalf("Error parsing config file: %v", err)
}

tarCmd := exec.Command(config.TarPath, "-c", config.SrcFolderPath)
zstdCmd := exec.Command(config.ZstdPath)
ageCmd := exec.Command(config.AgePath, "-r", config.AgeKey)

tarOut, _ := tarCmd.StdoutPipe()
zstdCmd.Stdin = tarOut

zstdOut, _ := zstdCmd.StdoutPipe()
ageCmd.Stdin = zstdOut

encryptedFile := fmt.Sprintf("/tmp/%s_%s.tar.zst.age", time.Now().UTC().Format("20060102150405"), config.DstName)
f, err := os.Create(encryptedFile)
if err != nil {
log.Fatalf("os.Create: %v", err)
}
defer f.Close()
defer os.Remove(encryptedFile)

// set up the pipe between age and the encrypted file
var buffer bytes.Buffer
ageCmd.Stdout = &buffer

if err := tarCmd.Start(); err != nil {
log.Fatalf("tarCmd.Start: %v", err)
}
if err := zstdCmd.Start(); err != nil {
log.Fatalf("zstdCmd.Start: %v", err)
}
if err := ageCmd.Start(); err != nil {
log.Fatalf("ageCmd.Start: %v", err)
}

// wait for all commands to finish
if err := tarCmd.Wait(); err != nil {
log.Fatalf("tarCmd.Wait: %v", err)
}
tarOut.Close()

if err := zstdCmd.Wait(); err != nil {
log.Fatalf("zstdCmd.Wait: %v", err)
}
zstdOut.Close()

if err := ageCmd.Wait(); err != nil {
log.Fatalf("ageCmd.Wait: %v", err)
}

io.Copy(f, &buffer)

err = uploadToS3(encryptedFile, config)
if err != nil {
log.Fatalf("error uploading file to s3: %v", err)
}
pushNotify(config)
}

func uploadToS3(filename string, config Config) error {
sess, _ := session.NewSession(&aws.Config{
Endpoint: aws.String(config.Endpoint),
Region: aws.String("us-west-000"),
Credentials: credentials.NewStaticCredentials(config.KeyID, config.KeySec, ""),
})

file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()

uploader := s3manager.NewUploader(sess)

_, err = uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(config.BucketName),
Key: aws.String(filepath.Base(filename)),
Body: file,
})

return err
}

func pushNotify(conf Config) {
message := fmt.Sprintf("Project %s Backup Success", conf.DstName)
client := &http.Client{}
_, err := client.PostForm("https://api.pushover.net/1/messages.json",
url.Values{"token": {conf.PushOverToken}, "user": {conf.PushOverUser}, "message": {message}})
if err != nil {
log.Fatal(err)
return
}
}

config.yaml 配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# B2 配置
endpoint: "s3.us-west-000.backblazeb2.com"
key_id: ""
key_sec: ""
bucket_name: ""

src_folder_path: ""
dst_name: "note"

# 可执行文件配置
tar_path: "/usr/bin/tar"
zstd_path: "/opt/homebrew/bin/zstd"
age_path: "/opt/homebrew/bin/age"

age_key: ""

# 通知配置
push_over_user: ""
push_over_token: ""

其中涉及到了age加密的东西,可以参照https://github.com/FiloSottile/age

Mac 系统设置

代码写完后,就是运维了,我主力桌面操作系统是Mac,因此我拿它举例,会每个月1号的早上九点进行备份,即使那个时候电脑没开也没关系,它会在合适的时候执行一次

  1. 编写一个配置文件放到~/Library/LaunchAgents/com.ficapy.notebackup.plist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ficapy.notebackup</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/script/s3_backup</string>
<string>-f</string>
<string>/usr/local/script/notebackup.yaml</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Day</key>
<integer>1</integer>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>
  1. 管理
1
2
3
4
5
6
7
8
9
10
### 加载
launchctl load ~/Library/LaunchAgents/com.ficapy.notebackup.plist
### 卸载(修改了该plist文件后,需要先卸载,再执行加载)
launchctl unload ~/Library/LaunchAgents/com.ficapy.notebackup.plist
### 查看状态
launchctl list | grep com.ficapy.notebackup
### 手动触发,马上执行
launchctl kickstart -k gui/$(id -u)/com.ficapy.notebackup
### 检查plist文件是否有误
launchctl load ~/Library/LaunchAgents/com.ficapy.notebackup.plist

建议使用launchctl手动触发一次,因为因为权限限制,执行的时候可能会让你提示啥的,所以先执行一次确保真的能跑通

  1. 其他

使用系统自带app reminders创建个提醒,提醒自己每个月的1号,pushover是否真的收到了备份成功的通知