石头崽崽 發表於 2025-11-18 09:51:19

使用Go开发一个文件同步小工具(附源码)

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">它是怎么工作的</a></li><li><a href="#_label1">为什么说它&quot;佛系&quot;</a></li><li><a href="#_label2">使用须知(别踩坑)</a></li><li><a href="#_label3">改进建议(留给未来的你)</a></li><li><a href="#_label4">源码奉上</a></li><li><a href="#_label5">结语</a></li></ul></div><p>&quot;你复制,我粘贴;你改了,我跟着动。&quot; &mdash;&mdash; FileSync 的座右铭</p>
<p>大家好!今天我要给大家介绍一个我最近写的小玩具&mdash;&mdash;FileSync。它不是什么高大上的分布式同步系统,也不是什么带 GUI 界面的炫酷软件,而是一个朴实无华、靠命令行吃饭、还带点&quot;佛系&quot;气质的 Go 语言小工具。</p>
<p>它的任务很简单:把 A 文件夹里的内容,原封不动地同步到 B 文件夹里,并且自动跳过日志目录(比如 \logs\、\log\、\runlog\),避免把一堆没用的日志也搬过去占地方。</p>
<p>最重要的是&mdash;&mdash;它还能优雅退出!只要你在终端敲个 exit,它就会乖乖收工,不闹脾气&nbsp;</p>
<p class="maodian"><a name="_label0"></a></p><h2>它是怎么工作的</h2>
<p>FileSync 的逻辑其实非常直白:</p>
<ul><li>启动主线程:开始遍历源目录(fromDir)。</li><li>智能过滤:遇到名字里带 log 的文件夹?直接跳过!我们不关心日志,只关心&quot;正经 文件&quot;。</li><li>同步操作:<ul><li>如果是目录 &rarr; 在目标位置创建同名目录。</li><li>如果是文件 &rarr; 比较修改时间!只有源文件比目标新(或目标不存在),才复制过去,并保留原始修改时间。</li></ul></li><li>每小时扫一次:干完一轮活,就去&quot;打坐&quot;一小时(其实是 sleep 3600秒),然后继续巡逻。</li><li>随时听令退出:主 goroutine 在后台干活,主线程监听用户输入。一旦你说&quot;exit&quot;,它立刻收手,等所有任务结束再退出,绝不拖泥带水。</li></ul>
<p>是不是有点像一个勤恳又听话的数字园丁?</p>
<p class="maodian"><a name="_label1"></a></p><h2>为什么说它&quot;佛系&quot;</h2>
<ul><li>它不会疯狂刷屏告诉你&quot;我又复制了一个文件!&quot;(虽然现在会打印路径,但你可以轻松注释掉)。</li><li>它不争不抢,每小时才工作一次,其余时间都在&quot;冥想&quot;。</li><li>它尊重文件的&quot;前世今生&quot;&mdash;&mdash;连修改时间都要原样保留,生怕打扰了文件的情绪。</li><li>你说&quot;走&quot;,它绝不赖着不走,还会礼貌地说一句:&quot;FileSync soft exit&quot;。</li></ul>
<p>这哪是程序?分明是个修行千年的老和尚写的代码&nbsp;</p>
<p class="maodian"><a name="_label2"></a></p><h2>使用须知(别踩坑)</h2>
<p>必须传两个参数:<code>./FileSync /path/to/source /path/to/target</code></p>
<p>路径分隔符注意:代码里用了 <code>\\</code> 来判断 Windows 风格的日志路径。如果你在 Linux/macOS 上跑,可能需要改成 <code>/logs/</code>。不过 filepath.Walk 是跨平台的,所以实际运行没问题,只是过滤逻辑可能失效(因为 Linux 路径不含反斜杠)。建议改成 <code>/</code> 或使用 filepath.Separator 更健壮。</p>
<p>权限问题:确保程序有读源目录、写目标目录的权限。</p>
<p>大文件警告:目前是全量复制,没做增量或断点续传,超大文件可能会卡一下。</p>
<p class="maodian"><a name="_label3"></a></p><h2>改进建议(留给未来的你)</h2>
<p>用 fsnotify 监听文件变化,实时同步,告别&quot;每小时打坐&quot;。</p>
<p>支持配置文件,自定义忽略规则。</p>
<p>加日志输出开关,别总往 stdout 打。</p>
<p>支持双向同步 or 增量备份模式。</p>
<p>给它起个更酷的名字,比如 ZenSync?</p>
<p class="maodian"><a name="_label4"></a></p><h2>源码奉上</h2>
<p>下面就是这个&quot;佛系同步器&quot;的完整源码,Go 语言编写,简洁明了,欢迎拿去魔改!</p>
<div class="jb51code"><pre class="brush:go;">package main

import (
        "fmt"
        "io"
        "os"
        "path/filepath"
        "strings"
        "sync"
        "time"
)

var exit = false         // 退出状态控制
var wg sync.WaitGroup      // 保证正常退出

func main() {
        fmt.Println("FileSync soft run")
        if len(os.Args) &lt; 3 {
                fmt.Println("Usage: FileSync &lt;source_dir&gt; &lt;target_dir&gt;")
                return
        }
        fromDir := os.Args
        toDir := os.Args
        wg.Add(1)
        go mainRun(fromDir, toDir) // 启动工作主线程

        for {
                var cmd string
                fmt.Scanln(&amp;cmd)
                fmt.Println("cmd:", cmd)
                if cmd == "exit" {
                        exit = true
                        break
                } else {
                        fmt.Println("unknow command")
                        fmt.Println("exit exit soft")
                }
        }
        wg.Wait()
        fmt.Println("FileSync soft exit")
}

func mainRun(fromDir, toDir string) {
        defer wg.Done()
        for !exit {
                filepath.Walk(fromDir, func(path string, info os.FileInfo, err error) error {
                        if err != nil {
                                return nil
                        }
                        fmt.Println(path)
                        p := strings.ToLower(path)
                        // 跳过常见日志目录(注意:Windows风格路径)
                        if strings.Contains(p, "\\runlog\\") ||
                           strings.Contains(p, "\\logs\\") ||
                           strings.Contains(p, "\\log\\") {
                                return nil
                        }
                        if info.IsDir() {
                                syncDir(strings.Replace(path, fromDir, toDir, 1))
                        } else {
                                syncFile(path, strings.Replace(path, fromDir, toDir, 1))
                        }
                        return nil
                })
                // 每小时同步一次
                for i := 0; i &lt; 60*60; i++ {
                        time.Sleep(time.Second)
                        if exit {
                                break
                        }
                }
        }
}

func syncDir(dirname string) {
        if err := os.MkdirAll(dirname, 0777); err != nil {
                fmt.Println(err)
        }
}

func syncFile(f, t string) {
        CopyFile(f, t)
}

func CopyFile(f, t string) (written int64, err error) {
        src, err := os.Open(f)
        if err != nil {
                return
        }
        defer src.Close()
        st, _ := src.Stat()
        mt := st.ModTime()

        var dst *os.File
        if ft, exist := checkFileIsExist(t); exist {
                // 修改时间相同,跳过复制
                if st.ModTime().UnixNano() == ft.UnixNano() {
                        fmt.Println("修改时间相同,放弃")
                        return
                }
                dst, err = os.OpenFile(t, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0777)
                if err != nil {
                        fmt.Println(err)
                        return
                }
        } else {
                dst, err = os.Create(t)
                if err != nil {
                        fmt.Println(err)
                        return
                }
        }
        l, err := io.Copy(dst, src)
        dst.Close()
        if err == nil {
                // 保留原始修改时间
                err := os.Chtimes(t, mt, mt)
                if err != nil {
                        fmt.Println(err)
                }
        }
        return l, err
}

func checkFileIsExist(filename string) (time.Time, bool) {
        fi, err := os.Stat(filename)
        if os.IsNotExist(err) {
                return time.Now(), false
        } else if err != nil {
                return time.Now(), false
        }
        return fi.ModTime(), true
}
</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>结语</h2>
<p>虽然这个小工具简单,但它体现了 Go 语言的并发之美(一个 goroutine 干活,主线程监听退出)、文件操作的便捷性,以及&mdash;&mdash;一点点程序员的幽默感。</p>
<p>下次当你需要一个轻量、可控、不吵不闹的同步脚本时,不妨试试这个&quot;佛系 FileSync&quot;。说不定,它还能帮你悟出点编程禅意呢&nbsp;</p>
<p>代码已开源,心法自悟。</p>
<p>P.S. 如果你在 macOS/Linux 上使用,请记得把 <code>\\log\\</code> 这类判断改成 <code>/log/</code>,或者更优雅地用 filepath.Join(&quot;log&quot;) 来处理路径分隔符哦!</p>
頁: [1]
查看完整版本: 使用Go开发一个文件同步小工具(附源码)