基于磁盘ID删除文件

13
根据此帖子,使用带有SetFileInformationByHandleFILE_DISPOSITION_INFO参数的函数可以设置一个打开句柄的文件在所有句柄关闭时被删除。然而,我正在尝试根据由FILE_DISPOSITION_INFOOpenFileById检索到的文件索引(磁盘 ID)来删除文件,以便安全地删除仅大小写不同的目录中的文件/目录。在我的情况下,这样做是安全的,因为在 NTFS 系统上,文件索引在删除之前是 持久的,使得当前代码库处理的ReplaceFile不再需要。但是,尝试删除该句柄时,我会收到错误 87 (ERROR_INVALID_PARAMETER)。如果使用使用CreateFileW创建的句柄进行删除,则不会遇到任何问题。然而,我不能这样做,因为 Windows 将无法区分大小写相同的两个文件/文件夹(尽管 NTFS 可以)。我还知道使用OpenFileById打开的硬链接文件存在歧义,因为硬链接文件共享相同的磁盘 ID。对于此场景,硬链接文件的问题可以忽略不计。我只会根据 ID 删除目录,这些目录不能与硬链接。有一个参数或设置我在OpenFileById调用或SetFileInformationByHandle调用中遗漏的吗?

我尝试过的其他方法:

  • 使用 DuplicateHandleOpenFileById 句柄,提供 DELETEdwDesiredAccess 并使用它。 结果仍然是 ERROR_INVALID_PARAMETER
  • 使用 ReOpenFileOpenFileById 句柄,提供 DELETEdwDesiredAccess 并使用它。 结果仍然是 ERROR_INVALID_PARAMETER
  • 使用 ReOpenFileOpenFileById 句柄,提供 DELETEdwDesiredAccess 并提供 FILE_FLAG_DELETE_ON_CLOSE 标志。 没有错误提示,但文件在所有句柄关闭后仍然存在。

下面是一个最小化但完整的示例代码,可重现该问题:

#include <stdio.h>
#include <sys/stat.h>
#include <Windows.h>

DWORD getFileID(LPCWSTR path, LARGE_INTEGER *id)
{
    HANDLE h = CreateFileW(path, 0, 0, 0, OPEN_EXISTING,
        FILE_FLAG_OPEN_REPARSE_POINT |
        FILE_FLAG_BACKUP_SEMANTICS |
        FILE_FLAG_POSIX_SEMANTICS,
        0);
    if (h == INVALID_HANDLE_VALUE)
        return GetLastError();

    BY_HANDLE_FILE_INFORMATION info;
    if (!GetFileInformationByHandle(h, &info))
    {
        DWORD err = GetLastError();
        CloseHandle(h);
        return err;
    }
    id->HighPart = info.nFileIndexHigh;
    id->LowPart = info.nFileIndexLow;
    CloseHandle(h);
    return ERROR_SUCCESS;
}

DWORD deleteFileHandle(HANDLE fileHandle)
{
    FILE_DISPOSITION_INFO info;
    info.DeleteFileW = TRUE;
    if (!SetFileInformationByHandle(
        fileHandle, FileDispositionInfo, &info, sizeof(info)))
    {
        return GetLastError();
    }
    return ERROR_SUCCESS;
}

int wmain(DWORD argc, LPWSTR argv[])
{
    if (argc != 3)
    {
        fwprintf(stderr, L"Arguments: <rootpath> <path>\n");
        return 1;
    }

    DWORD err;
    HANDLE rootHandle = CreateFileW(
        argv[1], 0, 0, 0, OPEN_EXISTING,
        FILE_FLAG_OPEN_REPARSE_POINT |
        FILE_FLAG_BACKUP_SEMANTICS |
        FILE_FLAG_POSIX_SEMANTICS,
        0);
    if (rootHandle == INVALID_HANDLE_VALUE)
    {
        err = GetLastError();
        fwprintf(stderr,
            L"Could not open root directory '%s', error code %d\n",
            argv[1], err);
        return err;
    }

    LARGE_INTEGER fileID;
    err = getFileID(argv[2], &fileID);
    if (err != ERROR_SUCCESS)
    {
        fwprintf(stderr,
            L"Could not get file ID of file/directory '%s', error code %d\n",
            argv[2], err);
        CloseHandle(rootHandle);
        return err;
    }
    fwprintf(stdout,
        L"The file ID of '%s' is %lld\n",
        argv[2], fileID.QuadPart);

    FILE_ID_DESCRIPTOR idStruct;
    idStruct.Type = FileIdType;
    idStruct.FileId = fileID;
    HANDLE fileHandle = OpenFileById(
        rootHandle, &idStruct, DELETE, FILE_SHARE_DELETE, 0,
        FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS);
    if (fileHandle == INVALID_HANDLE_VALUE)
    {
        err = GetLastError();
        CloseHandle(rootHandle);
        fwprintf(stderr,
            L"Could not open file by ID %lld, error code %d\n",
            fileID.QuadPart, err);
        return err;
    }

    err = deleteFileHandle(fileHandle);
    if (err != ERROR_SUCCESS)
    {
        fwprintf(stderr,
            L"Could not delete file by ID '%lld', error code %d\n",
            fileID.QuadPart, err);
    }

    CloseHandle(fileHandle);
    struct _stat _tmp;
    fwprintf(stdout,
        L"File was %ssuccessfully deleted\n",
        (_wstat(argv[2], &_tmp) == 0) ? L"not " : L"");
    CloseHandle(rootHandle);
    return err;
}

任何解决方案必须与Vista及以上版本兼容。欢迎提出代码改进建议。


@HarryJohnston 这些文件是在Linux中创建的。理论上,如果程序可以通过磁盘ID删除文件,它也应该能够处理无效名称的文件。 - Alyssa Haroldsen
虽然看起来似乎不行。问题可能是磁盘ID标识文件而不是目录条目,因此您只能删除目录条目。尽管在NTFS中通过磁盘ID删除目录原则上是可能的,因为目录不能被硬链接,但似乎Windows实际上并不支持这样做。我认为您需要将obcaseinsensitive设置为零,并使用内核API。请参阅https://technet.microsoft.com/en-nz/library/cc725747.aspx。 - Harry Johnston
@HarryJohnston 主要限制是必须在默认配置下在Vista+上工作,并能够在后台运行。因此,没有NFS,没有安装程序,也没有重新启动。我看到重命名的一个问题是,当目录的子项被锁定时,该目录无法重命名(如果我没记错的话)。奇怪的是,如果句柄无法使用,我希望SetFileInformationByHandle返回ERROR_INVALID_HANDLE。还可以找到OpenFileById句柄的第一个文件路径。最后,对于包含“无效”字符的路径,如\*,重命名根本不起作用。 - Alyssa Haroldsen
1
SWAG:听起来很蠢,但你可能需要在OpenFileById调用中包含POSIX_SEMANTICS才能获得“兼容”的句柄?是的 - 我知道POSIX_SEMANTICS名义上只与文件名有关...但世界上还有更愚蠢的事情。@HarryJohnston提到了ERROR_INVALID_PARAMETER - 也许在内部,它正在“严重地”比较标志。 - Clay
1
记录一下,此处文档:文件系统行为概述(PDF)确认(第4.3.2节)您无法为通过ID打开的句柄设置删除关闭标志。 - Harry Johnston
显示剩余14条评论
4个回答

2

有一个用户模式版本的内核模式ZwCreateFile,名为NTCreteFile,它可以提供所有你无法通过OpenFileById(但可以通过CreateFile)获得的访问权限。它可以做到CreateFile所能做的一切,甚至可以创建目录。

好处是,在POBJECT_ATTRIBUTES参数中指定文件ID的方法非常巧妙(但有趣),因此你可以得到最佳效果...除了这个API比通常的笨拙Windows API更令人尴尬。

文档有两个版本,一个在:

https://msdn.microsoft.com/en-us/library/bb432380(v=vs.85).aspx

并且一个在:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff556465(v=vs.85).aspx

...链接到ZwCreateFile文档:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff566424(v=vs.85).aspx

我指出这一点的原因是第一篇文章省略了最后一篇文章中记录的一些好处(例如通过ID打开文件)。我发现这很常见,而且我也发现大多数记录的Zwxxx功能实际上存在于等效但未完全记录的NTxxx函数中。所以你需要恰到好处地掌握必要的功能。

1
为了让FILE_DISPOSITION_INFO正常工作,您需要在CreateFile函数中指定DELETE访问权限,如https://msdn.microsoft.com/en-us/library/windows/desktop/aa365539(v=VS.85).aspx所述:

当使用SetFileInformationByHandle时,必须在创建文件句柄时指定适当的访问标志。例如,如果应用程序将DeleteFile成员设置为TRUE并使用FILE_DISPOSITION_INFO,则需要在调用CreateFile函数时请求DELETE访问权限。有关示例,请参见示例代码部分。有关文件权限的更多信息,请参见文件安全和访问权限。 即:

//...
  HANDLE hFile = CreateFile( TEXT("tempfile"), 
                             GENERIC_READ | GENERIC_WRITE | DELETE,  //Specify DELETE access!
                             0 /* exclusive access */,
                             NULL, 
                             CREATE_ALWAYS,
                             0, 
                             NULL);

但是似乎使用OpenFileById()创建的句柄无法使用,因为该函数无法接受DELETE标志。
https://msdn.microsoft.com/en-us/library/windows/desktop/aa365432(v=vs.85).aspx上可以读到与OpenFileById()相关的内容: dwDesired

Access [in]
对象的访问级别。访问级别可以是读、写或读写。

即使设置DELETEGENERIC_ALL,该函数也会失败。
如果用设置了DELETE标志的CreateFile函数创建的句柄替换传递给SetFileInformationByHandle的句柄,如上所述,它就能正常工作。

CreateFile 没有用于创建 FILE_DISPOSITION_INFO 所使用的句柄,而是使用了 OpenFileById。访问权限使用了 GENERIC_ALL,其中包括删除。将其替换为 GENERIC_READ | GENERIC_WRITE | DELETE 没有任何区别。 - Alyssa Haroldsen
我已经知道如何使用CreateFile,就像描述的那样。但是,使用CreateFile仍然存在文件路径不唯一的问题。通常情况下,是否有可能通过磁盘ID删除文件,或者可靠地删除只在大小写上不同的两个文件(其中一个是文件,另一个是目录)? - Alyssa Haroldsen
微软文件系统不区分大小写,因此一般来说答案是否定的。我已经检查了对我来说最后的机会:使用ReOpenFile重新打开使用OpenFileById创建并添加DELETE标志的文件,但是这个解决方案也不起作用 :( - Frankie_C
问题在于NTFS是大小写敏感的,但Windows不是。ReOpenFile的想法很好,但很遗憾它没有起作用。然而,FindNextFile和类似的方法仍然将它们视为不同的文件。 - Alyssa Haroldsen
似乎 OpenFileById 是为网络文件设计的,因此对真实文件的访问受到限制。也许有一个函数可以更改打开的文件。这不是最常用的函数之一,我也想不起来了。也许你想在那个方向搜索类似 ReOpenFile 的东西... - Frankie_C

0

你有考虑使用FILE_FLAG_POSIX_SEMANTICS吗?它允许你使用CreateFile打开只有大小写不同的文件。

编辑:我看了一下你的代码,发现你已经在使用这个标志了。


不起作用。你自己试过了吗?同样会因为无效的名称而失败。 - Alyssa Haroldsen
我现在正在尝试您的示例,我可以重现这种行为。我猜现在我的问题是,您想让哪一部分起作用,删除仅在大小写上不同的文件还是删除由ID打开的文件? - Red Bug
删除已通过ID打开的文件,其中一个好处是能够删除仅在大小写上有差异的文件,以及包含无效文件名(例如包含:的文件名)。如果内核当前不区分大小写(这是默认情况),则FILE_FLAG_POSIX_SEMANTICS也不会按照描述的方式工作。如果您能够编写并验证有效的代码示例,那可能会很有用。 - Alyssa Haroldsen

-1

假设文件名为XXX和xxx,您想要删除XXX。

  1. MoveFile(“XXX”,“我认为它是XXX”)
  2. 如果XXX被重命名,则DeleteFile(“我认为它是XXX”)
  3. 否则,DeleteFile(“XXX”); MoveFile(“我认为它是XXX”,“xxx”)

至于OpenFileById,正如您所指出的,具有多个名称(也称为硬链接)的文件可能存在潜在歧义。允许DELETE访问可能会对此造成混乱,并且可能会删除意外的名称(如果将其留给文件系统选择哪个名称)。我怀疑他们选择了从不授予DELETE访问权限的简单情况。

对于允许目录的硬链接,也可以提出类似的论点。当然,您有时可以正确地执行此操作,但一旦创建循环,事情就变得更加困难...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接