|
@@ -23,11 +23,152 @@ import { WebDavUrlUtil } from './WebDavUrlUtil';
|
|
|
import MediaTable from './MediaTable';
|
|
import MediaTable from './MediaTable';
|
|
|
import { FtpClient, FileInfo as FtpEntryInfo, StringEncoding, AccessOptions } from '@liuzhosoft/ftp4h';
|
|
import { FtpClient, FileInfo as FtpEntryInfo, StringEncoding, AccessOptions } from '@liuzhosoft/ftp4h';
|
|
|
import { BaiduConstants } from '../constants/BaiduConstants';
|
|
import { BaiduConstants } from '../constants/BaiduConstants';
|
|
|
-import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, buildAudioStreamingUrl, createFolder, deleteFiles as deleteBaiduFiles, ensureAudioStreamReady, fetchFileMetas as fetchBaiduFileMetas, listDirectory as listBaiduDirectory, refreshAccessToken as refreshBaiduAccessToken } from '../network/BaiduPanClient';
|
|
|
|
|
|
|
+import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, buildAudioStreamingUrl, calculateMD5, createFolder, deleteFiles as deleteBaiduFiles, ensureAudioStreamReady, fetchFileMetas as fetchBaiduFileMetas, getUploadDomain, listDirectory as listBaiduDirectory, precreateFile, refreshAccessToken as refreshBaiduAccessToken, uploadFilePart } from '../network/BaiduPanClient';
|
|
|
import { ServerLogUtil } from './ServerLogUtil';
|
|
import { ServerLogUtil } from './ServerLogUtil';
|
|
|
|
|
+import { taskpool } from '@kit.ArkTS';
|
|
|
|
|
+import { fileIo } from '@kit.CoreFileKit';
|
|
|
|
|
+import { util } from '@kit.ArkTS';
|
|
|
|
|
+import { http } from '@kit.NetworkKit';
|
|
|
|
|
+import { JSON } from '@kit.ArkTS';
|
|
|
|
|
+import { simpleCalculateMD5, simplePrecreateFile, simpleUploadFilePart, simpleCreateFile, TaskPrecreateRequest, TaskPrecreateResponse, TaskUploadPartResponse, TaskCreateFileRequest, TaskCreateFileResponse, convertToBaiduPath } from './TaskPoolHelper';
|
|
|
|
|
|
|
|
const TAG = 'heanup RemoteDriveManager';
|
|
const TAG = 'heanup RemoteDriveManager';
|
|
|
|
|
|
|
|
|
|
+// 百度网盘上传TaskPool任务接口(使用前缀避免名称冲突)
|
|
|
|
|
+interface TaskBaiduUploadParams {
|
|
|
|
|
+ filePath: string;
|
|
|
|
|
+ accessToken: string;
|
|
|
|
|
+ remotePath: string;
|
|
|
|
|
+ songName: string;
|
|
|
|
|
+ songId: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface TaskBaiduUploadResult {
|
|
|
|
|
+ success: boolean;
|
|
|
|
|
+ error?: string;
|
|
|
|
|
+ filePath?: string;
|
|
|
|
|
+ remotePath?: string;
|
|
|
|
|
+ songId?: string;
|
|
|
|
|
+ uploadId?: string;
|
|
|
|
|
+ progress?: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 百度网盘文件上传TaskPool任务
|
|
|
|
|
+ * 注意:TaskPool任务函数不能访问外部变量,必须使用纯函数
|
|
|
|
|
+ */
|
|
|
|
|
+@Concurrent
|
|
|
|
|
+async function executeBaiduUpload(params: TaskBaiduUploadParams): Promise<TaskBaiduUploadResult> {
|
|
|
|
|
+ const filePath = params.filePath;
|
|
|
|
|
+ const accessToken = params.accessToken;
|
|
|
|
|
+ const remotePath = params.remotePath;
|
|
|
|
|
+ const songName = params.songName;
|
|
|
|
|
+ const songId = params.songId;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 读取文件数据
|
|
|
|
|
+ const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ const stat = await fileIo.stat(filePath);
|
|
|
|
|
+ const fileSize = stat.size;
|
|
|
|
|
+ const fileData = new ArrayBuffer(fileSize);
|
|
|
|
|
+ await fileIo.read(file.fd, fileData);
|
|
|
|
|
+ fileIo.closeSync(file);
|
|
|
|
|
+
|
|
|
|
|
+ if (fileSize === 0) {
|
|
|
|
|
+ throw new Error('文件大小为0');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 计算分片信息
|
|
|
|
|
+ const PART_SIZE = 4 * 1024 * 1024; // 4MB分片大小
|
|
|
|
|
+ const totalParts = Math.ceil(fileSize / PART_SIZE);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 计算分片MD5列表
|
|
|
|
|
+ const blockMd5List: string[] = [];
|
|
|
|
|
+ for (let i = 0; i < totalParts; i++) {
|
|
|
|
|
+ const start = i * PART_SIZE;
|
|
|
|
|
+ const end = Math.min(start + PART_SIZE, fileSize);
|
|
|
|
|
+ const partData = fileData.slice(start, end);
|
|
|
|
|
+ // 使用导入的MD5计算函数
|
|
|
|
|
+ const md5 = await simpleCalculateMD5(partData);
|
|
|
|
|
+ blockMd5List.push(md5);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 预上传(第一阶段)
|
|
|
|
|
+ const precreateRequest: TaskPrecreateRequest = {
|
|
|
|
|
+ path: remotePath,
|
|
|
|
|
+ size: fileSize,
|
|
|
|
|
+ isdir: 0,
|
|
|
|
|
+ block_list: JSON.stringify(blockMd5List),
|
|
|
|
|
+ autoinit: 1,
|
|
|
|
|
+ rtype: 1,
|
|
|
|
|
+ local_ctime: Math.floor(Date.now() / 1000).toString(),
|
|
|
|
|
+ local_mtime: Math.floor(Date.now() / 1000).toString(),
|
|
|
|
|
+ dupicate_check: 0 // 不检查重复文件
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const precreateResponse = await simplePrecreateFile(accessToken, precreateRequest);
|
|
|
|
|
+ if (!precreateResponse.uploadid) {
|
|
|
|
|
+ throw new Error('预上传失败:未获取到uploadid');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 分片上传
|
|
|
|
|
+ const partsToUpload = precreateResponse.block_list || [];
|
|
|
|
|
+ Logger.info(`预上传返回block_list: ${JSON.stringify(partsToUpload)}, errno: ${precreateResponse.errno}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果block_list为空,说明所有分片都已存在,直接跳过分片上传
|
|
|
|
|
+ if (partsToUpload.length === 0) {
|
|
|
|
|
+ Logger.info( `所有分片已存在,跳过分片上传阶段`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Logger.info( `需要上传的分片列表: ${JSON.stringify(partsToUpload)}, 总计${partsToUpload.length}个分片`);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < partsToUpload.length; i++) {
|
|
|
|
|
+ const partSeq = partsToUpload[i];
|
|
|
|
|
+ const start = partSeq * PART_SIZE;
|
|
|
|
|
+ const end = Math.min(start + PART_SIZE, fileSize);
|
|
|
|
|
+ const partData = fileData.slice(start, end);
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info( `开始上传分片${partSeq}, 大小: ${partData.byteLength}字节`);
|
|
|
|
|
+ await simpleUploadFilePart(accessToken, remotePath, precreateResponse.uploadid!, partSeq, partData);
|
|
|
|
|
+ Logger.info(`分片${partSeq}上传完成`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 创建文件(第三阶段)
|
|
|
|
|
+ const createFileRequest: TaskCreateFileRequest = {
|
|
|
|
|
+ path: remotePath,
|
|
|
|
|
+ size: fileSize,
|
|
|
|
|
+ uploadid: precreateResponse.uploadid!,
|
|
|
|
|
+ block_list: JSON.stringify(blockMd5List)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const createFileResponse = await simpleCreateFile(accessToken, createFileRequest);
|
|
|
|
|
+ if (!createFileResponse.path) {
|
|
|
|
|
+ throw new Error('创建文件失败:未获取到文件路径');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ filePath: filePath,
|
|
|
|
|
+ remotePath: createFileResponse.path,
|
|
|
|
|
+ songId: songId,
|
|
|
|
|
+ uploadId: precreateResponse.uploadid,
|
|
|
|
|
+ progress: 100
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: false,
|
|
|
|
|
+ error: err.message,
|
|
|
|
|
+ filePath: filePath,
|
|
|
|
|
+ remotePath: remotePath,
|
|
|
|
|
+ songId: songId,
|
|
|
|
|
+ progress: 0
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
export interface BreadcrumbItem {
|
|
export interface BreadcrumbItem {
|
|
|
label: string;
|
|
label: string;
|
|
|
path: string;
|
|
path: string;
|
|
@@ -2302,7 +2443,12 @@ export class RemoteDriveManager {
|
|
|
this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
|
|
this.notifyObservers(RemoteDriveManagerStates.SetCurrentUploadTask);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- await this.uploadSingleFile(task);
|
|
|
|
|
|
|
+ if(this.currentAccount.webType==RemoteDriveType.WebDav){
|
|
|
|
|
+ await this.uploadSingleFile(task);
|
|
|
|
|
+ }else if(this.currentAccount.webType==RemoteDriveType.Baidu){
|
|
|
|
|
+ await this.uploadBaiduFile(task);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
|
|
|
// 上传成功,移到完成队列
|
|
// 上传成功,移到完成队列
|
|
|
Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
|
|
Logger.info(TAG, `任务成功,移至完成队列: ${task.song.name}`);
|
|
@@ -2601,7 +2747,7 @@ export class RemoteDriveManager {
|
|
|
const err = error as Error;
|
|
const err = error as Error;
|
|
|
const endTime = Date.now();
|
|
const endTime = Date.now();
|
|
|
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 详细错误日志
|
|
// 详细错误日志
|
|
|
Logger.error(TAG, '========== 上传任务失败 ==========');
|
|
Logger.error(TAG, '========== 上传任务失败 ==========');
|
|
|
Logger.error(TAG, `文件名: ${song.name}`);
|
|
Logger.error(TAG, `文件名: ${song.name}`);
|
|
@@ -2614,15 +2760,133 @@ export class RemoteDriveManager {
|
|
|
Logger.error(TAG, `错误堆栈: ${err.stack || '无堆栈信息'}`);
|
|
Logger.error(TAG, `错误堆栈: ${err.stack || '无堆栈信息'}`);
|
|
|
Logger.error(TAG, `重试次数: ${task.retryCount || 0}`);
|
|
Logger.error(TAG, `重试次数: ${task.retryCount || 0}`);
|
|
|
Logger.error(TAG, '====================================');
|
|
Logger.error(TAG, '====================================');
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 根据错误类型进行分类处理
|
|
// 根据错误类型进行分类处理
|
|
|
this.handleUploadError(err, task);
|
|
this.handleUploadError(err, task);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 重新抛出错误以便上层处理
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传百度网盘文件(使用TaskPool避免主线程阻塞)
|
|
|
|
|
+ * @param task 上传任务
|
|
|
|
|
+ */
|
|
|
|
|
+ private async uploadBaiduFile(task: TransferTask): Promise<void> {
|
|
|
|
|
+ const song = task.song;
|
|
|
|
|
+ const account = task.account;
|
|
|
|
|
+ const startTime = Date.now();
|
|
|
|
|
+
|
|
|
|
|
+ // 详细日志:百度网盘上传开始
|
|
|
|
|
+ Logger.info(TAG, '========== 百度网盘上传任务开始 ==========');
|
|
|
|
|
+ Logger.info(TAG, `文件名: ${song.name}`);
|
|
|
|
|
+ Logger.info(TAG, `文件路径: ${song.filePath}`);
|
|
|
|
|
+ Logger.info(TAG, `文件大小: ${song.videoSize || 0} 字节`);
|
|
|
|
|
+ Logger.info(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
|
|
|
|
|
+ Logger.info(TAG, `重试次数: ${task.retryCount || 0}`);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查网络是否允许上传
|
|
|
|
|
+ Logger.info(TAG, '检查网络连接状态...');
|
|
|
|
|
+ const networkAllowed = await this.checkNetworkAllowed();
|
|
|
|
|
+ if (!networkAllowed) {
|
|
|
|
|
+ const errorMsg = '当前网络不允许上传,请连接WiFi或在设置中允许移动网络上传';
|
|
|
|
|
+ Logger.error(TAG, `网络检查失败: ${errorMsg}`);
|
|
|
|
|
+ throw new Error(errorMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ Logger.info(TAG, '网络检查通过');
|
|
|
|
|
+
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.UploadStart);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取访问令牌
|
|
|
|
|
+ const accessToken = await this.ensureBaiduAccessToken(account);
|
|
|
|
|
+ if (!accessToken) {
|
|
|
|
|
+ throw new Error('百度网盘授权已过期,请重新登录');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建上传路径
|
|
|
|
|
+ const uploadPath = task.customUploadPath || account.uploadFilePath || '/apps/ttmusic';
|
|
|
|
|
+ const normalizedPath = this.normalizeFullPath(uploadPath);
|
|
|
|
|
+ const fileName = song.fileName || song.name || `upload-${Date.now()}`;
|
|
|
|
|
+ const remotePath = `${normalizedPath === '/' ? '' : normalizedPath}/${fileName}`;
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, '---------- 百度网盘路径信息 ----------');
|
|
|
|
|
+ Logger.info(TAG, `上传基础路径: ${uploadPath}`);
|
|
|
|
|
+ Logger.info(TAG, `规范化后的路径: ${normalizedPath}`);
|
|
|
|
|
+ Logger.info(TAG, `文件名: ${fileName}`);
|
|
|
|
|
+ Logger.info(TAG, `最终远程路径: ${remotePath}`);
|
|
|
|
|
+ Logger.info(TAG, '------------------------------');
|
|
|
|
|
+
|
|
|
|
|
+ // 使用TaskPool执行上传任务
|
|
|
|
|
+ const taskId = `baidu-upload-${song.id || Date.now()}`;
|
|
|
|
|
+ Logger.info(TAG, '创建TaskPool上传任务...');
|
|
|
|
|
+
|
|
|
|
|
+ // 转换路径为百度网盘要求的格式
|
|
|
|
|
+ const baiduRemotePath = convertToBaiduPath(remotePath);
|
|
|
|
|
+ Logger.info(TAG, `原始路径: ${remotePath}, 转换后路径: ${baiduRemotePath}`);
|
|
|
|
|
+
|
|
|
|
|
+ const taskParams: TaskBaiduUploadParams = {
|
|
|
|
|
+ filePath: song.filePath,
|
|
|
|
|
+ accessToken: accessToken,
|
|
|
|
|
+ remotePath: baiduRemotePath,
|
|
|
|
|
+ songName: song.name,
|
|
|
|
|
+ songId: song.id || `file-${Date.now()}`
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const uploadTask: taskpool.Task = new taskpool.Task(executeBaiduUpload, taskParams);
|
|
|
|
|
+
|
|
|
|
|
+ // 提交任务到TaskPool
|
|
|
|
|
+ Logger.info(TAG, '提交上传任务到TaskPool...');
|
|
|
|
|
+ const result = await taskpool.execute(uploadTask) as TaskBaiduUploadResult;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查上传结果
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ const errorMessage = result.error || '上传失败';
|
|
|
|
|
+ throw new Error(errorMessage);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 上传成功
|
|
|
|
|
+ const endTime = Date.now();
|
|
|
|
|
+ const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
|
|
|
+
|
|
|
|
|
+ Logger.info(TAG, '========== 百度网盘上传任务成功 ==========');
|
|
|
|
|
+ Logger.info(TAG, `文件名: ${song.name}`);
|
|
|
|
|
+ Logger.info(TAG, `文件路径: ${result.filePath || song.filePath}`);
|
|
|
|
|
+ Logger.info(TAG, `目标路径: ${result.remotePath || remotePath}`);
|
|
|
|
|
+ Logger.info(TAG, `UploadId: ${result.uploadId || 'unknown'}`);
|
|
|
|
|
+ Logger.info(TAG, `耗时: ${duration} 秒`);
|
|
|
|
|
+ Logger.info(TAG, '====================================');
|
|
|
|
|
+
|
|
|
|
|
+ // 更新上传进度到100%
|
|
|
|
|
+ this.notifyObservers(RemoteDriveManagerStates.UploadProgress);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ const endTime = Date.now();
|
|
|
|
|
+ const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
|
|
|
+
|
|
|
|
|
+ // 详细错误日志
|
|
|
|
|
+ Logger.error(TAG, '========== 百度网盘上传任务失败 ==========');
|
|
|
|
|
+ Logger.error(TAG, `文件名: ${song.name}`);
|
|
|
|
|
+ Logger.error(TAG, `文件路径: ${song.filePath}`);
|
|
|
|
|
+ Logger.error(TAG, `目标账户: ${account.name} (ID: ${account.id})`);
|
|
|
|
|
+ Logger.error(TAG, `耗时: ${duration} 秒`);
|
|
|
|
|
+ Logger.error(TAG, `错误信息: ${err.message}`);
|
|
|
|
|
+ Logger.error(TAG, `错误堆栈: ${err.stack || '无堆栈信息'}`);
|
|
|
|
|
+ Logger.error(TAG, `重试次数: ${task.retryCount || 0}`);
|
|
|
|
|
+ Logger.error(TAG, '====================================');
|
|
|
|
|
+
|
|
|
|
|
+ // 根据错误类型进行分类处理
|
|
|
|
|
+ this.handleUploadError(err, task);
|
|
|
|
|
+
|
|
|
// 重新抛出错误以便上层处理
|
|
// 重新抛出错误以便上层处理
|
|
|
throw err;
|
|
throw err;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取错误类型
|
|
* 获取错误类型
|
|
|
* @param error 错误对象
|
|
* @param error 错误对象
|