马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
前言
我以前将一个目录作为 OneDrive 同步目录使用,所有文件都保留在本地,后来停用了这个同步目录。
这些文件可以正常读取,图片、视频却只能显示默认图标,资源管理器无法生成缩略图。奇怪的是,将文件剪切到原 OneDrive 目录外,再剪切回来,缩略图就恢复了。
这并不是缩略图缓存或视频解码器的问题,而是 OneDrive 的按需文件(Files On-Demand)在 NTFS 中留下了云文件元数据(cloud file metadata)。这套机制由 Windows 的 Cloud Filter API 提供[1]。
问题原因
OneDrive 的按需文件并不只靠文件属性工作。一个云文件通常同时包含两类状态:
- 文件属性(file attributes),例如固定(pinned)、未固定(unpinned)、脱机(offline)和读取时召回(recall on data access)。
- 重剖析点(reparse point),其中保存了云文件提供步伐(cloud file provider)的标识和文件身份(file identity)。
可以先用 attrib 查看文件属性:- $path = 'H:\OldOneDrive\Video\example.mp4'
- cmd /c attrib "$path"
复制代码 例如下面的 P 表示文件具有固定(pinned)属性:- A P H:\OldOneDrive\Video\example.mp4
复制代码 再用 fsutil reparsepoint[2] 查看重剖析点:- fsutil reparsepoint query "$path"
复制代码 有问题的文件可能返回雷同效果:- Reparse Tag Value : 0x9000601a
- Tag value: Microsoft
复制代码 0x9000?01A 属于 Cloud Files 使用的重剖析标签(reparse tag)。即使文件正文已经完整保存在本地,Windows 外壳(Windows Shell)仍会先将它当成云占位文件(cloud placeholder)处理,并尝试找到原来的存储提供步伐(storage provider)。
当原 OneDrive 账户或同步根(sync root)已经不存在时,缩略图接口可能返回:- 0x8004B207 WTS_E_NOSTORAGEPROVIDERTHUMBNAILHANDLER
复制代码 它的含义是找不到存储提供步伐的缩略图处理器(No storage provider thumbnail handler)。因此文件可以播放或打开,但资源管理器只显示默认图标。
为什么只运行 attrib 没有效果
下面的命令只能清除文件属性:- cmd /c attrib -P -U -O "$path"
复制代码 执行后,P 可能确实消失了,但再次运行 fsutil reparsepoint query,仍然可以看到 0x9000?01A 标签。
这是因为重剖析点是独立的 NTFS 元数据,并不是 attrib 管理的属性位(attribute bits)。attrib 没有删除重剖析数据的本领,执行顺序也不能改变这一点。
直接执行下面的命令通常也不行:- fsutil reparsepoint delete "$path"
复制代码 Cloud Files 筛选驱动(Cloud Files minifilter,驱动名为 CldFlt)会阻止通用的重剖析点删除操作,可能返回:- Error 395: Access to the cloud file is denied.
复制代码 395 对应 Win32 错误 ERROR_CLOUD_FILE_ACCESS_DENIED[3]。这不肯定是访问控制列表(Access Control List,ACL)权限不足;即使使用管理员或 SYSTEM 权限,仍可能被 Cloud Files 本身的状态机(state machine)拒绝。
水合与未水合
水合(hydration)是指将云端正文下载并写入本地占位文件。完全水合(fully hydrated)的文件已经具有完整的本地数据,可以在云提供步伐离线时正常读取。
未水合(unhydrated)的文件只有名称、逻辑大小(logical size)、时间戳和云端身份,部分或全部正文并不在本机。它通常是 NTFS 稀疏文件(sparse file),应用读取时会触发云文件提供步伐下载正文。
可以用下面的命令辅助检查:- fsutil sparse queryflag "$path"
- try {
- $stream = [IO.File]::OpenRead($path)
- $value = $stream.ReadByte()
- $stream.Dispose()
- "文件可读取,第一个字节:$value"
- }
- catch {
- "读取失败:$($_.Exception.Message)"
- }
复制代码 如果原云文件提供步伐已经不可用,未水合文件可能返回[3:1]:- 0x8007016A ERROR_CLOUD_FILE_PROVIDER_NOT_RUNNING
复制代码 这时不能安全地强行删除云标签。因为正文根本不在本地,去掉占位元数据只会留下逻辑大小看似正常、实际数据缺失的破坏文件。应先重新连接原云盘并完成水合,或者确认文件不再需要后删除它。
方法一:移出旧同步根再移回
对于少量文件,最简单的方法是将文件移动到旧 OneDrive 同步根之外,再移动回来。
[!IMPORTANT]
临时目录应与原文件位于同一个 NTFS 卷。例如原文件在 H:,临时目录也应放在 H:。同卷移动只是修改 NTFS 目录项,不会复制文件正文;跨卷移动会退化为复制后删除,大文件会产生大量读写。
下面的示例会记录哈希(hash)和 NTFS 文件 ID(file ID),移动文件后再核对:- $source = 'H:\OldOneDrive\Video\example.mp4'
- $originalDirectory = Split-Path -LiteralPath $source
- $temporaryDirectory = 'H:\OneDriveRepairTemp'
- $temporaryPath = Join-Path $temporaryDirectory (Split-Path -Leaf $source)
- New-Item -ItemType Directory -Path $temporaryDirectory -Force | Out-Null
- $hashBefore = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash
- $idBefore = fsutil file queryFileID "$source"
- # 同卷移动:文件正文不会被复制
- Move-Item -LiteralPath $source -Destination $temporaryPath
- fsutil reparsepoint query "$temporaryPath"
- Move-Item -LiteralPath $temporaryPath -Destination $source
- # 清除移动后可能残留的云属性
- cmd /c attrib -P -U -O "$source"
- $hashAfter = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash
- $idAfter = fsutil file queryFileID "$source"
- [pscustomobject]@{
- HashMatch = $hashBefore -eq $hashAfter
- FileIdBefore = $idBefore
- FileIdAfter = $idAfter
- }
复制代码 文件跨出旧同步根时,CldFlt 会将已经完整水合的占位文件还原为普通文件。由于这个目录已经不再注册为 OneDrive 同步根,文件移回后不会重新得到云标签。
在我的测试中,移出和移回前后的文件 ID 与 SHA-256 均完全相同,0x9000601A 标签在移出时消失,缩略图立即恢复。这证实同卷移动没有复制正文。
这种方法适合手动修复少量文件,但不适合数万个文件,也不建议直接移动整个旧同步根目录:同步根本身可能也具有云重剖析标签,移动它未必形成预期的“跨出同步根”行为。
方法二:使用 CfRevertPlaceholder 原地修复
Windows 提供了专门的 Cloud Files API:CfRevertPlaceholder[4]。它用于将已经水合的占位文件或目录原地还原(in-place revert)为普通文件或目录,不需要复制、移动、重命名或删除文件。
该 API 接受属性句柄或无访问权限句柄,但调用者本身必须对目标具有 WRITE_DATA 或 WRITE_DAC 权限。普通用户拥有本身文件的写入权限时即可调用,不需要管理员或 SYSTEM 权限。
下面的 PowerShell 脚本会:
- 使用 CfGetPlaceholderInfo[5] 判断路径是否为 Cloud Files 占位文件。
- 对占位文件调用 CfRevertPlaceholder。
- 清除 Pinned、Unpinned、Offline 和 Recall 属性位。
- 先处理文件,再按路径深度从深到浅处理目录,末了处理同步根。
- 保留无法水合或没有权限的文件,并输出失败列表。
[!WARNING]
若要直接使用该脚本,请先在小范围文件内尝试,以免造成不可逆的效果
[code]param( [Parameter(Mandatory)] [string] $Root)Set-StrictMode -Version Latest$ErrorActionPreference = 'Stop'$normalResults = @( [Convert]::ToUInt32('80070001', 16), # ERROR_INVALID_FUNCTION [Convert]::ToUInt32('80070178', 16), # ERROR_NOT_A_CLOUD_FILE [Convert]::ToUInt32('80070186', 16), # ERROR_CLOUD_FILE_NOT_UNDER_SYNC_ROOT [Convert]::ToUInt32('80071126', 16) # ERROR_NOT_A_REPARSE_POINT)Add-Type -TypeDefinition @'using System;using System.ComponentModel;using System.Runtime.InteropServices;using Microsoft.Win32.SafeHandles;public static class CloudFileRepair{ private const uint ShareAll = 0x7; private const uint OpenExisting = 3; private const uint OpenReparsePoint = 0x00200000; private const uint BackupSemantics = 0x02000000; private const uint InvalidAttributes = 0xFFFFFFFF; // OFFLINE | RECALL_ON_OPEN | PINNED | UNPINNED | RECALL_ON_DATA_ACCESS private const uint CloudAttributeMask = 0x005C1000; private const uint DirectoryAttribute = 0x00000010; private const uint NormalAttribute = 0x00000080; [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern SafeFileHandle CreateFileW( string fileName, uint desiredAccess, uint shareMode, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern uint GetFileAttributesW(string fileName); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetFileAttributesW( string fileName, uint fileAttributes); [DllImport("cldapi.dll")] private static extern int CfGetPlaceholderInfo( SafeFileHandle fileHandle, int infoClass, IntPtr infoBuffer, uint infoBufferLength, out uint returnedLength); [DllImport("cldapi.dll")] private static extern int CfRevertPlaceholder( SafeFileHandle fileHandle, uint revertFlags, IntPtr overlapped); private static string GetExtendedPath(string path) { if (path.StartsWith(@"\\?\", StringComparison.Ordinal)) return path; if (path.StartsWith(@"\\", StringComparison.Ordinal)) return @"\\?\UNC\" + path.Substring(2); return @"\\?\" + path; } private static uint HResultFromWin32(int error) { if (error |