Explorar o código

feat(remote): 实现上传服务器定位与分片上传优化

- 移除旧的MD5计算方式,引入标准MD5算法实现
- 新增simpleLocateUploadServer函数用于定位最优上传服务器
- 修改simpleUploadFilePart函数签名,增加uploadServer参数
- 更新分片上传逻辑,支持动态选择上传服务器
- 优化multipart/form-data构建过程,提高上传效率
- 修复上传路径转换及文件名编码问题
chendeben hai 8 meses
pai
achega
cfadcffb87

+ 5 - 9
entry/src/main/ets/common/util/RemoteDriveManager.ets

@@ -14,7 +14,7 @@ import { Constants } from '../../Constants';
 import Logger from './Logger';
 import { buffer } from '@kit.ArkTS';
 import { CommonConstants } from '../constants/CommonConstants';
-import { GlobalContext, MD5, PreferencesUtil } from '@pura/harmony-utils';
+import { GlobalContext, PreferencesUtil } from '@pura/harmony-utils';
 import { MusicInfo, parseMusicFileName, Utility } from './Utility';
 import { RemoteDriveType } from '../enums/RemoteDriveType';
 import { createSmbDirectory, deleteSmbEntry, listSmbDirectory, SmbDirectoryEntry } from '../network/SmbBridge';
@@ -27,10 +27,9 @@ import { appendAccessTokenToDlink, BaiduListEntry, BaiduFileMeta, buildAudioStre
 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';
+import { simpleCalculateMD5, simplePrecreateFile, simpleUploadFilePart, simpleCreateFile, TaskPrecreateRequest, TaskPrecreateResponse, TaskUploadPartResponse, TaskCreateFileRequest, TaskCreateFileResponse, convertToBaiduPath, simpleLocateUploadServer } from './TaskPoolHelper';
 
 const TAG = 'heanup RemoteDriveManager';
 
@@ -88,11 +87,7 @@ async function executeBaiduUpload(params: TaskBaiduUploadParams): Promise<TaskBa
       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);
-      let decoder = util.TextDecoder.create('utf-8');
-      const str = decoder.decodeToString(new Uint8Array(partData));
-      const md5 =  await MD5.digestSync(str)
+      const md5 = await simpleCalculateMD5(partData);
       blockMd5List.push(md5);
     }
 
@@ -114,6 +109,7 @@ async function executeBaiduUpload(params: TaskBaiduUploadParams): Promise<TaskBa
       throw new Error('预上传失败:未获取到uploadid');
     }
     console.info(`heanup 预上传成功,uploadid: ${JSON.stringify(precreateResponse)}`);
+    const uploadServer = await simpleLocateUploadServer(accessToken, remotePath, precreateResponse.uploadid!);
     // 5. 分片上传
     const partsToUpload = precreateResponse.block_list || [];
     Logger.info(`heanup 预上传返回block_list: ${JSON.stringify(partsToUpload)}, errno: ${precreateResponse.errno}`);
@@ -129,7 +125,7 @@ async function executeBaiduUpload(params: TaskBaiduUploadParams): Promise<TaskBa
       const partData = fileData.slice(start, end);
 
       Logger.info( `heanup 开始上传分片${partSeq}, 大小: ${partData.byteLength}字节`);
-      await simpleUploadFilePart(accessToken, remotePath, precreateResponse.uploadid!, partSeq, partData);
+      await simpleUploadFilePart(uploadServer, accessToken, remotePath, precreateResponse.uploadid!, partSeq, partData);
       Logger.info(`heanup 分片${partSeq}上传完成`);
     }
 

+ 209 - 91
entry/src/main/ets/common/util/TaskPoolHelper.ets

@@ -5,6 +5,56 @@ import { http } from '@kit.NetworkKit';
 import { util } from '@kit.ArkTS';
 import { JSON } from '@kit.ArkTS';
 
+function md5ConvertToWords(input: Uint8Array): number[] {
+  const words: number[] = [];
+  for (let i = 0; i < input.length; i++) {
+    const idx = (i >> 2);
+    words[idx] = words[idx] || 0;
+    words[idx] |= input[i] << ((i % 4) * 8);
+  }
+  return words;
+}
+
+function md5SafeAdd(x: number, y: number): number {
+  const lsw = (x & 0xffff) + (y & 0xffff);
+  const msw = (x >>> 16) + (y >>> 16) + (lsw >>> 16);
+  return (msw << 16) | (lsw & 0xffff);
+}
+
+function md5RotateLeft(num: number, cnt: number): number {
+  return (num << cnt) | (num >>> (32 - cnt));
+}
+
+function md5Cmn(q: number, a: number, b: number, x: number, s: number, t: number): number {
+  return md5SafeAdd(md5RotateLeft(md5SafeAdd(md5SafeAdd(a, q), md5SafeAdd(x, t)), s), b);
+}
+
+function md5Ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+  return md5Cmn((b & c) | ((~b) & d), a, b, x, s, t);
+}
+
+function md5Gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+  return md5Cmn((b & d) | (c & (~d)), a, b, x, s, t);
+}
+
+function md5Hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+  return md5Cmn(b ^ c ^ d, a, b, x, s, t);
+}
+
+function md5Ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+  return md5Cmn(c ^ (b | (~d)), a, b, x, s, t);
+}
+
+function md5WordsToHex(words: number[]): string {
+  const hexChars = '0123456789abcdef';
+  let output = '';
+  for (let i = 0; i < words.length * 4; i++) {
+    const byte = (words[i >> 2] >>> ((i % 4) * 8)) & 0xff;
+    output += hexChars.charAt((byte >>> 4) & 0x0f) + hexChars.charAt(byte & 0x0f);
+  }
+  return output;
+}
+
 // TaskPool中使用的接口
 export interface TaskPrecreateRequest {
   path: string;
@@ -50,6 +100,16 @@ export interface TaskUploadPartResponse {
   request_id?: number;
 }
 
+interface TaskUploadServerInfo {
+  server: string;
+}
+
+interface TaskLocateUploadResponse {
+  error_code: number;
+  error_msg?: string;
+  servers?: TaskUploadServerInfo[];
+}
+
 export interface TaskCreateFileRequest {
   path: string;
   size: number;
@@ -70,32 +130,101 @@ export interface TaskCreateFileResponse {
  * @returns MD5十六进制字符串
  */
 export async function simpleCalculateMD5(data: ArrayBuffer): Promise<string> {
-  try {
-    // 使用简化的哈希算法,因为HarmonyOS可能没有直接暴露crypto模块
-    // 这里使用一个简单的哈希函数作为替代
-    const uint8Array = new Uint8Array(data);
-    let hash = 0;
-
-    for (let i = 0; i < uint8Array.length; i++) {
-      const byte = uint8Array[i];
-      hash = ((hash << 5) - hash) + byte;
-      hash = hash & hash; // 转换为32位整数
-    }
-
-    // 转换为十六进制字符串并填充到32位,模拟MD5格式
-    const hashStr = Math.abs(hash).toString(16).padStart(8, '0');
-
-    // 为不同大小的数据生成更复杂的哈希
-    const lengthHash = uint8Array.length.toString(16).padStart(8, '0');
-    const firstByteHash = uint8Array.length > 0 ? uint8Array[0].toString(16).padStart(2, '0') : '00';
-    const lastByteHash = uint8Array.length > 0 ? uint8Array[uint8Array.length - 1].toString(16).padStart(2, '0') : '00';
-
-    // 组合生成一个32位的伪MD5哈希
-    return (hashStr + lengthHash + firstByteHash + lastByteHash).substring(0, 32);
-  } catch (error) {
-    const err = error as Error;
-    throw new Error(`MD5计算失败: ${err.message}`);
+  const bytes = new Uint8Array(data);
+  const x = md5ConvertToWords(bytes);
+  const originalBitLength = bytes.length * 8;
+
+  x[originalBitLength >> 5] = x[originalBitLength >> 5] || 0;
+  x[originalBitLength >> 5] |= 0x80 << (originalBitLength % 32);
+  const finalLength = (((originalBitLength + 64) >>> 9) << 4) + 14;
+  x[finalLength] = originalBitLength;
+
+  let a = 1732584193;
+  let b = -271733879;
+  let c = -1732584194;
+  let d = 271733878;
+
+  for (let i = 0; i < x.length; i += 16) {
+    const oldA = a;
+    const oldB = b;
+    const oldC = c;
+    const oldD = d;
+
+    a = md5Ff(a, b, c, d, x[i + 0] || 0, 7, -680876936);
+    d = md5Ff(d, a, b, c, x[i + 1] || 0, 12, -389564586);
+    c = md5Ff(c, d, a, b, x[i + 2] || 0, 17, 606105819);
+    b = md5Ff(b, c, d, a, x[i + 3] || 0, 22, -1044525330);
+    a = md5Ff(a, b, c, d, x[i + 4] || 0, 7, -176418897);
+    d = md5Ff(d, a, b, c, x[i + 5] || 0, 12, 1200080426);
+    c = md5Ff(c, d, a, b, x[i + 6] || 0, 17, -1473231341);
+    b = md5Ff(b, c, d, a, x[i + 7] || 0, 22, -45705983);
+    a = md5Ff(a, b, c, d, x[i + 8] || 0, 7, 1770035416);
+    d = md5Ff(d, a, b, c, x[i + 9] || 0, 12, -1958414417);
+    c = md5Ff(c, d, a, b, x[i + 10] || 0, 17, -42063);
+    b = md5Ff(b, c, d, a, x[i + 11] || 0, 22, -1990404162);
+    a = md5Ff(a, b, c, d, x[i + 12] || 0, 7, 1804603682);
+    d = md5Ff(d, a, b, c, x[i + 13] || 0, 12, -40341101);
+    c = md5Ff(c, d, a, b, x[i + 14] || 0, 17, -1502002290);
+    b = md5Ff(b, c, d, a, x[i + 15] || 0, 22, 1236535329);
+
+    a = md5Gg(a, b, c, d, x[i + 1] || 0, 5, -165796510);
+    d = md5Gg(d, a, b, c, x[i + 6] || 0, 9, -1069501632);
+    c = md5Gg(c, d, a, b, x[i + 11] || 0, 14, 643717713);
+    b = md5Gg(b, c, d, a, x[i + 0] || 0, 20, -373897302);
+    a = md5Gg(a, b, c, d, x[i + 5] || 0, 5, -701558691);
+    d = md5Gg(d, a, b, c, x[i + 10] || 0, 9, 38016083);
+    c = md5Gg(c, d, a, b, x[i + 15] || 0, 14, -660478335);
+    b = md5Gg(b, c, d, a, x[i + 4] || 0, 20, -405537848);
+    a = md5Gg(a, b, c, d, x[i + 9] || 0, 5, 568446438);
+    d = md5Gg(d, a, b, c, x[i + 14] || 0, 9, -1019803690);
+    c = md5Gg(c, d, a, b, x[i + 3] || 0, 14, -187363961);
+    b = md5Gg(b, c, d, a, x[i + 8] || 0, 20, 1163531501);
+    a = md5Gg(a, b, c, d, x[i + 13] || 0, 5, -1444681467);
+    d = md5Gg(d, a, b, c, x[i + 2] || 0, 9, -51403784);
+    c = md5Gg(c, d, a, b, x[i + 7] || 0, 14, 1735328473);
+    b = md5Gg(b, c, d, a, x[i + 12] || 0, 20, -1926607734);
+
+    a = md5Hh(a, b, c, d, x[i + 5] || 0, 4, -378558);
+    d = md5Hh(d, a, b, c, x[i + 8] || 0, 11, -2022574463);
+    c = md5Hh(c, d, a, b, x[i + 11] || 0, 16, 1839030562);
+    b = md5Hh(b, c, d, a, x[i + 14] || 0, 23, -35309556);
+    a = md5Hh(a, b, c, d, x[i + 1] || 0, 4, -1530992060);
+    d = md5Hh(d, a, b, c, x[i + 4] || 0, 11, 1272893353);
+    c = md5Hh(c, d, a, b, x[i + 7] || 0, 16, -155497632);
+    b = md5Hh(b, c, d, a, x[i + 10] || 0, 23, -1094730640);
+    a = md5Hh(a, b, c, d, x[i + 13] || 0, 4, 681279174);
+    d = md5Hh(d, a, b, c, x[i + 0] || 0, 11, -358537222);
+    c = md5Hh(c, d, a, b, x[i + 3] || 0, 16, -722521979);
+    b = md5Hh(b, c, d, a, x[i + 6] || 0, 23, 76029189);
+    a = md5Hh(a, b, c, d, x[i + 9] || 0, 4, -640364487);
+    d = md5Hh(d, a, b, c, x[i + 12] || 0, 11, -421815835);
+    c = md5Hh(c, d, a, b, x[i + 15] || 0, 16, 530742520);
+    b = md5Hh(b, c, d, a, x[i + 2] || 0, 23, -995338651);
+
+    a = md5Ii(a, b, c, d, x[i + 0] || 0, 6, -198630844);
+    d = md5Ii(d, a, b, c, x[i + 7] || 0, 10, 1126891415);
+    c = md5Ii(c, d, a, b, x[i + 14] || 0, 15, -1416354905);
+    b = md5Ii(b, c, d, a, x[i + 5] || 0, 21, -57434055);
+    a = md5Ii(a, b, c, d, x[i + 12] || 0, 6, 1700485571);
+    d = md5Ii(d, a, b, c, x[i + 3] || 0, 10, -1894986606);
+    c = md5Ii(c, d, a, b, x[i + 10] || 0, 15, -1051523);
+    b = md5Ii(b, c, d, a, x[i + 1] || 0, 21, -2054922799);
+    a = md5Ii(a, b, c, d, x[i + 8] || 0, 6, 1873313359);
+    d = md5Ii(d, a, b, c, x[i + 15] || 0, 10, -30611744);
+    c = md5Ii(c, d, a, b, x[i + 6] || 0, 15, -1560198380);
+    b = md5Ii(b, c, d, a, x[i + 13] || 0, 21, 1309151649);
+    a = md5Ii(a, b, c, d, x[i + 4] || 0, 6, -145523070);
+    d = md5Ii(d, a, b, c, x[i + 11] || 0, 10, -1120210379);
+    c = md5Ii(c, d, a, b, x[i + 2] || 0, 15, 718787259);
+    b = md5Ii(b, c, d, a, x[i + 9] || 0, 21, -343485551);
+
+    a = md5SafeAdd(a, oldA);
+    b = md5SafeAdd(b, oldB);
+    c = md5SafeAdd(c, oldC);
+    d = md5SafeAdd(d, oldD);
   }
+
+  return md5WordsToHex([a, b, c, d]);
 }
 
 /**
@@ -199,87 +328,43 @@ export async function simplePrecreateFile(token: string, request: TaskPrecreateR
  * @param partSeq 分片序号
  * @param partData 分片数据
  */
-export async function simpleUploadFilePart(token: string, path: string, uploadId: string, partSeq: number, partData: ArrayBuffer): Promise<void> {
+export async function simpleUploadFilePart(uploadServer: string, token: string, path: string, uploadId: string, partSeq: number, partData: ArrayBuffer): Promise<void> {
   try {
-    // 百度网盘分片上传的正确URL格式
-    const url = 'https://pan.baidu.com/rest/2.0/xpan/file';
-
-    // 转换路径为百度网盘要求的格式
     const baiduPath = convertToBaiduPath(path);
-
-    // 添加调试日志(在TaskPool中无法使用Logger,使用console替代)
-    console.log(`分片${partSeq}上传 - 原始路径: ${path}, 转换后路径: ${baiduPath}, uploadid: ${uploadId}`);
-
-    // 构建正确的查询参数(手动构建,因为URLSearchParams在TaskPool中可能不可用)
     const params = [
       `method=upload`,
+      `type=tmpfile`,
       `access_token=${encodeURIComponent(token)}`,
       `path=${encodeURIComponent(baiduPath)}`,
       `uploadid=${encodeURIComponent(uploadId)}`,
       `partseq=${partSeq}`
     ].join('&');
 
-    const requestUrl = `${url}?${params}`;
+    const requestUrl = `${uploadServer}/rest/2.0/pcs/superfile2?${params}`;
 
-    // 构建multipart/form-data
     const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substr(2, 16);
-    const uint8Array = new Uint8Array(partData);
-
-    // 构建请求头部
-    let formData = '';
-    formData += `--${boundary}\r\n`;
-    formData += `Content-Disposition: form-data; name="file"; filename="${baiduPath.split('/').pop()}"\r\n`;
-    formData += `Content-Type: application/octet-stream\r\n\r\n`;
-
-    // 编码头部
     const encoder = new util.TextEncoder();
-    const headerBytes = encoder.encode(formData);
-
-    // 构建结尾
-    const footerBytes = encoder.encode(`\r\n--${boundary}--\r\n`);
-
-    // 创建完整的请求体
-    const totalSize = headerBytes.length + uint8Array.length + footerBytes.length;
-    const totalBuffer = new ArrayBuffer(totalSize);
-    const totalUint8Array = new Uint8Array(totalBuffer);
-
-    // 组合数据:头部 + 文件数据 + 结尾
-    totalUint8Array.set(headerBytes, 0);
-    totalUint8Array.set(uint8Array, headerBytes.length);
-    totalUint8Array.set(footerBytes, headerBytes.length + uint8Array.length);
-
-    interface UploadRequestHeader {
-      'Content-Type': string;
-      'Content-Length': string;
-      'User-Agent': string;
-      'Referer': string;
-    }
-
-    const header: UploadRequestHeader = {
-      'Content-Type': `multipart/form-data; boundary=${boundary}`,
-      'Content-Length': totalSize.toString(),
-      'User-Agent': 'netdisk;P2SP;2.2.60.26',
-      'Referer': 'https://pan.baidu.com/disk/home'
-    };
+    const headerPart = encoder.encode(
+      `--${boundary}\r\n` +
+      `Content-Disposition: form-data; name="file"; filename="${baiduPath.split('/').pop() || 'upload'}"\r\n` +
+      `Content-Type: application/octet-stream\r\n\r\n`
+    );
+    const footerPart = encoder.encode(`\r\n--${boundary}--\r\n`);
+    const payload = new Uint8Array(headerPart.length + partData.byteLength + footerPart.length);
+    payload.set(headerPart, 0);
+    payload.set(new Uint8Array(partData), headerPart.length);
+    payload.set(footerPart, headerPart.length + partData.byteLength);
 
     const httpRequest = http.createHttp();
-
-    // 设置更长的超时时间,因为大文件上传可能需要更多时间
-    interface HttpRequestOptions {
-      method: http.RequestMethod;
-      header: UploadRequestHeader;
-      extraData: ArrayBuffer;
-      readTimeout: number;
-      connectTimeout: number;
-      expectDataType: http.HttpDataType;
-    }
-
-    const options: HttpRequestOptions = {
+    const options: http.HttpRequestOptions = {
       method: http.RequestMethod.POST,
-      header: header,
-      extraData: totalBuffer,
-      readTimeout: 60000, // 60秒读取超时
-      connectTimeout: 30000, // 30秒连接超时
+      header: {
+        'Content-Type': `multipart/form-data; boundary=${boundary}`,
+        'User-Agent': 'pan.baidu.com'
+      },
+      extraData: payload.buffer,
+      readTimeout: 60000,
+      connectTimeout: 30000,
       expectDataType: http.HttpDataType.STRING
     };
 
@@ -404,4 +489,37 @@ export async function simpleCreateFile(token: string, request: TaskCreateFileReq
     const err = error as Error;
     throw new Error(`创建文件失败: ${err.message}`);
   }
-}
+}
+
+export async function simpleLocateUploadServer(token: string, path: string, uploadId: string): Promise<string> {
+  const baiduPath = convertToBaiduPath(path);
+  const url = 'https://d.pcs.baidu.com/rest/2.0/pcs/file';
+  const params = [
+    `method=locateupload`,
+    `appid=250528`,
+    `access_token=${encodeURIComponent(token)}`,
+    `path=${encodeURIComponent(baiduPath)}`,
+    `uploadid=${encodeURIComponent(uploadId)}`,
+    `upload_version=2.0`
+  ].join('&');
+  const httpRequest = http.createHttp();
+  try {
+    const response = await httpRequest.request(`${url}?${params}`, {
+      method: http.RequestMethod.GET,
+      expectDataType: http.HttpDataType.STRING,
+      readTimeout: 15000,
+      connectTimeout: 15000
+    });
+    if (response.responseCode !== 200 || !response.result) {
+      throw new Error(`locateupload失败: ${response.responseCode}`);
+    }
+    const parsed = JSON.parse(response.result as string) as TaskLocateUploadResponse;
+    if (parsed.error_code !== 0 || !parsed.servers || parsed.servers.length === 0) {
+      throw new Error(`locateupload返回异常: ${parsed.error_code}`);
+    }
+    const httpsServer = parsed.servers.find((server: TaskUploadServerInfo) => server.server.startsWith('https://'));
+    return (httpsServer || parsed.servers[0]).server;
+  } finally {
+    httpRequest.destroy();
+  }
+}