Просмотр исходного кода

feat(network): 实现百度网盘文件分片下载缓存功能

- 新增百度网盘文件缓存策略类 BaiduCacheStrategy
- 实现分片下载函数 downloadBaiduFileChunked 支持大文件断点续传
- 添加单片下载函数 downloadSingleChunk 处理 HTTP Range 请求
- 集成远程缓存管理器 RemoteCacheManager 管理百度网盘资源
- 支持自动创建缓存目录及文件路径解析
- 添加下载进度日志记录与性能统计
- 实现文件完整性校验与异常重试机制
chendeben 8 месяцев назад
Родитель
Сommit
c1ed858f92
1 измененных файлов с 205 добавлено и 0 удалено
  1. 205 0
      entry/src/main/ets/common/network/BaiduFileCache.ets

+ 205 - 0
entry/src/main/ets/common/network/BaiduFileCache.ets

@@ -0,0 +1,205 @@
+import { http } from '@kit.NetworkKit';
+import FileManager from '../util/FileManager';
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
+import Logger from '../util/Logger';
+import { RemoteCacheType, resolveCacheFilePath, normalizeCacheRelativePath } from './RemoteSongCache';
+import { fileIo } from '@kit.CoreFileKit';
+import { RemoteCacheManager, RemoteCacheRequest, RemoteCacheStrategy } from './RemoteCacheManager';
+import { buffer, util } from '@kit.ArkTS';
+
+const TAG = 'BaiduFileCache';
+const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB per chunk
+
+async function downloadBaiduFileChunked(downloadUrl: string, localPath: string, totalSize: number): Promise<void> {
+  try {
+    Logger.info(TAG, `开始下载: ${downloadUrl}, 文件大小: ${totalSize} 字节`);
+
+    // 分片下载
+    const file = fileIo.openSync(localPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.TRUNC);
+    try {
+      if (totalSize === 0) {
+        // 无法获取大小,一次性下载
+        Logger.info(TAG, `文件大小为0,使用一次性下载`);
+        await downloadSingleChunk(downloadUrl, 0, -1, file);
+      } else {
+        // 分片下载
+        let downloadedSize: number = 0;
+        let chunkIndex: number = 0;
+
+        while (downloadedSize < totalSize) {
+          const rangeStart = downloadedSize;
+          const rangeEnd = Math.min(downloadedSize + CHUNK_SIZE - 1, totalSize - 1);
+          const chunkSizeBytes = rangeEnd - rangeStart + 1;
+          const progressPercent = Math.round((downloadedSize / totalSize) * 100);
+
+          Logger.info(TAG, `下载进度: ${progressPercent}% (${downloadedSize}/${totalSize} 字节) 分片 ${chunkIndex + 1}`);
+          Logger.info(TAG, `下载分片 ${chunkIndex + 1}: 字节范围 ${rangeStart}-${rangeEnd} (大小 ${chunkSizeBytes} 字节)`);
+          
+          const chunkStartTime = Date.now();
+          await downloadSingleChunk(downloadUrl, rangeStart, rangeEnd, file);
+          const chunkEndTime = Date.now();
+          const chunkDuration = (chunkEndTime - chunkStartTime) / 1000;
+          
+          downloadedSize = rangeEnd + 1;
+          chunkIndex++;
+          
+          Logger.info(TAG, `分片 ${chunkIndex} 下载完成,耗时 ${chunkDuration.toFixed(2)} 秒`);
+        }
+      }
+
+      const finalSize = await FileManager.getFileSize(localPath);
+      Logger.info(TAG, `下载完成,最终文件大小: ${finalSize} 字节`);
+    } finally {
+      fileIo.closeSync(file);
+    }
+  } catch (error) {
+    const err = error as Error;
+    Logger.error(TAG, `分片下载失败: ${err.message}`);
+    throw err;
+  }
+}
+
+async function downloadSingleChunk(
+  downloadUrl: string,
+  rangeStart: number,
+  rangeEnd: number,
+  file: fileIo.File
+): Promise<void> {
+  const httpRequest = http.createHttp();
+
+  try {
+    const options: http.HttpRequestOptions = {
+      method: http.RequestMethod.GET,
+      readTimeout: 180000,
+      connectTimeout: 30000,
+      expectDataType: http.HttpDataType.ARRAY_BUFFER,
+      header: {
+        'User-Agent': 'pan.baidu.com'
+      }
+    };
+
+    // 添加 Range 头支持分片
+    if (rangeEnd >= 0) {
+      if (!options.header) {
+        options.header = {};
+      }
+      options.header['Range'] = `bytes=${rangeStart}-${rangeEnd}`;
+      const rangeSize = rangeEnd - rangeStart + 1;
+      Logger.info(TAG, `开始下载分片: 范围 ${rangeStart}-${rangeEnd} (${(rangeSize / 1024 / 1024).toFixed(2)}MB)`);
+    } else {
+      Logger.info(TAG, `开始一次性下载完整文件`);
+    }
+
+    const startTime = Date.now();
+    const response = await httpRequest.request(downloadUrl, options);
+    const endTime = Date.now();
+    const duration = (endTime - startTime) / 1000;
+
+    // 206 是分片响应码,200 是完整响应码,都是成功
+    if (response.responseCode !== 200 && response.responseCode !== 206) {
+      throw new Error(`HTTP ${response.responseCode}`);
+    }
+
+    if (response.result instanceof ArrayBuffer) {
+      await fileIo.write(file.fd, response.result);
+      const sizeMB = (response.result.byteLength / 1024 / 1024).toFixed(2);
+      Logger.info(TAG, `分片数据写入完成: ${response.result.byteLength} 字节 (${sizeMB}MB),耗时 ${duration.toFixed(2)} 秒,速度 ${((response.result.byteLength / 1024 / 1024) / duration).toFixed(2)}MB/s`);
+    } else if (typeof response.result === 'string') {
+      const encoder = new util.TextEncoder();
+      const buf = encoder.encode(response.result).buffer;
+      await fileIo.write(file.fd, buf);
+      Logger.info(TAG, `分片字符串数据写入完成,耗时 ${duration.toFixed(2)} 秒`);
+    }
+  } catch (error) {
+    const err = error as Error;
+    Logger.error(TAG, `分片下载失败 (${rangeStart}-${rangeEnd}): ${err.message}`);
+    throw err;
+  } finally {
+    httpRequest.destroy();
+  }
+}
+
+async function ensureBaiduFileCachedInternal(
+  account: WebDavAccount,
+  fsId: string,
+  remotePath: string,
+  downloadUrl: string,
+  fileSize: number
+): Promise<string> {
+  const normalizedRelative = normalizeCacheRelativePath(remotePath);
+  const cacheInfo = await resolveCacheFilePath(
+    RemoteCacheType.BAIDU,
+    account.id?.toString(),
+    normalizedRelative
+  );
+  const cachePath = cacheInfo.cachePath;
+
+  let exists = await FileManager.isExist(cachePath);
+  if (exists) {
+    const size = await FileManager.getFileSize(cachePath);
+    if (size <= 0) {
+      await FileManager.deleteFile(cachePath);
+      exists = false;
+    }
+  }
+
+  if (!exists) {
+    try {
+      Logger.info(TAG, `开始下载百度网盘文件: fsId=${fsId}, fileSize=${fileSize} 字节, 到 ${cachePath}`);
+      await downloadBaiduFileChunked(downloadUrl, cachePath, fileSize);
+      Logger.info(TAG, `百度网盘文件下载完成: ${cachePath}`);
+    } catch (error) {
+      await FileManager.deleteFile(cachePath);
+      const err = error as Error;
+      Logger.error(TAG, `百度网盘下载失败: ${err.message}`);
+      throw err;
+    }
+  } else {
+    Logger.info(TAG, `百度网盘文件已缓存: ${cachePath}`);
+  }
+
+  return cachePath;
+}
+
+class BaiduCacheStrategy implements RemoteCacheStrategy {
+  readonly type: RemoteCacheType = RemoteCacheType.BAIDU;
+
+  async ensure(options: RemoteCacheRequest): Promise<string> {
+    const downloadUrl = options.downloadUrl;
+    const fsId = options.fsId;
+    const fileSize = options.fileSize ?? 0;
+
+    if (!downloadUrl) {
+      throw new Error('缺少百度网盘下载链接');
+    }
+    if (!fsId) {
+      throw new Error('缺少百度网盘文件ID');
+    }
+
+    return ensureBaiduFileCachedInternal(
+      options.account,
+      fsId,
+      options.remotePath,
+      downloadUrl,
+      fileSize
+    );
+  }
+}
+
+RemoteCacheManager.registerStrategy(new BaiduCacheStrategy());
+
+export async function ensureBaiduFileCached(
+  account: WebDavAccount,
+  fsId: string,
+  remotePath: string,
+  downloadUrl: string,
+  fileSize: number
+): Promise<string> {
+  return RemoteCacheManager.ensureCached(RemoteCacheType.BAIDU, {
+    account,
+    remotePath,
+    fsId,
+    downloadUrl,
+    fileSize
+  });
+}