监听文件夹新增文件

监听文件夹新增文件 #

业务需求,需要监听一个文件夹是否有新增文件,文件数目1000w,还要考虑文件是否已经上传完毕,程序跑在docker内。

在 Linux 环境下,处理“大文件异步写入”是一个经典的同步问题。如果你直接在监视到文件创建时就读取,由于文件还在写入,你的程序会读取到不完整的数据。

以下是几种最常用且可靠的解决方案,按推荐程度排序:

采用“写入-重命名”模式(最推荐,最稳健)(我的业务有技术人员私自上传,被pass) #

这是工业界的标准做法。其核心思想是:不要让监听程序直接看到正在写入的文件。

  • 流程:写入程序先将文件写到一个临时目录,或者起一个特殊后缀名(例如:data.xml.tmp)。文件完全写完并关闭后,调用 mv (rename) 将文件移动到监听目录,或者重命名为最终文件名(data.xml)。
  • 为什么有效: 在同一个文件系统中,mv 操作是原子的(Atomic)。这意味着监听程序感知到文件存在的那一瞬间,文件已经完整地在那儿了。
  • **代码逻辑:**监听程序只处理 *.xml,忽略 *.tmp。

配合“标志文件”(Marker Files) #

如果你对写入端有控制权,可以在写完主文件后,再创建一个极小的 .done 文件。

  • **流程:**写入 bigfile.dat。写完后,创建一个 bigfile.dat.ok 空文件。
  • 监听端: 只监视 .ok 后缀的文件,一旦发现,就去处理同名的 .dat 文件。

观察文件大小和修改时间(轮询法) #

这是最原始的方法,适用于无法获取内核通知的情况(如 NFS 挂载目录)。

  • **逻辑:**获取文件当前的 size 和 mtime。等待一段时间(如 1-2 秒)。再次获取 size 和 mtime。如果两次结果完全一致,则初步判定文件写入完成。
  • 缺点: 不够优雅,且如果写入方网络很慢,可能会产生误判。

经过测试,不可用,windows在文件写入开始,日期和大小就已经写入,并不会随着数据传输而改变。Linux没有测。

循环遍历问题 #

根据您的需求(找到新增文件并添加),我给您分析几种合理的处理方案:

方案一:文件系统监控(fsnotify)⭐⭐⭐⭐⭐ #

原理:实时监听文件系统事件

优点:
- 实时性最高,文件创建后立即响应
- CPU占用极低,事件驱动
- 适合长期运行的服务

缺点:
- Windows上监控数量有限制(约64K句柄)
- 需要递归添加所有子目录到监控
- 程序重启期间的文件变化会遗漏

适用场景:文件不频繁、目录层级可控

方案二:增量扫描(基于修改时间)⭐⭐⭐⭐ #

原理:只扫描最近修改过的目录

// 记录每个目录的最后修改时间
dirModTimes map[string]time.Time

// 扫描时先检查目录修改时间
if dirInfo.ModTime().After(lastScanTime) {
    // 只扫描这个目录
}
优点:
- 实现简单,可靠性高
- 自动跳过未变化的目录(90%+的目录可跳过)
- 不依赖第三方库

缺点:
- 仍需遍历目录树(但大部分只stat不readdir)
- 某些操作系统目录时间更新不及时

适用场景:目录结构稳定、文件不频繁变化

方案三:文件索引缓存(类似数据库)⭐⭐⭐ #

原理:维护文件路径的哈希索引

// 记录目录内容的快照(文件名哈希)
dirSnapshots map[string]map[string]struct{}

// 对比快照找出新增文件
优点:
- 精确知道哪些是新增文件
- 可以检测删除、重命名等变化

缺点:
- 内存占用较大(大量文件时)
- 初始化时需要全量扫描
- 实现复杂度较高

适用场景:需要完整的文件变更追踪

方案四:混合方案(推荐)⭐⭐⭐⭐⭐ #

原理:fsnotify + 定期全量扫描

fsnotify处理99%的实时变化
+
低频全量扫描兜底(如每小时1次)
优点:
- 兼顾实时性和可靠性
- fsnotify失效时有backup
- 程序重启遗漏的文件会被补扫

缺点:
- 代码稍复杂

适用场景:生产环境(推荐)

文件系统监控(fsnotify)的缺点 #

1. Windows 系统限制 ⚠️ #

监控句柄数量限制 #
// Windows 每个进程最多约 64K 个监控句柄
// 假设您的目录结构:
scanPath/
  ├── case1/          // 1个监控
  ├── case2/          // 1个监控
     ├── subdir1/    // 1个监控
     └── subdir2/    // 1个监控
  └── case3/          // 1个监控

// 如果有 10,000 个子目录 = 10,000 个监控句柄

实际影响

  • 您的证据文件可能按案件分目录存储
  • 如果案件数量超过几万个,会触发限制
  • 错误信息:too many open filesaccess denied
性能开销 #
// 每个目录监控都会消耗:
- 内核对象句柄
- 内存缓冲区
- CPU事件处理

// 10,000个目录 ≈ 50-100MB 内存 + 持续的CPU开销

2. 程序重启期间的盲区 🚨 #

时间窗口问题 #
T1: 程序停止 (fsnotify 停止监听)
    ↓
    [文件A.bcp 被复制]  ← 这个文件会被遗漏!
    [文件B.bcp 被复制]  ← 这个文件会被遗漏!
    ↓
T2: 程序重启
    initCache() 只加载数据库已有文件
    但文件A、B 不在数据库中!

真实场景

15:00 - 程序升级重启
15:01 - 办案人员复制了 5 个证据文件
15:02 - 程序启动完成
结果:这 5 个文件永远不会被扫描!

当前代码的问题

func (s *Scanner) initCache(scanPath string) {
    // 只从数据库加载已存在的文件
    _, records, err := getRecords(&QueryOptions{})
    
    // ❌ 没有实际扫描文件系统!
    // 重启期间新增的文件不会被发现
}

3. 事件丢失风险 💥 #

事件缓冲区溢出 #
// fsnotify 内部有事件队列
watcher.Events (chan Event, 缓冲区大小有限)

// 如果短时间内大量文件变化:
复制 1000 个文件  产生 1000  CREATE 事件
 事件队列满  后续事件被丢弃
 部分文件永远不会被处理

触发场景

# 办案人员批量复制案件文件夹
xcopy /s D:\evidence\case_2024_001 E:\scanPath\

# 可能产生:
- 1000 个文件 CREATE 事件
- 100 个目录 CREATE 事件
- 如果处理速度跟不上 → 事件丢失
处理速度瓶颈 #
case event := <-watcher.Events:
    // 您的处理逻辑:
    GetBCPInfo(filePath)      // 解析文件 (可能很慢)
    insertFile(bcpInfo, ...)   // 数据库插入 (IO 操作)
    
// 如果单个文件处理需要 100ms
// 每秒只能处理 10 个文件
// 但复制可能每秒产生 100 个事件 → 队列堆积

4. 网络共享目录不支持 🌐 #

// ❌ 不支持的场景
scanPath = "\\\\server\\evidence"  // SMB/CIFS 网络共享
scanPath = "Z:\\"                   // 映射的网络驱动器

// fsnotify 依赖操作系统的文件系统通知
// 网络共享无法可靠触发 CREATE 事件

您的实际情况

  • 如果证据文件存储在 NAS 或文件服务器
  • fsnotify 可能完全无法工作

5. 新目录监控延迟 ⏱️ #

监控添加的竞态条件 #
// 时间线:
T1: 检测到目录 "case123" 创建
T2: 开始添加监控 watcher.Add("case123")
     (耗时 10-50ms)
T3: 此时如果有文件被复制到 case123/
     可能遗漏这个文件的 CREATE 事件
T4: 监控添加完成

// 代码中的问题:
if event.Op&fsnotify.Create == fsnotify.Create {
    if info.IsDir() {
        time.Sleep(50 * time.Millisecond)  // 延迟不够
        s.addDirWatch(event.Name)           // 监控还没生效
    }
}

6. 事件类型复杂性 🔄 #

不同操作系统的事件差异 #
// Windows:
复制文件  CREATE 事件

// Linux:
复制文件  CREATE + WRITE + CHMOD 多个事件

// macOS:
复制文件  CREATE + MODIFY + ATTRIB 多个事件

导致的问题

// 同一个文件可能触发多次处理
case event := <-watcher.Events:
    // Windows: 1 次 CREATE
    // Linux: 3 次事件 → 可能尝试插入 3 次数据库
重命名/移动的处理 #
// 用户操作:移动文件
move case1/file.bcp  case2/file.bcp

// fsnotify 事件:
1. case1/file.bcp  RENAME (from)
2. case2/file.bcp  RENAME (to)

// ❌ 您的代码只处理 CREATE,会遗漏这种情况

7. 内存泄漏风险 💾 #

监控目录持续增长 #
type Scanner struct {
    watchedDirs map[string]time.Time  // 只增不减
}

// 如果案件目录不断增加:
// 1月: 1000 个目录
// 6月: 10000 个目录
// 1年: 50000 个目录

// map 会持续占用内存,即使某些目录已经不再使用

8. 符号链接和硬链接问题 🔗 #

// 如果存在符号链接:
scanPath/link_to_evidence  /other/path/evidence

// 可能导致:
1. 重复监控同一个目录
2. 文件路径解析错误
3. 死循环循环链接

#