Задать вопрос
@accountnujen

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

пишу на пару с ИИ приложение на 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
}

  • Вопрос задан
  • 151 просмотр
Подписаться 2 Простой 9 комментариев
Пригласить эксперта
Ответы на вопрос 1
Vamp
@Vamp
Судя по тикету, вашей проблеме в прошлом году стукнуло юбилейные 10 лет. Насколько я понял, сложность в реализации этой фичи заключается в том, что файл может быть перемещён из папки, которую вы отслеживаете, в неотслеживаемую папку. Либо если файл перемещается в/из файловой системы, не поддерживающей события (сетевые маунты какие-нибудь). И если для windows получить нормальный ивент ещё возможно (хотя опять же вопросы как оно работает если в переименовании участвует неотслеживаемая папка или сетевая шара), то в остальных ОС такого нет. А так как библиотека кросплатформенная, то она реализует только пересекающийся между платформами функционал.

Так как унверсального решения не существует, то теперь это ваша проблема. Вы можете использовать эвристику, как советует ИИ. Либо напрямую работать с win api через cgo.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы