最近发现用于为用户提供下载服务的服务器带宽承载压力逐渐增大,我建议使用阿里云的 OSS 来解决这个问题,毕竟 OSS 具备高效的存储和分发能力。

然而,老板坚持选择多台服务器结合 CDN 进行内容分发。

虽然这种方案可以缓解部分流量压力,但每次有内容更新时都需要手动同步到每台服务器。

为了简化这一过程,我编写了一个自动同步的小脚本,来实现内容的快速分发

话不多说直接上代码

当前脚本为检测特定目录下.apk文件的变更,根据实际需要调整第66行即可

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package main

import (
"fmt"
"io"
"log"
"sort"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/fsnotify/fsnotify"
)

var lastEventTime = make(map[string]time.Time)
var debounceDuration = 10 * time.Second // 可调整的去重时间间隔

// 监测路径
var rootDir = "/www/wwwroot/ftp"

// 目标服务器列表
var servers = []string{
"同步服务器1登录账号@同步服务器1IP:/",
"同步服务器2登录账号@同步服务器2IP:/",
"同步服务器3登录账号@同步服务器3IP:/",
"root@47.98.152.221:/",
}

// 子目录与目标路径的映射
var pathMap = map[string]string{
"本地服务器路径标识": "同步服务器路径(从跟路径起算)",
"本地服务器路径标识": "同步服务器路径(从跟路径起算)",
"Anxin": "www/wwwroot/Anxin",
}

func main() {
// 设置日志输出到文件
logFile, err := os.OpenFile("script.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("无法打开日志文件: %v", err)
}
defer logFile.Close()
log.SetOutput(logFile)

// 创建文件监控器
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// 监测更多事件类型
if (event.Op&fsnotify.Write == fsnotify.Write ||
event.Op&fsnotify.Create == fsnotify.Create ||
event.Op&fsnotify.Rename == fsnotify.Rename ||
event.Op&fsnotify.Chmod == fsnotify.Chmod) &&
strings.HasSuffix(event.Name, ".apk") { // 判断是否是.apk文件
// 去重逻辑
if lastTime, exists := lastEventTime[event.Name]; exists {
if time.Since(lastTime) < debounceDuration {
// 如果上次处理时间在去重时间间隔内,跳过此事件
continue
}
}
lastEventTime[event.Name] = time.Now()
log.Printf("监测到文件变更: %s, 事件类型: %v\n", event.Name, event.Op)
// 调用 scp 同步文件
err := syncFile(event.Name)
if err != nil {
log.Printf("文件同步失败: %v", err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("错误:", err)
}
}
}()
// 监控目录及子目录
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
err = watcher.Add(path)
if err != nil {
log.Fatal(err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
<-done
}

// syncFile 使用 scp 同步文件到多台服务器的不同路径,并复制到本地对应目录
func syncFile(filePath string) error {
// 找到文件所属的子目录
subDir := ""
// 先按路径长度降序排序,确保长名称优先匹配
for _, dir := range sortedKeys(pathMap) {
if strings.Contains(filePath, dir) {
subDir = dir
break
}
}
if subDir == "" {
return fmt.Errorf("文件没有匹配的目标目录: %s", filePath)
}
// 获取同步目标路径
targetPath := pathMap[subDir]
fmt.Println("匹配目录:", targetPath)

// 执行 scp 命令同步文件到每个服务器
for _, server := range servers {
// 组合目标路径
destination := fmt.Sprintf("%s%s", server, targetPath)
// 构建并执行 scp 命令
cmd := exec.Command("scp", filePath, destination)
// 捕获输出和错误信息
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("执行 scp 命令失败: %v, 输出: %s", err, output)
return err
}
log.Printf("文件 %s 同步到 %s\n", filePath, destination)
}

// 复制文件到本地对应的目录
localDestination := filepath.Join(rootDir, targetPath)
err := copyFileToLocal(filePath, localDestination)
if err != nil {
log.Printf("文件复制到本地失败: %v", err)
return err
}
log.Printf("文件 %s 复制到本地目录 %s\n", filePath, localDestination)

return nil
}

// copyFileToLocal 将文件复制到本地目录
func copyFileToLocal(srcPath, dstDir string) error {
// 确保目标目录存在
if err := os.MkdirAll(dstDir, os.ModePerm); err != nil {
return fmt.Errorf("创建本地目录失败: %v", err)
}
// 获取文件名
fileName := filepath.Base(srcPath)
// 目标文件路径
dstPath := filepath.Join(dstDir, fileName)
// 打开源文件
srcFile, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("打开源文件失败: %v", err)
}
defer srcFile.Close()
// 创建目标文件
dstFile, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("创建目标文件失败: %v", err)
}
defer dstFile.Close()
// 复制文件内容
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return fmt.Errorf("复制文件内容失败: %v", err)
}
return nil
}

// sortedKeys 返回按长度降序排序的键列表
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return len(keys[i]) > len(keys[j])
})
return keys
}