用 C# 和 Cloud Files API 打造一个类 OneDrive 的网盘客户端

一个基于腾讯云 SMH 的 Windows 网盘客户端 Demo,从零到完全可用的全过程记录。

前言

你有没有想过,OneDrive 那种”点击即下载、云端图标状态”的体验是怎么实现的?

Windows 资源管理器中,OneDrive 文件左下角会显示一个蓝色小云朵(☁️),双击后自动下载并打开,下载完成后变成绿色对勾(✅)。右键还能选择”释放空间”把本地文件变回云朵。这套体验看起来很神奇,但核心其实是一个叫做 Cloud Files API (cfapi) 的 Windows 系统级 API。

本文分享了我用 C# 基于 Cloud Files API 和腾讯云智能媒资托管(SMH)从零搭建一个网盘客户端的完整过程,包括踩过的坑和最终方案。

项目是什么

SmhDemo 是一个 Windows 网盘客户端 Demo,实现了以下核心功能:

  • 在 Windows 资源管理器中注册一个虚拟”SMH 网盘”同步盘
  • 浏览云端文件目录,文件显示为”仅联机”(☁️ 云朵图标)
  • 双击文件自动下载并打开,完成后变为”已在此设备上”(✅ 绿色勾)
  • 支持”释放空间”脱水、”始终保留”固定等 OneDrive 同款操作
  • 拖拽/复制文件到网盘自动上传,支持秒传检测
  • 文件夹创建、删除、重命名等完整文件管理
  • 双向增量同步(云端变更自动同步到本地)
  • 冲突检测与解决
📁 此电脑
├── ☁️ SMH 网盘                    ← 注册的同步根
│   ├── 📁 工作文档/
│   │   ├── ☁️  报告.docx          [占位符 - 云端]
│   │   ├── ✅  方案.pdf           [已下载 - 本地]
│   │   └── 📌  合同.pdf           [已固定 - 不自动回收]
│   ├── 📁 图片/
│   └── ✅  说明.txt

为什么选 SMH 作为后端

这个项目的核心目标是做一个类 OneDrive 的网盘客户端,而腾讯云智能媒资托管(Smart Media Hosting,SMH)正是为这类场景量身打造的 PaaS 产品。

SMH 是什么

SMH 定位于对象存储 COS 之上的存储处理中间件,专门为开发者构建网盘、相册、小程序等媒资应用提供一站式解决方案。相比直接使用 COS,SMH 在三个维度上做了关键增强:

能力维度 COS(对象存储) SMH(智能媒资托管)
账号体系 需自行实现 内置 ToC 账号管理、多租户隔离
文件管理 扁平 Key-Value 高效目录式文件管理、ETag 变更追踪
智能处理 需额外接入数据万象 内置 AI 分析(人脸、标签、元数据)

SMH 的核心能力

存储与管理:文件数据托管在腾讯云 COS 上,享受多架构冗余、异地容灾、高持久性保障。SMH 在此基础上提供了目录式文件管理、文件级别读写权限控制、独立访问令牌鉴权,以及媒体库和租户空间的交叉授权体系。

文件操作 API:提供完整的 RESTful API,本项目用到的关键接口包括:

API 能力 说明 本项目使用方式
目录列表 递归遍历云端目录树 用户浏览网盘文件夹时调用
文件下载 302 跳转到 COS URL,支持 Range 断点续传 水合引擎三阶段下载
简单上传 ≤100MB 文件一步上传 小文件直接上传
分片上传 >100MB 大文件分片传输 大文件上传
秒传检测 通过 SHA-256 Hash 匹配已有文件,命中即跳过上传 拖拽文件时自动探测
文件元数据操作 创建目录、删除、重命名 完整的文件管理功能

智能分析(AI 能力):SMH 集成了腾讯云的 AI 技术,虽然本项目是 Demo 未深入使用,但这是 SMH 区别于纯存储方案的核心价值:

  • 人脸识别与聚类 — 自动识别图片中的人脸并聚类,适合智能相册场景
  • 标签识别 — 对图片内容自动打标签,海量标签覆盖常见事物
  • 元数据提取 — 提取图片/视频的拍摄时间、GPS 定位等信息
  • 智能检索 — 基于标签、人脸等 AI 分析结果实现快速检索

为什么 SMH 适合做网盘后端

SMH 天然支持网盘场景所需的核心能力:秒传(避免重复传输)、分片上传(大文件可靠传输)、ETag 变更追踪(增量同步的基础)、目录式管理(资源管理器的文件树映射)。同时,它作为 PaaS 中间件,无需自行搭建存储服务、无需管理服务器,申请一个媒体库即可通过 API 完成所有操作。

本项目 Demo 阶段主要使用了 SMH 的存储与文件管理能力,后续完全可以基于 SMH 的 AI 能力扩展出智能相册、内容检索等高级功能。

技术选型

决策项 选择 理由
客户端语言 C# / .NET 8 Windows API 集成方便,P/Invoke 灵活
后端存储 腾讯云 SMH 免搭建,API 齐全,秒传+分片+目录管理开箱即用
资源管理器集成 Windows Cloud Files API (cfapi) OneDrive 同款技术,原生占位符文件+按需下载
本地状态存储 SQLite (WAL 模式) 轻量可靠,线程安全
系统托盘 WinForms NotifyIcon 后台常驻,右键菜单管理

为什么选 Cloud Files API 而不是其他方案?

Windows 提供了几种与资源管理器集成的方式:

  • Shell Namespace Extension (NSE):高度自定义但开发复杂度极高
  • Sync Engine (cfapi):OneDrive/OneDrive for Business 使用的方案,原生支持占位符、按需下载、状态图标
  • 虚拟文件系统驱动:需要内核签名,部署困难

cfapi 是唯一一个”系统内置 + 开发友好 + 体验和 OneDrive 一致”的方案。它是 Windows 10 1709 起内置的 API,微软官方称之为”Cloud Filter”。

整体架构

整个系统分为 6 层,从上到下依次为:

各层职责

项目 职责
Windows OS 系统层 Explorer 提供用户界面,cfapi 提供占位符/回调机制,NTFS 提供文件系统支撑
SmhDemo.App 应用层 SyncHostedService 管理 6 步启动序列和生命周期,系统托盘提供状态展示和操作入口
SmhDemo.CloudFiles 核心层 SyncProvider 分发 cfapi 回调,HydrationEngine 处理下载水合,FSW 监控本地变更,RemoteSyncService 轮询云端变更
SmhDemo.SmhClient HTTP 层 SmhHttpClient 封装 SMH REST API(秒传/简单/分片三种上传模式),TokenManager 管理访问令牌
SmhDemo.Storage 持久层 SyncStateStore 记录文件同步状态(路径/ETag/大小/InodeId),ScopeRuleStore 管理选择性同步规则
Cloud Backend 云端 SMH API 处理元数据操作,COS 对象存储承载实际文件数据

数据流

  • 下载路径(蓝色):用户双击文件 → Explorer 触发 cfapi 回调 → HydrationEngine 三阶段流水线下载 → 4MB 分块传输给 cfapi → 标记 IN_SYNC
  • 上传路径(绿色):FSW 检测本地变更 → 计算 SHA-256 hash → 转换为同步占位符 → 秒传探测/简单上传/分片上传
  • 远程同步(橙色):RemoteSyncService 每 30 秒轮询 → ETag 对比 → 冲突检测 → 本地占位符创建/更新/删除
  • 脱水(红色):用户右键”释放空间” → Windows 设置 UNPINNED 属性 → FSW 后台扫描检测 → 调用 CfDehydratePlaceholder 释放磁盘空间

核心机制详解

1. 占位符文件:一切的基础

cfapi 的核心概念是占位符文件 (Placeholder File)。它是一个真实存在于 NTFS 文件系统上的文件,但内容在云端,本地只有约 1KB 的元信息(文件名、大小、时间戳等)。在资源管理器中看起来和普通文件一样,只是左下角多了个云朵图标。

占位符文件有三种状态:

状态 图标 含义 磁盘占用
已脱水 ☁️ 蓝色云朵 仅元信息,内容在云端 ~1KB
已水合 ✅ 绿色勾(空心) 内容已下载到本地 完整大小
已固定 ✅ 绿色勾(实心) 系统不会自动回收 完整大小

状态转换逻辑和 OneDrive 完全一致:

☁️ 已脱水 ──双击打开──→ ✅ 已水合 ──"始终保留"──→ ✅ 已固定
  ↑                      │                        │
  └──────"释放空间"───────┴────────────────────────┘

2. 目录浏览:FETCH_PLACEHOLDERS 回调

当用户在资源管理器中展开同步根下的某个文件夹时,Windows 内核驱动 cldflt.sys 会拦截目录枚举请求,并向我们的程序发送 FETCH_PLACEHOLDERS 回调。

// 回调核心流程
void OnFetchPlaceholders(callbackInfo, callbackParameters)
{
    // 1. 转换本地路径为 SMH 云端路径
    var smhPath = ConvertToSmhPath(localPath);

    // 2. 调用 SMH API 获取目录内容
    var items = await smhClient.ListAllDirectoryItemsAsync(smhPath);

    // 3. 用 CfCreatePlaceholders 批量创建占位符
    placeholderManager.CreatePlaceholders(localPath, items);
}

关键踩坑:不要用 CfExecute(TransferPlaceholders) 的数组参数来创建占位符——会创建出”可见但不可访问”的坏占位符。必须用 CfCreatePlaceholders 独立创建,再用 CfExecute 通知完成。

防重入机制:Windows 可能对同一目录多次触发回调。用 ConcurrentDictionary 缓存已填充的目录路径,避免无限循环。

3. 文件下载:三阶段水合引擎

这是整个项目最核心的部分。当用户双击一个”已脱水”的文件时,cldflt.sys 检测到文件内容不可用,发送 FETCH_DATA 回调。

我设计了一个三阶段水合引擎,支持进度报告、断点续传和并发控制:

阶段1: 下载数据
├── 小文件(<50MB): 下载到 MemoryStream
└── 大文件(≥50MB): 下载到临时 FileStream
    └── 支持 HTTP Range 断点续传

阶段2: 传输给 cfapi
├── 4MB 分块(必须 4096 页对齐!)
└── 每 4MB 或 500ms 报告进度(Explorer 显示进度条)

阶段3: 完成处理
├── CfSetInSyncState(IN_SYNC)
├── SHChangeNotify 刷新图标
└── 更新 SQLite 同步状态

页对齐这个坑值得单独说CfExecute(TransferData) 要求 Offset 和 Length 必须是 4096(PAGE_SIZE)的倍数,最后一块除外。但 stream.ReadAsync() 返回的块大小不保证对齐。解决方案是用 4MB 累积缓冲区,攒满一整块再传输:

byte[] chunkBuffer = new byte[4 * 1024 * 1024]; // 4MB,本身已 4096 对齐
int chunkFilled = 0;

while ((bytesRead = await stream.ReadAsync(...)) > 0)
{
    chunkFilled += bytesRead;
    if (chunkFilled >= ChunkSize)
    {
        // 满块传输(已页对齐)
        TransferData(connectionKey, transferKey, chunkBuffer, ChunkSize, transferred);
        transferred += ChunkSize;
    }
}
// 流结束时刷新余量(最后一块不要求页对齐)
if (chunkFilled > 0)
    TransferData(..., chunkFilled, transferred);

4. 文件上传:先转后传 + 秒传

用户拖拽文件到同步文件夹时,FileSystemWatcher 检测到 Created 事件,触发上传流程。

上传流程采用三步走策略:

步骤 1: 计算哈希(在文件还是普通文件时,避免转占位符后数据变化)
         SMH 自定义迭代式 SHA256(不是标准 SHA256)

步骤 2: 转换为同步占位符(先转后传,消除"无身份窗口")

步骤 3: 智能上传
         → 秒传探测(强制 HTTP/1.1)
         → 命中(200) → 直接完成
         → 未命中 → 按文件大小选择上传方式
            → ≤100MB: 简单上传
            → >100MB: 分片上传

秒传的 HTTP/1.1 陷阱:.NET 8 的 HttpClient 默认协商 HTTP/2,但 SMH 服务端在 HTTP/2 下无法正确解析秒传 POST body 中的 hash 参数,导致秒传永远不命中。诊断过程很曲折——curl 和 Python 都返回 200,唯独 .NET 返回 201。最终通过 request.Version = HttpVersion.Version11 解决。

另一个坑:扩展名大小写。SMH 秒传索引按小写扩展名匹配,.MP4 无法命中但 .mp4 可以。解决方案是探测时强制小写扩展名,命中后 rename 回原始文件名。

5. 释放空间(脱水):主动检测而非被动回调

这里有一个非常反直觉的设计:Windows 的”释放空间”操作不会触发 cfapi 的脱水回调

它只是设置了一个 FILE_ATTRIBUTE_UNPINNED 属性标记,然后就没有然后了。提供程序必须自己主动调用 CfDehydratePlaceholder 来执行实际脱水。

所以我用 FileSystemWatcher 监控文件属性变化,检测到 Unpinned 标志后主动执行脱水:

用户右键"释放空间"
  → Windows 设置 Unpinned 属性
  → FSW 检测到 Attributes 变化
  → 检查:是占位符 + 已水合 + Unpinned
  → 主动调用 CfDehydratePlaceholder()
  → 文件变回 ☁️ 云朵状态,释放本地磁盘空间

脱水的前提条件是文件必须处于 IN_SYNC 状态。如果水合后忘记调用 CfSetInSyncState,脱水会返回 0x80070187 错误。

6. 双向增量同步

单向上传解决了,但如果在另一台设备上修改了云端文件呢?需要一个远程同步服务来检测云端变更:

RemoteSyncService 每 30 秒轮询
  → 递归遍历云端目录树
  → 对比本地 sync_state 中的 ETag
  → 检测变更类型:
     ├── REMOTE_NEW: 云端新增 → 本地创建占位符
     ├── REMOTE_MODIFIED: 云端修改 → 本地更新占位符
     └── REMOTE_DELETED: 云端删除 → 保留已水合文件,仅删脱水占位符

智能间隔:基础 30 秒 → 无变更时逐步延长到 120 秒 → 检测到变更切换快速模式 15 秒 × 3 轮。

那些让人头秃的坑

坑 1:文件拷贝到网盘报 0x8007017C

用户拷贝文件到同步文件夹时,Windows 等待占位符转换完成,但转换需要重试(文件可能被 Indexer 锁定),超时后返回错误。

解决:将占位符转换改为后台异步执行,不阻塞 Windows 的复制操作。

坑 2:PPT 保存后文件变成普通文件

PowerPoint 的”安全保存”模式:先写临时文件 pptXXXX.tmp → 删除原文件 → 重命名 tmp 为原文件名。FSW 只看到一个 Renamed 事件,旧路径没有 sync_state,被跳过处理。

解决:通过模式匹配识别(旧路径是 .tmp + 新路径有 sync_state),视为内容更新触发上传。

坑 3:回收站恢复后文件过一会自动消失

这是两个独立 Bug 叠加:

  1. RemoteSync 的 SuppressEvents 缓冲回放模式会回放”RemoteSync 自己删除占位符”产生的 Deleted 事件,导致删了云端文件
  2. HandleRemoteDeletedAsync 无条件删除本地已水合文件

解决:SuppressEvents 改为 Drop 模式(不缓冲不回放),HandleRemoteDeletedAsync 保护已水合文件。

坑 4:下载大文件超时 0x800701AA

cfapi 的 FETCH_DATA 回调有 60 秒超时。大文件一次性下载整个流期间不调用任何 cfapi 函数,超时后报错。

解决:在下载循环中每 15 秒调用一次 CfReportProviderProgress,这个调用会重置超时计时器

开发进度

整个项目经历了 24 个 Phase 的迭代,从最小可用(MVP)到完整的网盘客户端:

  • Phase 1-2:最小可用 — 浏览云端文件 + 下载打开
  • Phase 3-4:写操作 — 上传、创建、删除、重命名
  • Phase 5-7:产品化 — 系统托盘、开机自启、看门狗保活
  • Phase 8:脱水功能 — 释放空间 + FileIdentity 修复
  • Phase 10:水合引擎重构 — 进度报告、断点续传、并发控制
  • Phase 11-16:企业级特性 — 冲突检测、选择性同步、双向同步
  • Phase 17-18:秒传修复 — HTTP/1.1 强制 + 扩展名归一化
  • Phase 19-22:稳定性加固 — 剪切操作、回收站恢复、事件防丢失

累计编写了超过 660 个自动化测试用例,覆盖了水合引擎、秒传检测、冲突解决、选择性同步等核心模块。

总结

做一个”类 OneDrive”的网盘客户端,核心就是用好 Cloud Files API。这个 API 提供了系统级的集成能力,但文档和示例都比较少,开发过程中踩了不少坑。

最关键的几点经验

  1. 占位符文件的页对齐传输 — 违反 4096 对齐要求会得到莫名其妙的错误码
  2. IN_SYNC 状态管理 — 水合后必须标记 IN_SYNC,脱水前必须 IN_SYNC
  3. FileIdentity 不可遗漏 — CfConvertToPlaceholder 必须传入,否则脱水直接报错
  4. “释放空间”需要主动脱水 — Windows 不会自动触发,必须自己检测 Unpinned 属性
  5. FSW 事件不要缓冲回放 — 无法区分事件来源,回放可能导致数据丢失
  6. HTTP 版本很关键 — .NET 默认 HTTP/2,某些服务端(如 SMH)在 HTTP/2 下行为不同

项目代码结构和完整的 3000 行设计文档都维护在仓库中,如果你也在做类似的 Windows 云存储客户端集成,希望这篇文章能帮你少走弯路。

此条目发表在COS, 网盘分类目录,贴了标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注