Как получить исходное имя файла до переименования с помощью библиотеки 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 для доступа к renamedFrom
- Альтернативные подходы
- Практическая реализация
- Оптимизация производительности
Прямое использование Windows API
Библиотека fsnotify на Windows использует функцию ReadDirectoryChangesW из Windows API, которая может предоставлять полную информацию о переименовании, включая исходное имя файла. Однако fsnotify скрывает эту информацию.
Для прямого доступа к Windows API:
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(¬ifyInfo.FileName))[:notifyInfo.FileNameLength/2])
fmt.Printf("Старое имя: %s\n", oldName)
case FILE_ACTION_RENAMED_NEW_NAME:
newName := windows.UTF16ToString((*[1024]uint16)(unsafe.Pointer(¬ifyInfo.FileName))[:notifyInfo.FileNameLength/2])
fmt.Printf("Новое имя: %s\n", newName)
}
offset += notifyInfo.NextEntryOffset
if notifyInfo.NextEntryOffset == 0 {
break
}
}
}
}
Расширение fsnotify для доступа к renamedFrom
Поскольку renamedFrom является приватным полем, можно создать обертку для fsnotify:
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 с буферизацией
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. Комбинирование событий
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:
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)
}
}
}
Оптимизация производительности
Для улучшения производительности可以考虑:
- Использование пула буферов:
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 4096)
},
}
func (w *WindowsFileWatcher) monitor() {
for {
buffer := bufferPool.Get().([]byte)
defer bufferPool.Put(buffer)
// Использование буфера...
}
}
- Фильтрация событий:
func (w *WindowsFileWatcher) shouldProcessEvent(path string, op uint32) bool {
// Пропускаем временные файлы
if strings.Contains(path, "~") || strings.Contains(path, ".tmp") {
return false
}
return true
}
- Пакетная обработка событий:
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 скрывает всю информацию о переименовании, кроме нового имени файла.