"你复制,我粘贴;你改了,我跟着动。" —— FileSync 的座右铭
大家好!今天我要给大家介绍一个我最近写的小玩具——FileSync。它不是什么高大上的分布式同步系统,也不是什么带 GUI 界面的炫酷软件,而是一个朴实无华、靠命令行吃饭、还带点"佛系"气质的 Go 语言小工具。
它的任务很简单:把 A 文件夹里的内容,原封不动地同步到 B 文件夹里,并且自动跳过日志目录(比如 \logs\、\log\、\runlog\),避免把一堆没用的日志也搬过去占地方。
最重要的是——它还能优雅退出!只要你在终端敲个 exit,它就会乖乖收工,不闹脾气 😌
🧠 它是怎么工作的?
FileSync 的逻辑其实非常直白:
- 启动主线程:开始遍历源目录(fromDir)。
- 智能过滤:遇到名字里带 log 的文件夹?直接跳过!我们不关心日志,只关心"正经文件"。
- 同步操作:
- 如果是目录 → 在目标位置创建同名目录。
- 如果是文件 → 比较修改时间!只有源文件比目标新(或目标不存在),才复制过去,并保留原始修改时间。
- 每小时扫一次:干完一轮活,就去"打坐"一小时(其实是 sleep 3600秒),然后继续巡逻。
- 随时听令退出:主 goroutine 在后台干活,主线程监听用户输入。一旦你说"exit",它立刻收手,等所有任务结束再退出,绝不拖泥带水。
是不是有点像一个勤恳又听话的数字园丁?🌱
😂 为什么说它"佛系"?
- 它不会疯狂刷屏告诉你"我又复制了一个文件!"(虽然现在会打印路径,但你可以轻松注释掉)。
- 它不争不抢,每小时才工作一次,其余时间都在"冥想"。
- 它尊重文件的"前世今生"——连修改时间都要原样保留,生怕打扰了文件的情绪。
- 你说"走",它绝不赖着不走,还会礼貌地说一句:"FileSync soft exit"。
这哪是程序?分明是个修行千年的老和尚写的代码 🙏
⚠️ 使用须知(别踩坑)
- 必须传两个参数:
./FileSync /path/to/source /path/to/target - 路径分隔符注意:代码里用了
\\来判断 Windows 风格的日志路径。如果你在 Linux/macOS 上跑,可能需要改成/logs/。不过 filepath.Walk 是跨平台的,所以实际运行没问题,只是过滤逻辑可能失效(因为 Linux 路径不含反斜杠)。建议改成/或使用 filepath.Separator 更健壮。 - 权限问题:确保程序有读源目录、写目标目录的权限。
- 大文件警告:目前是全量复制,没做增量或断点续传,超大文件可能会卡一下。
💡 改进建议(留给未来的你)
- 用 fsnotify 监听文件变化,实时同步,告别"每小时打坐"。
- 支持配置文件,自定义忽略规则。
- 加日志输出开关,别总往 stdout 打。
- 支持双向同步 or 增量备份模式。
- 给它起个更酷的名字,比如 ZenSync?
📜 源码奉上!
下面就是这个"佛系同步器"的完整源码,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) < 3 {
fmt.Println("Usage: FileSync <source_dir> <target_dir>")
return
}
fromDir := os.Args[1]
toDir := os.Args[2]
wg.Add(1)
go mainRun(fromDir, toDir) // 启动工作主线程
for {
var cmd string
fmt.Scanln(&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 < 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
}
🎉 结语
虽然这个小工具简单,但它体现了 Go 语言的并发之美(一个 goroutine 干活,主线程监听退出)、文件操作的便捷性,以及——一点点程序员的幽默感。
下次当你需要一个轻量、可控、不吵不闹的同步脚本时,不妨试试这个"佛系 FileSync"。说不定,它还能帮你悟出点编程禅意呢 🧘♂️
代码已开源,心法自悟。
—— 一位不想被日志淹没的 Go 程序员
P.S. 如果你在 macOS/Linux 上使用,请记得把 \\log\\ 这类判断改成 /log/,或者更优雅地用 filepath.Join("log") 来处理路径分隔符哦!
往期部分文章列表
- 用 Go 写个"端口扫描器",100 行代码扫描你家路由器?
- 从"双击打不开"到"管理员都服了":用 Go 打造你的专属 .mgx 编辑器
- 震惊!Go语言居然可以这样玩Windows窗口,告别臃肿GUI库
- 剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器
- 一文讲透 Go 的 defer:你的"善后管家",别让他变成"背锅侠"!
- 你知道程序怎样优雅退出吗?—— Go 开发中的"体面告别"全指南
- 用golang解救PDF文件中的图片只要200行代码!
- 200KB 的烦恼,Go 语言 20 分钟搞定!—— 一个程序员的图片压缩自救指南
- 从"CPU 烧开水"到优雅暂停:Go 里 sync.Cond 的正确打开方式
- 时移世易,篡改天机:吾以 Go 语令 Windows 文件"返老还童"记
- golang圆阵列图记:天灵灵地灵灵图标排圆形
- golang解图记
- 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
- 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
- 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
- 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
- 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
- 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
- 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器