如何检测 win32 NTFS 下的文件移动?

How to detect a file move under win32 NTFS?

我有一个数据库,允许将附加信息附加到文件系统 (NTFS) 中的任何文件。文件 ID 是它的完整路径,所以为了保持一致性,我需要观察数据库中的任何文件是否被删除、重命名或移动。

目前,我正在尝试通过将 ReadDirectoryChangesW 函数与 FILE_NOTIFY_CHANGE_FILE_NAME 结合使用来实现此目的 | FILE_NOTIFY_CHANGE_DIR_NAME 作为筛选条件。

问题是这样,我只收到重命名、创建和删除的通知。因此,我需要根据 'added' 和 'removed' 事件以及相关文件名来猜测何时发生移动(在同一卷上,移动 [ctrl-x, ctrl-v] 实际上是文件删除紧随其后的是文件创建,路径不同,但文件名保持不变。

有谁知道是否有更好的解决方案?

这是我根据观察得出的理解:

关于在 NTFS 下移动文件

在同一卷内

'removed' 事件后立即(没有延迟或延迟很小)是相同文件名和不同路径(无论移动文件的大小如何)的 'added' 事件。

如果正在监视整个卷的特殊情况:当文件被删除时,它实际上被添加到回收站(路径包含卷回收站但文件名不同 [某种哈希])。

在两个不同的卷之间

首先,目标卷上有一个 'added' 事件。
之后,当复制完成时,原始卷上有一个 'removed' 事件。

(注意:同时可能会发生多个事件:文件越大,延迟时间越长。)

如果这些文件在您的控制之下(如果您在添加到数据库时具有写入权限,那么您始终至少具有读取权限),我会在替代数据流中通过我的 GUID 标记它们,或者使用对象 ID (https://msdn.microsoft.com/en-us/library/aa364557%28v=VS.85%29.aspx),无论哪个更合适。

确实无法收到有关 'moved' 事件的通知,因为此类事件始终是 path/filename 重命名或双重事件删除和创建的结果。
虽然使用 USN 日志可以使事情变得更容易一些,但仍有一些额外的工作要做。

在这种情况下,我需要即时检查文件系统更改(我的应用程序在后台 运行),因此使用日志(日志)没有意义。

这是我想出的用于与 DeviceIoControl 和 ReadDirectoryChangesW 函数以及包含自定义 FileActionInfo 项目的队列一起使用的逻辑。

struct FileActionInfo {
    WCHAR fileName[FILE_NAME_MAX];
    CHAR drive;
    DWORD action;
    time_t timeStamp;

};

在使用 USN 时,这对于猜测所有移动事件应该也很有用:

算法

- when a 'added' event occurs
    - if previous event was a 'removed' event on same volume
        - if 'added' event contains recycle bin path, ignore it (file deleted)
        - else if 'removed' event contains recycle bin path, handle as a 'restored'/'undelete' event, remove 'removed' event from queue
        - else 
            - if 'added' event has same filename, handle as a 'moved' event, remove 'removed' event from queue
            - else push 'added' event to queue
    - else push 'added' event to queue


- when a 'removed' event occurs, search the queue for an 'added' event for the same filename on a different volume
    - if found, handle it as a 'moved' event and remove 'added' event from queue
    - else push 'removed' event to queue, launch a delayedRemoval thread


delayedRemoval thread(&FileActionInfo) {
    // we cannot wait forever , because 'added' event might never occur (if the file was actually deleted).
    sleep(2000)
    if given 'removed' event is still in the queue
        handle as an actual 'removed' event, and remove it from queue
    return;
}

例外情况

  • 如果在同一会话期间在不同卷上创建了两个具有相同文件名的文件,然后其中一个被删除,这将被错误地作为 'moved' 事件处理
  • 随着时间的推移,FileActionInfo 队列可能会变大,我们可以偶尔清理一下(设置移动文件的最大延迟)
    • 如果复制持续时间超过允许的延迟,我们可能会错过 'moved' 事件
    • 队列越大,向其中搜索事件的时间就越长,因此我们需要快速访问:(存储相同项目但访问模式不同的几个对象:vector、hashtable)

为了以防万一,这是我写的FileActionQueue.h。
最重要的方法是 Last() 和 Search(LPCWSTR 文件名、DWORD 操作、PCHAR 驱动器)。

#pragma once


#include <time.h>
#include <string>
#include <cwchar>
#include <vector>
#include <map>

using std::wstring;
using std::vector;
using std::map;

/* constants defined in winnt.h :
#define FILE_ACTION_ADDED                   0x00000001   
#define FILE_ACTION_REMOVED                 0x00000002   
#define FILE_ACTION_MODIFIED                0x00000003   
#define FILE_ACTION_RENAMED_OLD_NAME        0x00000004   
#define FILE_ACTION_RENAMED_NEW_NAME        0x00000005
*/
#define FILE_ACTION_MOVED                    0x00000006

class FileActionInfo {
public:
    LPWSTR    fileName;
    CHAR    drive;
    DWORD    action;
    time_t    timestamp;

    FileActionInfo(LPCWSTR fileName, CHAR drive, DWORD action) {        
        this->fileName = (WCHAR*) GlobalAlloc(GPTR, sizeof(WCHAR)*(wcslen(fileName)+1));
        wcscpy(this->fileName, fileName);
        this->drive = drive;
        this->action = action;
        this->timestamp = time(NULL);
    }

    ~FileActionInfo() {
        GlobalFree(this->fileName);    
    }
};

/*
There are two structures storing pointers to FileActionInfo items : a vector and a map. 
This is because we need to be able to:
1) quickly retrieve the latest added item
2) quickly search among all queued items (in which case we use fileName as hashcode)
*/
class FileActionQueue {
private:
    vector<FileActionInfo*> *qActionQueue;
    map<wstring, vector<FileActionInfo*>> *mActionMap;

    void Queue(vector<FileActionInfo*> *v, FileActionInfo* lpAction) {
        v->push_back(lpAction);
    }

    void Dequeue(vector<FileActionInfo*> *v, FileActionInfo* lpAction) {
        for(int i = 0, nCount = v->size(); i < nCount; ++i){
            if(lpAction == v->at(i)) {
                v->erase(v->begin() + i);
                break;
            }
        }
    }

public:

    FileActionQueue() {
        this->qActionQueue = new vector<FileActionInfo*>;
        this->mActionMap = new map<wstring, vector<FileActionInfo*>>;
    }

    ~FileActionQueue() {
        delete qActionQueue;
        delete mActionMap;    
    }

    void Add(FileActionInfo* lpAction) {
        this->Queue(&((*this->mActionMap)[lpAction->fileName]), lpAction);
        this->Queue(this->qActionQueue, lpAction);
    }

    void Remove(FileActionInfo* lpAction) {
        this->Dequeue(&((*this->mActionMap)[lpAction->fileName]), lpAction);
        this->Dequeue(this->qActionQueue, lpAction);
    }

    FileActionInfo* Last() {
        vector<FileActionInfo*> *v = this->qActionQueue;
        if(v->size() == 0) return NULL;
        return v->at(v->size()-1);
    }

    FileActionInfo* Search(LPCWSTR fileName, DWORD action, PCHAR drives) {
        FileActionInfo* result = NULL;
        vector<FileActionInfo*> *v;
        if( v = &((*this->mActionMap)[fileName])) {
            for(int i = 0, nCount = v->size(); i < nCount && !result; ++i){
                FileActionInfo* lpAction = v->at(i);
                if(wcscmp(lpAction->fileName, fileName) == 0 && lpAction->action == action) {
                    int j = 0;
                    while(drives[j] && !result) {
                        if(lpAction->drive == drives[j]) result = lpAction;
                        ++j;
                    }            
                }
            }
        }
        return result;
    }
};