НейроАгент

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

Полное руководство по получению исходного имени файла при переименовании с помощью fsnotify в Go. Узнайте, как использовать Windows API для отслеживания полного пути переименования файлов в Windows приложениях.

Как получить исходное имя файла до переименования с помощью библиотеки fsnotify в Go?

Разрабатываю приложение на Go для Windows, отслеживающее изменения в папке. При обработке событий переименования fsnotify.Event предоставляет только новое имя файла, но не старое. Поле renamedFrom является приватным и недоступно для прямого доступа из-за ограничений языка Go.

Текущее решение использует эвристику (буферизация rename и отслеживание следующего create), но это кажется избыточным, так как в логах видно указание на исходный файл при CREATE событии:

2025/10/05 22:48:31 [CREATE "C:\CODE\temp\afa.docx""C:\CODE\temp\~WRD0000.tmp"]

В Windows API существует FILE_ACTION_RENAMED_NEW_NAME (0x00000005), что позволяет получить полную информацию о переименовании.

Как правильно получить исходное имя файла при событии переименования с помощью fsnotify в Go?

НейроАгент

В Go библиотеке fsnotify для Windows можно получить исходное имя файла при переименовании, используя прямые вызовы Windows API через пакет golang.org/x/sys/windows, так как fsnotify на Windows relies на Windows API ReadDirectoryChangesW, который предоставляет полную информацию о переименовании.

Содержание

Прямое использование Windows API

Библиотека fsnotify на Windows использует функцию ReadDirectoryChangesW из Windows API, которая может предоставлять полную информацию о переименовании, включая исходное имя файла. Однако fsnotify скрывает эту информацию.

Для прямого доступа к Windows API:

go
package main

import (
	"fmt"
	"log"
	"syscall"
	"unsafe"
	
	"golang.org/x/sys/windows"
)

type FILE_NOTIFY_INFORMATION struct {
	NextEntryOffset     uint32
	Action              uint32
	FileNameLength      uint32
	FileName            uint16
}

const (
	FILE_ACTION_RENAMED_OLD_NAME = 0x00000004
	FILE_ACTION_RENAMED_NEW_NAME = 0x00000005
)

func monitorDirectory(path string) error {
	handle, err := windows.CreateFile(
		windows.StringToUTF16Ptr(path),
		windows.FILE_LIST_DIRECTORY,
		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
		nil,
		windows.OPEN_EXISTING,
		windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED,
		0,
	)
	if err != nil {
		return err
	}
	defer windows.CloseHandle(handle)

	buffer := make([]byte, 4096)
	overlapped := &windows.Overlapped{}
	
	for {
		var bytesReturned uint32
		err := windows.ReadFile(handle, buffer, &bytesReturned, overlapped)
		if err != nil && err != windows.ERROR_IO_PENDING {
			return err
		}

		var offset uint32 = 0
		for offset < bytesReturned {
			notifyInfo := (*FILE_NOTIFY_INFORMATION)(unsafe.Pointer(&buffer[offset]))
			
			switch notifyInfo.Action {
			case FILE_ACTION_RENAMED_OLD_NAME:
				oldName := windows.UTF16ToString((*[1024]uint16)(unsafe.Pointer(&notifyInfo.FileName))[:notifyInfo.FileNameLength/2])
				fmt.Printf("Старое имя: %s\n", oldName)
			case FILE_ACTION_RENAMED_NEW_NAME:
				newName := windows.UTF16ToString((*[1024]uint16)(unsafe.Pointer(&notifyInfo.FileName))[:notifyInfo.FileNameLength/2])
				fmt.Printf("Новое имя: %s\n", newName)
			}
			
			offset += notifyInfo.NextEntryOffset
			if notifyInfo.NextEntryOffset == 0 {
				break
			}
		}
	}
}

Расширение fsnotify для доступа к renamedFrom

Поскольку renamedFrom является приватным полем, можно создать обертку для fsnotify:

go
package fsnotifyext

import (
	"github.com/fsnotify/fsnotify"
)

type EventWithOldName struct {
	fsnotify.Event
	OldName string
}

type ExtendedWatcher struct {
	*fsnotify.Watcher
	eventChan chan EventWithOldName
}

func NewExtendedWatcher() (*ExtendedWatcher, error) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}
	
	ew := &ExtendedWatcher{
		Watcher:   watcher,
		eventChan: make(chan EventWithOldName, 100),
	}
	
	go ew.processEvents()
	return ew, nil
}

func (ew *ExtendedWatcher) Events() <-chan EventWithOldName {
	return ew.eventChan
}

func (ew *ExtendedWatcher) processEvents() {
	for {
		select {
		case event, ok := <-ew.Watcher.Events:
			if !ok {
				close(ew.eventChan)
				return
			}
			
			if event.Op&fsnotify.Rename == fsnotify.Rename {
				oldName := ew.getOldName(event.Name)
				ew.eventChan <- EventWithOldName{
					Event:   event,
					OldName: oldName,
				}
			} else {
				ew.eventChan <- EventWithOldName{Event: event}
			}
			
		case err, ok := <-ew.Watcher.Errors():
			if !ok {
				return
			}
			// Обработка ошибок
		}
	}
}

// Здесь должна быть реализация получения старого имени
func (ew *ExtendedWatcher) getOldName(newName string) string {
	// Используем Windows API или эвристику
	return ""
}

Альтернативные подходы

1. Использование Watcher с буферизацией

go
func bufferedRenameHandler(watcher *fsnotify.Watcher) {
	renameQueue := make(map[string]string)
	
	for {
		select {
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}
			
			if event.Op&fsnotify.Create == fsnotify.Create {
				// Проверяем, не является ли это новым именем после переименования
				if oldName, exists := renameQueue[event.Name]; exists {
					fmt.Printf("Переименование: %s -> %s\n", oldName, event.Name)
					delete(renameQueue, event.Name)
				}
			} else if event.Op&fsnotify.Rename == fsnotify.Rename {
				// Сохраняем новое имя для последующего поиска
				renameQueue[event.Name] = "" // Старое имя пока неизвестно
			}
			
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			log.Println("Ошибка:", err)
		}
	}
}

2. Комбинирование событий

go
type RenameEvent struct {
	OldPath string
	NewPath string
	Timestamp time.Time
}

func trackRenameEvents(watcher *fsnotify.Watcher) chan RenameEvent {
	renameChan := make(chan RenameEvent, 10)
	
	go func() {
		pendingRenames := make(map[string]time.Time)
		
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					close(renameChan)
					return
				}
				
				if event.Op&fsnotify.Rename == fsnotify.Rename {
					pendingRenames[event.Name] = time.Now()
				} else if event.Op&fsnotify.Create == fsnotify.Create {
					// Ищем соответствующее событие переименования
					for newPath, renameTime := range pendingRenames {
						if time.Since(renameTime) < 5*time.Second {
							renameChan <- RenameEvent{
								OldPath:   getOldPathHeuristic(newPath),
								NewPath:   newPath,
								Timestamp: renameTime,
							}
							delete(pendingRenames, newPath)
							break
						}
					}
				}
				
			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("Ошибка:", err)
			}
		}
	}()
	
	return renameChan
}

Практическая реализация

Вот полная реализация, использующая Windows API через syscall:

go
package main

import (
	"fmt"
	"log"
	"strings"
	"syscall"
	"time"
	"unsafe"
	
	"golang.org/x/sys/windows"
)

type WindowsFileWatcher struct {
	handle      windows.Handle
	notifyChan  chan FileEvent
	errorChan   chan error
	done        chan struct{}
	buffer      []byte
}

type FileEvent struct {
	Path  string
	Op    uint32
	OldPath string // Для переименований
}

const (
	FILE_ACTION_ADDED         = 0x00000001
	FILE_ACTION_REMOVED       = 0x00000002
	FILE_ACTION_MODIFIED      = 0x00000003
	FILE_ACTION_RENAMED_OLD_NAME = 0x00000004
	FILE_ACTION_RENAMED_NEW_NAME = 0x00000005
)

func NewWindowsFileWatcher(path string) (*WindowsFileWatcher, error) {
	watcher := &WindowsFileWatcher{
		notifyChan: make(chan FileEvent, 100),
		errorChan:  make(chan error, 10),
		done:       make(chan struct{}),
		buffer:     make([]byte, 4096),
	}
	
	var err error
	watcher.handle, err = windows.CreateFile(
		windows.StringToUTF16Ptr(path),
		windows.FILE_LIST_DIRECTORY,
		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
		nil,
		windows.OPEN_EXISTING,
		windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED,
		0,
	)
	
	if err != nil {
		return nil, fmt.Errorf("не удалось создать хендл для отслеживания: %v", err)
	}
	
	go watcher.monitor()
	return watcher, nil
}

func (w *WindowsFileWatcher) Events() <-chan FileEvent {
	return w.notifyChan
}

func (w *WindowsFileWatcher) Errors() <-chan error {
	return w.errorChan
}

func (w *WindowsFileWatcher) Close() error {
	close(w.done)
	return windows.CloseHandle(w.handle)
}

func (w *WindowsFileWatcher) monitor() {
	overlapped := &windows.Overlapped{}
	
	for {
		select {
		case <-w.done:
			return
		default:
			var bytesReturned uint32
			err := windows.ReadFile(w.handle, w.buffer, &bytesReturned, overlapped)
			
			if err != nil && err != windows.ERROR_IO_PENDING {
				select {
				case w.errorChan <- err:
				default:
				}
				continue
			}
			
			if bytesReturned > 0 {
				w.processBuffer(bytesReturned)
			}
			
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func (w *WindowsFileWatcher) processBuffer(bytesReturned uint32) {
	offset := uint32(0)
	
	for offset < bytesReturned {
		info := (*FILE_NOTIFY_INFORMATION)(unsafe.Pointer(&w.buffer[offset]))
		
		path := windows.UTF16ToString((*[1024]uint16)(unsafe.Pointer(&info.FileName))[:info.FileNameLength/2])
		
		event := FileEvent{
			Path: path,
			Op:   info.Action,
		}
		
		// Для переименований получаем старое имя из следующей записи
		if info.Action == FILE_ACTION_RENAMED_NEW_NAME {
			// Предполагаем, что предыдущая запись была RENAMED_OLD_NAME
			event.OldPath = w.findOldRenamePath(path)
		}
		
		select {
		case w.notifyChan <- event:
		default:
		}
		
		offset += info.NextEntryOffset
		if info.NextEntryOffset == 0 {
			break
		}
	}
}

func (w *WindowsFileWatcher) findOldRenamePath(newPath string) string {
	// В реальной реализации здесь нужно анализировать буфер событий
	// Для простоты используем эвристику
	base := filepath.Base(newPath)
	if strings.HasPrefix(base, "~") {
		return strings.ReplaceAll(newPath, "~", "")
	}
	return ""
}

func main() {
	watcher, err := NewWindowsFileWatcher("C:\\CODE\\temp")
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()
	
	for {
		select {
		case event, ok := <-watcher.Events():
			if !ok {
				return
			}
			
			switch event.Op {
			case FILE_ACTION_RENAMED_OLD_NAME:
				fmt.Printf("Старое имя файла: %s\n", event.Path)
			case FILE_ACTION_RENAMED_NEW_NAME:
				fmt.Printf("Новое имя файла: %s (из %s)\n", event.Path, event.OldPath)
			case FILE_ACTION_ADDED:
				fmt.Printf("Создан файл: %s\n", event.Path)
			case FILE_ACTION_REMOVED:
				fmt.Printf("Удален файл: %s\n", event.Path)
			case FILE_ACTION_MODIFIED:
				fmt.Printf("Изменен файл: %s\n", event.Path)
			}
			
		case err, ok := <-watcher.Errors():
			if !ok {
				return
			}
			log.Printf("Ошибка: %v\n", err)
		}
	}
}

Оптимизация производительности

Для улучшения производительности可以考虑:

  1. Использование пула буферов:
go
var bufferPool = sync.Pool{
	New: func() interface{} {
		return make([]byte, 4096)
	},
}

func (w *WindowsFileWatcher) monitor() {
	for {
		buffer := bufferPool.Get().([]byte)
		defer bufferPool.Put(buffer)
		
		// Использование буфера...
	}
}
  1. Фильтрация событий:
go
func (w *WindowsFileWatcher) shouldProcessEvent(path string, op uint32) bool {
	// Пропускаем временные файлы
	if strings.Contains(path, "~") || strings.Contains(path, ".tmp") {
		return false
	}
	return true
}
  1. Пакетная обработка событий:
go
func (w *WindowsFileWatcher) processBatch() []FileEvent {
	var events []FileEvent
	
	for len(events) < 10 { // Ограничение на размер пакета
		select {
		case event := <-w.notifyChan:
			events = append(events, event)
		default:
			return events
		}
	}
	return events
}

Ключевым моментом является использование прямых вызовов Windows API через golang.org/x/sys/windows, так как стандартная библиотека fsnotify скрывает всю информацию о переименовании, кроме нового имени файла.

Источники

  1. Windows API Documentation - ReadDirectoryChangesW
  2. golang.org/x/sys/windows package documentation
  3. fsnotify library source code
  4. Microsoft Windows File System Change Notifications