如何仅检测卷上已删除,已更改和已创建的文件?

时间:2011-09-14 18:49:14

标签: c++ windows backup ntfs

我需要知道是否有一种简单的方法可以只检测在NTFS卷上删除,修改或创建的文件。

我已经用C ++编写了一个异地备份程序。在第一次备份之后,我检查每个文件的存档位以查看是否进行了任何更改,并仅备份已更改的文件。此外,它从VSS快照备份以防止文件锁定。

这似乎在大多数文件系统上都可以正常工作,但是对于一些文件和目录很多的人来说,这个过程需要很长时间,而且备份通常需要一天以上才能完成备份。

我尝试使用更改日志轻松检测对NTFS卷所做的更改,但更改日志会显示大量记录,其中大部分与创建和销毁的小型临时文件有关。另外,我可以使用文件名,文件引用号和父文件引用号,但是我无法获得完整的文件路径。父文件引用号以某种方式应该为您提供父目录路径。

编辑:这需要每天运行,所以在每次扫描开始时,它应该只记录自上次扫描以来发生的变化。或者至少,从某某时间和日期开始,应该有一种方式来表达变化。

4 个答案:

答案 0 :(得分:19)

您可以使用FSCTL_ENUM_USN_DATA枚举卷上的所有文件。这是一个快速的过程(即使在非常老的机器上,我的测试返回的速度也超过每秒6000条记录,而20000+更典型)并且只包含当前存在的文件。

返回的数据包括文件标志和USN,因此您可以根据自己喜欢的方式检查更改。

您仍需要通过将父ID与目录的文件ID相匹配来计算文件的完整路径。一种方法是使用足够大的缓冲区来同时保存所有文件记录,并搜索记录以找到需要备份的每个文件的匹配父级。对于大容量,您可能需要将目录记录处理为更高效的数据结构,可能是哈希表。

或者,您可以根据需要读取/重新读取父目录的记录。这样效率较低,但性能可能仍然令人满意,具体取决于备份的文件数量。 Windows确实会缓存FSCTL_ENUM_USN_DATA返回的数据。

此程序在C卷中搜索名为test.txt的文件,并返回有关找到的任何文件的信息,以及有关其父目录的信息。

#include <Windows.h>

#include <stdio.h>

#define BUFFER_SIZE (1024 * 1024)

HANDLE drive;
USN maxusn;

void show_record (USN_RECORD * record)
{
    void * buffer;
    MFT_ENUM_DATA mft_enum_data;
    DWORD bytecount = 1;
    USN_RECORD * parent_record;

    WCHAR * filename;
    WCHAR * filenameend;

    printf("=================================================================\n");
    printf("RecordLength: %u\n", record->RecordLength);
    printf("MajorVersion: %u\n", (DWORD)record->MajorVersion);
    printf("MinorVersion: %u\n", (DWORD)record->MinorVersion);
    printf("FileReferenceNumber: %lu\n", record->FileReferenceNumber);
    printf("ParentFRN: %lu\n", record->ParentFileReferenceNumber);
    printf("USN: %lu\n", record->Usn);
    printf("Timestamp: %lu\n", record->TimeStamp);
    printf("Reason: %u\n", record->Reason);
    printf("SourceInfo: %u\n", record->SourceInfo);
    printf("SecurityId: %u\n", record->SecurityId);
    printf("FileAttributes: %x\n", record->FileAttributes);
    printf("FileNameLength: %u\n", (DWORD)record->FileNameLength);

    filename = (WCHAR *)(((BYTE *)record) + record->FileNameOffset);
    filenameend= (WCHAR *)(((BYTE *)record) + record->FileNameOffset + record->FileNameLength);

    printf("FileName: %.*ls\n", filenameend - filename, filename);

    buffer = VirtualAlloc(NULL, BUFFER_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

    if (buffer == NULL)
    {
        printf("VirtualAlloc: %u\n", GetLastError());
        return;
    }

    mft_enum_data.StartFileReferenceNumber = record->ParentFileReferenceNumber;
    mft_enum_data.LowUsn = 0;
    mft_enum_data.HighUsn = maxusn;

    if (!DeviceIoControl(drive, FSCTL_ENUM_USN_DATA, &mft_enum_data, sizeof(mft_enum_data), buffer, BUFFER_SIZE, &bytecount, NULL))
    {
        printf("FSCTL_ENUM_USN_DATA (show_record): %u\n", GetLastError());
        return;
    }

    parent_record = (USN_RECORD *)((USN *)buffer + 1);

    if (parent_record->FileReferenceNumber != record->ParentFileReferenceNumber)
    {
        printf("=================================================================\n");
        printf("Couldn't retrieve FileReferenceNumber %u\n", record->ParentFileReferenceNumber);
        return;
    }

    show_record(parent_record);
}

void check_record(USN_RECORD * record)
{
    WCHAR * filename;
    WCHAR * filenameend;

    filename = (WCHAR *)(((BYTE *)record) + record->FileNameOffset);
    filenameend= (WCHAR *)(((BYTE *)record) + record->FileNameOffset + record->FileNameLength);

    if (filenameend - filename != 8) return;

    if (wcsncmp(filename, L"test.txt", 8) != 0) return;

    show_record(record);
}

int main(int argc, char ** argv)
{
    MFT_ENUM_DATA mft_enum_data;
    DWORD bytecount = 1;
    void * buffer;
    USN_RECORD * record;
    USN_RECORD * recordend;
    USN_JOURNAL_DATA * journal;
    DWORDLONG nextid;
    DWORDLONG filecount = 0;
    DWORD starttick, endtick;

    starttick = GetTickCount();

    printf("Allocating memory.\n");

    buffer = VirtualAlloc(NULL, BUFFER_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

    if (buffer == NULL)
    {
        printf("VirtualAlloc: %u\n", GetLastError());
        return 0;
    }

    printf("Opening volume.\n");

    drive = CreateFile(L"\\\\?\\c:", GENERIC_READ, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_FLAG_NO_BUFFERING, NULL);

    if (drive == INVALID_HANDLE_VALUE)
    {
        printf("CreateFile: %u\n", GetLastError());
        return 0;
    }

    printf("Calling FSCTL_QUERY_USN_JOURNAL\n");

    if (!DeviceIoControl(drive, FSCTL_QUERY_USN_JOURNAL, NULL, 0, buffer, BUFFER_SIZE, &bytecount, NULL))
    {
        printf("FSCTL_QUERY_USN_JOURNAL: %u\n", GetLastError());
        return 0;
    }

    journal = (USN_JOURNAL_DATA *)buffer;

    printf("UsnJournalID: %lu\n", journal->UsnJournalID);
    printf("FirstUsn: %lu\n", journal->FirstUsn);
    printf("NextUsn: %lu\n", journal->NextUsn);
    printf("LowestValidUsn: %lu\n", journal->LowestValidUsn);
    printf("MaxUsn: %lu\n", journal->MaxUsn);
    printf("MaximumSize: %lu\n", journal->MaximumSize);
    printf("AllocationDelta: %lu\n", journal->AllocationDelta);

    maxusn = journal->MaxUsn;

    mft_enum_data.StartFileReferenceNumber = 0;
    mft_enum_data.LowUsn = 0;
    mft_enum_data.HighUsn = maxusn;

    for (;;)
    {
//      printf("=================================================================\n");
//      printf("Calling FSCTL_ENUM_USN_DATA\n");

        if (!DeviceIoControl(drive, FSCTL_ENUM_USN_DATA, &mft_enum_data, sizeof(mft_enum_data), buffer, BUFFER_SIZE, &bytecount, NULL))
        {
            printf("=================================================================\n");
            printf("FSCTL_ENUM_USN_DATA: %u\n", GetLastError());
            printf("Final ID: %lu\n", nextid);
            printf("File count: %lu\n", filecount);
            endtick = GetTickCount();
            printf("Ticks: %u\n", endtick - starttick);
            return 0;
        }

//      printf("Bytes returned: %u\n", bytecount);

        nextid = *((DWORDLONG *)buffer);
//      printf("Next ID: %lu\n", nextid);

        record = (USN_RECORD *)((USN *)buffer + 1);
        recordend = (USN_RECORD *)(((BYTE *)buffer) + bytecount);

        while (record < recordend)
        {
            filecount++;
            check_record(record);
            record = (USN_RECORD *)(((BYTE *)record) + record->RecordLength);
        }

        mft_enum_data.StartFileReferenceNumber = nextid;
    }
}

附加说明

  • 正如评论中所讨论的,您可能需要在Windows 7之后的Windows版本上用MFT_ENUM_DATA替换MFT_ENUM_DATA_V0。(这可能还取决于您使用的是哪种编译器和SDK 。)

  • 我正在打印64位文件引用号,就好像它们是32位一样。这对我来说只是一个错误。可能在生产代码中你无论如何也不会打印它们,但仅供参考。

答案 1 :(得分:4)

变更日志是您最好的选择。您可以使用文件引用号来匹配文件创建/删除对,从而忽略临时文件,而无需进一步处理它们。

我认为您必须扫描主文件表才能理解ParentFileReferenceNumber。当然,您只需要在执行此操作时跟踪目录,并使用允许您快速查找信息的数据结构,因此您只需扫描一次MFT。

答案 2 :(得分:1)

您可以使用ReadDirectoryChanges和周围的Windows API。

答案 3 :(得分:-1)

我知道如何在java中实现这一点。如果您在C ++中实现Java代码,它将对您有所帮助。

在Java中,您可以使用Jnotify API实现此目的。它还会查找子目录中的更改。