пишу на пару с ИИ приложение на Go для windows, в котором есть функция отслеживания изменений в папке: создан, изменен, удален, переименован. Вот по поводу последнего есть проблема. Мне отдают только новое имя файла, старое мне недоступно. Это либо галлюцинации либо действительно только так: ии говорит, что невозможно понять, какое имя было до, потому что:
- renamedFrom (с маленькой буквы) — это неэкспортируемое (приватное) поле внутри структуры fsnotify.Event.
- Правила языка Go запрещают нам напрямую обращаться к приватным полям из другого пакета.
- Наш отладочный вывод log.Printf("%#v", event) мог показать это поле, потому что он использует специальные механизмы "рефлексии", но в обычном коде мы не можем написать event.renamedFrom.
Он предлагает решать проблему с помощью эвристики (хранить в буфере rename и ловить следующий create), что я считаю шизой на ранней стадии. При том что в логах мы видим вот такую картину
2025/10/05 22:48:31 [RENAME "C:\CODE\temp\~WRD0000.tmp"]
2025/10/05 22:48:31 [CREATE "C:\CODE\temp\afa.docx" ← "C:\CODE\temp\~WRD0000.tmp"]
Как видно, есть указатель при CREATE, если этот CREATE был на основе RENAME. Ну и как он сказал выше, использовать мы эти данные не можем.
Почему я считаю это галлюцинациями: когда я пытался понять, как внутри fsnotify работает - этот клоун сказал, что внутри ровно эта эвристика и используется. Что является гоневом, потому что в API Windows есть вот такое: FILE_ACTION_RENAMED_NEW_NAME (0x00000005).
вот код, который у меня сейчас
package main
import (
"log"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
var (
debounceTimers = make(map[string]*time.Timer)
debounceMu sync.Mutex
)
var (
// Карта для отслеживания файлов, которые мы меняем сами.
internallyModifiedFiles = make(map[string]time.Time)
internalModMu sync.Mutex
)
func startFileWatcher(folderPath string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("ОШИБКА: не удалось создать наблюдателя: %v", err)
return
}
defer watcher.Close()
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// Пропускаем "внутренние" изменения
internalModMu.Lock()
ignoreUntil, isInternal := internallyModifiedFiles[event.Name]
if isInternal && time.Now().Before(ignoreUntil) {
internalModMu.Unlock()
log.Printf("Watcher: Игнорируем внутреннее изменение для %s", filepath.Base(event.Name))
continue
}
if isInternal {
delete(internallyModifiedFiles, event.Name)
}
internalModMu.Unlock()
// Пропускаем системные/временные файлы
baseName := filepath.Base(event.Name)
if shouldIgnoreFile(baseName) {
continue
}
// --- ПРОСТАЯ И НАДЕЖНАЯ ЛОГИКА ---
// ОБРАБОТКА УДАЛЕНИЯ И ПЕРЕИМЕНОВАНИЯ (как простое удаление)
if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
log.Printf("Обнаружено удаление/переименование: %s. Добавляем в очередь.", baseName)
relativePath, err := filepath.Rel(settings.LocalPath, event.Name)
if err == nil {
syncQueue <- SyncEvent{Path: filepath.ToSlash(relativePath), Type: "delete"}
}
continue
}
// ОБРАБОТКА СОЗДАНИЯ И ИЗМЕНЕНИЯ (через Debounce)
if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) {
filePath := event.Name
const debounceDuration = 4 * time.Second
debounceMu.Lock()
timer, exists := debounceTimers[filePath]
if exists {
timer.Reset(debounceDuration)
} else {
timer = time.AfterFunc(debounceDuration, func() {
log.Printf("Debounce: Таймер для '%s' сработал. Добавляем в очередь.", baseName)
syncQueue <- SyncEvent{Path: filePath, Type: "upsert"}
debounceMu.Lock()
delete(debounceTimers, filePath)
debounceMu.Unlock()
})
debounceTimers[filePath] = timer
}
debounceMu.Unlock()
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("Ошибка наблюдателя:", err)
}
}
}()
err = watcher.Add(folderPath)
if err != nil {
log.Printf("ОШИБКА: не удалось добавить папку '%s' для отслеживания: %v", folderPath, err)
return
}
log.Printf("Начато отслеживание изменений в папке: %s", folderPath)
<-make(chan bool)
}
func markFileAsInternallyModified(path string) {
internalModMu.Lock()
defer internalModMu.Unlock()
internallyModifiedFiles[path] = time.Now().Add(10 * time.Second)
}
// shouldIgnoreFile - вспомогательная функция для проверки имени файла.
func shouldIgnoreFile(name string) bool {
baseName := filepath.Base(name)
if baseName == lockFileName || baseName == syncStateFileName || strings.HasPrefix(baseName, "~") {
return true
}
return false
}