星河避难所

返回

服务器文件变更同步小脚本Blur image

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

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

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

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

话不多说直接上代码

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

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
}
plaintext
服务器文件变更同步小脚本
https://hejunjie.life/blog/44283ef4/
作者 何俊杰
发布时间 2024年10月14日
版权信息 CC BY-NC-SA 4.0
评论似乎卡住了,尝试刷新?✨