Как правильно подойти к вопросу: можно ли получить исходное имя файла до переименования из fsnotify?

Ссылка скопирована
Go
1 ответ

Сейчас ситуация такая: пишу на пару с ИИ приложение на 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 }

Нужно решить такую задачу?

Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.

Заказать помощь
Лучший ответ
1
Mobile-редакция Ответ

В fsnotify получить “старое имя файла до переименования” напрямую нельзя. Библиотека получает события от ОС, а большинство файловых API отдают событие переименования уже как факт изменения, без пары old path -> new path. Поэтому если приложение должно знать оба имени, эту связь нужно восстанавливать на уровне вашей логики.

Практический подход такой: держите небольшое состояние каталога до события и после события. При событии Rename или Create перечитывайте список файлов и сравнивайте с предыдущим снимком. Если один файл пропал и один появился за короткий интервал, можно считать это переименованием.

type Snapshot map[string]os.FileInfo
 
func scan(dir string) Snapshot {
    result := Snapshot{}
    entries, _ := os.ReadDir(dir)
    for _, entry := range entries {
        info, err := entry.Info()
        if err == nil {
            result[filepath.Join(dir, entry.Name())] = info
        }
    }
    return result
}

type Snapshot map[string]os.FileInfo func scan(dir string) Snapshot { result := Snapshot{} entries, _ := os.ReadDir(dir) for _, entry := range entries { info, err := entry.Info() if err == nil { result[filepath.Join(dir, entry.Name())] = info } } return result }

Для более надёжного сопоставления сравнивайте не только имя, но и размер, время изменения, inode/file id, если платформа позволяет. На Linux можно смотреть inode, на Windows это сложнее, но можно использовать дополнительные признаки.

Важно учитывать, что редакторы часто не делают обычный rename. Они создают временный файл, пишут туда данные, удаляют старый и переименовывают временный. Поэтому события могут выглядеть как цепочка Create, Write, Remove, Rename.

Итог: из одного события fsnotify старое имя получить нельзя. Делайте слой отслеживания состояния каталога, сравнивайте снимки и восстанавливайте связь эвристически. Для критичных операций лучше логировать переименование внутри приложения, которое само выполняет rename.

Другие ответы (0)

Пока нет других ответов. Будьте первым, кто поможет автору.

Ответить на вопрос

комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Вам также может быть интересно