|
|
@@ -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
|
|
|
+ });
|
|
|
+}
|