Следим за папками и файлами

Решил я взяться за простую задачу: отслеживать любые изменения в определенной папке. Методом проб и ошибок вроде как получилось добиться сносного результата. И так начнем :)

Первое , что надо сделать: это открыть объект (в данном случае папку) для возможности отслеживания. В этом поможет функция CreateFile.

hDir = CreateFile(_
    "C:\22",_
    FILE_LIST_DIRECTORY,_
    FILE_SHARE_READ+FILE_SHARE_WRITE+FILE_SHARE_DELETE,_
    0,_
    OPEN_EXISTING,_
    FILE_FLAG_BACKUP_SEMANTICS,_
    0 _
    )


В данном случае:

  • В первом параметре передаем путь до папки
  • Во втором уровень доступа (для перечисления)
  • В третьем флаги (для отслеживания чтения, записи, удаления)
  • Четвертый параметр нам не нужен, обнуляем
  • Пятый параметр определяет только существующую папку. Если папки не окажется, функция завершится с ошибкой  
  • В шестом передаем флаг для установки нужных привилегий
  • Последний параметр не нужен, обнуляем

Функция вернет нам хедл, который будем использовать в функции ReadDirectoryChanges. По идее это функция ReadDirectoryChangesW, но в freebasic при декларации почему-то решили отрезать последнюю букву :)

Данная функция при успешном вызове, будет возвращать значение отличное от нуля. В коде это будет выглядеть так:

        If ReadDirectoryChanges(_
            hDir,_
            buf,_
            10000,_
            TRUE,_
            &h17F,_ ' совокупность флагов
            0,_
            @ov,_
            0_
            )=0 Then Exit While


Здесь:

  • В первом параметре передаем хендл, возвращенный CreateFile
  • Во втором буфер, в который будет возвращена нужная нам информация
  • В третьем параметре передаем размер буфера
  • В четвертом устанавливаем возможность следить за подкаталогами
  • В пятом совокупность флагов, которые определяют слежение за папками, файлами и их различными изменениями. Если нужно следить за чем-то определенным (например за размерами файлов), то советую просмотреть эту страницу и определиться с выбором самим.
  • Шестой параметр определяет размер возвращаемых данных, но при ассинхронном вызове он не используется, обнуляем
  • Седьмой параметр - указатель на структуру OVERLAPPED для ассинхронного чтения
  • Восьмой не используется, обнуляем

Буфер, который используется в функции по сути указатель на структуру или множество структур FILE_NOTIFY_INFORMATION, которые располагаются друг за другом.

В начале кода мы определим данную структуру сами, поскольку объявленная в заголовке нам не годится...

#Undef FILE_NOTIFY_INFORMATION
Type FILE_NOTIFY_INFORMATION
    NextEntryOffset As DWORD
    Action As DWORD
    FileNameLength As DWORD
    FileName As Wstring * 255
End Type


В ней:

  • NextEntryOffset - смещение в байтах для каждой структуры по отношению к предыдущей. Выудив информацию из структуры по смещению ноль, мы получим следущую, прибавив к имеющемуся смещению значение в NextEntryOffset. И так будем проделывать, пока NextEntryOffset не будет равно нулю.
  • Action - тип изменения, которое произошло. Расписывать не буду, их всего пять и в коде ниже интуитивно понятно, которое для чего.
  • FileNameLength - длина в байтах имени файла или папки, в котором произошли изменения
  • FileName - имя файла или папки в юникоде, в котором произошли изменения. Я поставил длину этого буфера 255 символов

Поскольку указатель buf у нас будет меняться, мы в самом начале его адрес сохраним в указатель BufBackup, а перед использованием функции ReadDirectoryChanges восстановим его. И так получив информацию с помощью ReadDirectoryChanges мы в цикле начинаем вытаскивать информацию из совокупности структур. Сначала создадим временный указатель на структуру FILE_NOTIFY_INFORMATION и присвоим ему адрес буфера. Далее узнаем следущее смещение другой структуры и установим для буфера новый адрес:

Dim As FILE_NOTIFY_INFORMATION Ptr fni
fni = Cast(FILE_NOTIFY_INFORMATION Ptr,buf)
iOffset = fni->NextEntryOffset
buf+=iOffset

Далее выделяем достаточный буфер для имени файла и копируем в него путь до файла или папки с длиной, указанной в поле структуры FileNameLength, естественно делая акцент на то, что у нас строки в юникоде:

wsFileName = Left(fni->FileName,fni->FileNameLength\2)

Следущий этап: узнаем какой тип изменения произошел (тут по моему все очевидно):

Select Case fni->Action
   Case FILE_ACTION_ADDED
        ? "Создан файл: " & wsFileName
   Case FILE_ACTION_REMOVED
        ? "Удален файл: " & wsFileName
   Case FILE_ACTION_RENAMED_OLD_NAME
        ? "Старое имя файла: " & wsFileName
   Case FILE_ACTION_RENAMED_NEW_NAME
        ? "Новое имя файла: " & wsFileName
   Case FILE_ACTION_MODIFIED
        ? "Изменен файл: " & wsFileName
End Select


И конечно очищаем структуру каждый раз перед использованием на всякий случай с помощью ZeroMemory.

Все это хозяйство мы определяем в процедуру и выносим ее в отдельный поток:

Threadcreate(@detect)

Do
    Sleep(10)
Loop Until GetAsyncKeyState(VK_ESCAPE)<0


И конечно нужно помнить, что исходный код должен быть сохранен в юникоде, а вначале листинга указать это директивой:

#DEFINE Unicode

А вот и полный код:

' Для завершения программы,
' просто нажмите ESCAPE

#DEFINE Unicode
#INCLUDE "windows.bi"

#Undef FILE_NOTIFY_INFORMATION
Type FILE_NOTIFY_INFORMATION
    NextEntryOffset As DWORD
    Action As DWORD
    FileNameLength As DWORD
    FileName As Wstring * 255
End Type

Dim Shared As Handle hDir

hDir = CreateFile(_
"C:\22",_
FILE_LIST_DIRECTORY,_
FILE_SHARE_READ+FILE_SHARE_WRITE+FILE_SHARE_DELETE,_
0,_
OPEN_EXISTING,_
FILE_FLAG_BACKUP_SEMANTICS,_
0 _
)

If hDir = INVALID_HANDLE_VALUE Then
    MessageBox(0,"По каким-то причинам открыть данный каталог нельзя","",0)
Endif

Sub detect(p As Any Ptr)
    Dim As Byte Ptr buf = Callocate(10000), BufBackup
    Dim As OVERLAPPED ov
    Dim As Integer iOffset

    BufBackup = buf

    While TRUE
        buf = BufBackup 
        If ReadDirectoryChanges(_
        hDir,_
        buf,_
        10000,_
        TRUE,_
        &h17F,_ ' совокупность флагов
        0,_
        @ov,_
        0 _
        )=0 Then Exit While

        Do
            Dim As FILE_NOTIFY_INFORMATION Ptr fni
            fni = Cast(FILE_NOTIFY_INFORMATION Ptr,buf)
            iOffset = fni->NextEntryOffset
            buf+=iOffset
            If fni->FileNameLength<>0 Then
                Dim As WString*256 wsFileName
                wsFileName = Left(fni->FileName,fni->FileNameLength\2)
                Select Case fni->Action
                    Case FILE_ACTION_ADDED
                        ? "Создан файл: " & wsFileName
                    Case FILE_ACTION_REMOVED
                        ? "Удален файл: " & wsFileName
                    Case FILE_ACTION_RENAMED_OLD_NAME
                        ? "Старое имя файла: " & wsFileName
                    Case FILE_ACTION_RENAMED_NEW_NAME
                        ? "Новое имя файла: " & wsFileName
                    Case FILE_ACTION_MODIFIED
                        ? "Изменен файл: " & wsFileName
                End Select
            Endif

        Loop While iOffset<>0
        ZeroMemory(BufBackup,10000)
    Wend
End Sub

Threadcreate(@detect)

Do
    Sleep(10)
Loop Until GetAsyncKeyState(VK_ESCAPE)<0