// rcp通信工具 import { BusinessError } from '@kit.BasicServicesKit'; import { buffer, HashMap, JSON, util, xml } from '@kit.ArkTS'; import { FileInfo } from '../../viewmodel/FileInfo'; import { rcp } from '@kit.RemoteCommunicationKit'; import { BackgroundManager } from './BackgroundManager'; import FileManager, { merge2paths } from './FileManager'; const UtilName = "heanup RcpSocket" export class RcpSocket { private static instance: RcpSocket; public ErrorMessage: string | BusinessError = '' public filesInfo: FileInfo[] = [] private backgroundManager = BackgroundManager.getInstance() //private rcpSession : rcp.Session | null = null constructor() { console.info(UtilName, 'testTag', 'RcpSocketUtil单例已创建') } private encodeUrlPath(path?: string): string { if (!path || path.length === 0) { return '/'; } let normalized = path.replace(/\\/g, '/'); if (!normalized.startsWith('/')) { normalized = `/${normalized}`; } normalized = normalized.replace(/\/+/g, '/'); const segments = normalized.split('/').map((segment) => { if (!segment || segment.length === 0) { return ''; } let decoded = segment; try { decoded = decodeURIComponent(segment); } catch (_err) { // ignore decode errors and keep raw segment } return encodeURIComponent(decoded); }); let encodedPath = segments.join('/'); if (!encodedPath.startsWith('/')) { encodedPath = `/${encodedPath}`; } if (encodedPath.length === 0) { encodedPath = '/'; } return encodedPath; } private buildRequestUrl(host: string, port: number, path: string, enableHttps: boolean): string { const protocol = enableHttps ? "https" : "http"; const encodedPath = this.encodeUrlPath(path); return `${protocol}://${host}:${port}${encodedPath}`; } static getInstance(): RcpSocket { if (!RcpSocket.instance) { RcpSocket.instance = new RcpSocket(); } return RcpSocket.instance; } public RcpSendHead(host: string, port: number, account: string, password: string, path: string, enableHttps: boolean): Promise { return new Promise((resolve, reject) => { const url = this.buildRequestUrl(host, port, path, enableHttps); const timeoutDuration: number = 10000; const speedThreshold: number = 5000; // 设置速度测试的时间阈值 console.info(UtilName, 'testTag', '发送HEAD的url:' + url) // 创建 RCP 会话配置 let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' } let reqCfg: rcp.Configuration = { security: secCfg, transfer: { timeout: { connectMs: timeoutDuration } } } let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg } let rcpSession = rcp.createSession(sessionCfg); // 构造基本认证头部 const encodedCredentials = buffer .from(`${account}:${password}`) .toString("base64"); const headers: rcp.RequestHeaders = { Depth: "1", "Content-Type": "application/xml", Accept: "text/xml", Authorization: `Basic ${encodedCredentials}`, }; // 创建请求对象 const req = new rcp.Request(url, "HEAD", headers); const startTime = Date.now(); // 连接 try { rcpSession .fetch(req) .then((response) => { const endTime = Date.now(); const elapsedTime = endTime - startTime; // 如果响应时间超过阈值,视为测速过慢 if (elapsedTime > speedThreshold) { console.error(UtilName, "testTag", `${host}Response time too slow: ${elapsedTime}ms`); rcpSession?.close() reject("Test speed too slow"); } else { console.info(UtilName, "testTag", host + " Connect and test speed succeed"); const contentLength = response.headers['content-length'] || '0' let fileSize = 0 if (contentLength) { if (Array.isArray(contentLength)) { const values = contentLength .map((value) => parseInt(value, 10)) .filter((value) => !isNaN(value)); if (values.length > 0) { // 取最大值 fileSize = Math.max(...values); } } else { fileSize = parseInt(contentLength, 10); } } else { console.info(UtilName, 'testTag', 'Content-Length 头未找到'); } rcpSession?.close(); resolve(fileSize); } }) .catch((err: BusinessError) => { console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`); rcpSession?.close() reject(err); }); } catch (e) { console.error(UtilName, 'testTag', '发起连接失败', JSON.stringify(e)) reject(e) } }); } public RcpSendDelete(host: string, port: number, account: string, password: string, path: string, enableHttps: boolean): Promise { return new Promise((resolve, reject) => { const url = this.buildRequestUrl(host, port, path, enableHttps); const timeoutDuration: number = 10000; console.info(UtilName, 'testTag', '发送Delete的url:' + url) // 创建 RCP 会话配置 let response = "" const customHttpEventsHandler: rcp.HttpEventsHandler = { onDataReceive: (incomingData: ArrayBuffer) => { response += this.buf2String(incomingData) }, onDataEnd: () => { }, }; const tracingConfig: rcp.TracingConfiguration = { verbose: true, infoToCollect: { textual: true, incomingData: true, outgoingData: true, }, collectTimeInfo: true, httpEventsHandler: customHttpEventsHandler }; let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' } let reqCfg: rcp.Configuration = { security: secCfg, tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration } } } let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg } let rcpSession = rcp.createSession(sessionCfg); // 构造基本认证头部 const encodedCredentials = buffer .from(`${account}:${password}`) .toString("base64"); const headers: rcp.RequestHeaders = { Authorization: `Basic ${encodedCredentials}`, }; // 创建请求对象 const req = new rcp.Request(url, "DELETE", headers); // 连接 rcpSession .fetch(req) .then(() => { console.info(UtilName, 'testTag', '执行删除的响应', JSON.stringify(response)) rcpSession.close() resolve() }) .catch((err: BusinessError) => { console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`); rcpSession?.close() reject(err); }); }); } public RcpSendMove(host: string, port: number, account: string, password: string, path: string, enableHttps: boolean, newPath: string): Promise { return new Promise((resolve, reject) => { const url = this.buildRequestUrl(host, port, path, enableHttps); const destinationUrl = this.buildRequestUrl(host, port, newPath, enableHttps) const timeoutDuration: number = 10000; console.info(UtilName, 'testTag', '发送MOVE的url:' + url) // 创建 RCP 会话配置 let response = "" const customHttpEventsHandler: rcp.HttpEventsHandler = { onDataReceive: (incomingData: ArrayBuffer) => { response += this.buf2String(incomingData) }, onDataEnd: () => { }, }; const tracingConfig: rcp.TracingConfiguration = { verbose: true, infoToCollect: { textual: true, incomingData: true, outgoingData: true, }, collectTimeInfo: true, httpEventsHandler: customHttpEventsHandler }; let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' } let reqCfg: rcp.Configuration = { security: secCfg, tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration } } } let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg } let rcpSession = rcp.createSession(sessionCfg); // 构造基本认证头部 const encodedCredentials = buffer .from(`${account}:${password}`) .toString("base64"); const headers: rcp.RequestHeaders = { Authorization: `Basic ${encodedCredentials}`, Destination: destinationUrl }; // 创建请求对象 const req = new rcp.Request(url, "MOVE", headers); // 连接 rcpSession .fetch(req) .then(() => { console.info(UtilName, 'testTag', 'move的响应结果', JSON.stringify(response)) rcpSession.close() resolve() }) .catch((err: BusinessError) => { console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`); rcpSession.close() reject(err); }); }); } // rcp方法 public async RcpSendPropFind( host: string, port: number, account: string, password: string, path: string, enableHttps: boolean ): Promise { return new Promise(async (resolve, reject) => { const url = this.buildRequestUrl(host, port, path, enableHttps); const timeoutDuration: number = 10000; console.info(UtilName, 'testTag', '发送PROPFIND请求的url:' + url) // 创建 RCP 会话配置 let response: string = '' const customHttpEventsHandler: rcp.HttpEventsHandler = { onDataReceive: async (incomingData: ArrayBuffer) => { response += this.buf2String(incomingData) await this.backgroundManager.updateDataTransferContinuousTask() }, onDataEnd: () => { }, }; const tracingConfig: rcp.TracingConfiguration = { verbose: true, infoToCollect: { textual: true, incomingData: true, outgoingData: true, }, collectTimeInfo: true, httpEventsHandler: customHttpEventsHandler }; let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' } let reqCfg: rcp.Configuration = {} if (enableHttps) { reqCfg = { security: secCfg, tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration, transferMs: timeoutDuration } } } } else { reqCfg = { tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration } } } } let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg } let rcpSession = rcp.createSession(sessionCfg); // 构造 PROPFIND 请求体 const requestBody = ` `; // 构造基本认证头部 const encodedCredentials = buffer .from(`${account}:${password}`) .toString("base64"); const headers: rcp.RequestHeaders = { "Depth": "1", "Content-Type": "application/xml", "Accept": "text/xml", "Authorization": `Basic ${encodedCredentials}`, }; // 创建请求对象 const req = new rcp.Request(url, "PROPFIND", headers, requestBody); // 发起请求 try { await rcpSession.fetch(req) .finally(() => { console.info(UtilName, 'testTag', 'PROPFIND执行完毕') if (response != '') { console.info(UtilName, 'testTag', 'WebDAV响应内容长度:', response.length.toString()); console.info(UtilName, 'testTag', 'WebDAV响应前500字符:', response.substring(0, 500)); // 提取文件信息 const filesInfo = this.extractHrefContents(response, path, url); console.info(UtilName, 'testTag', '解析出文件数量:', filesInfo.length.toString()); if (filesInfo.length !== 0) { console.info(UtilName, 'testTag', '请求成功') rcpSession.close() resolve(filesInfo); } else { let message = `请求${host}失败,响应信息:${response}` console.error(UtilName, 'testTag', message) rcpSession.close() reject(message) } } else { let error = `服务器响应信息为空,请求失败` console.error(UtilName, 'testTag', error) rcpSession.close() reject(error); } }) // 处理成功响应 //console.info(UtilName, 'testTag', JSON.stringify(res)); } catch (err) { // 处理错误响应 console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`); let error = `错误码${err.code},错误信息${err.data}` rcpSession.close() reject(error) } }); } // PROPFIND递归方法 public async RcpSendPropFindInfinity( host: string, port: number, account: string, password: string, path: string, enableHttps: boolean, useCache: boolean, saveCache: boolean, cachePath: string ): Promise { return new Promise(async (resolve, reject) => { const url = this.buildRequestUrl(host, port, path, enableHttps); const timeoutDuration: number = 10000; console.info(UtilName, 'testTag', '发送PROPFIND递归请求的url:' + url) let cacheFileInfos_str: string = '' if (useCache) { try { cacheFileInfos_str = await FileManager.readFileToString(cachePath) if (cacheFileInfos_str) { let files = JSON.parse(cacheFileInfos_str) as FileInfo[] console.info(UtilName, 'testTag', '使用缓存') resolve(files) return } } catch (e) { console.error(UtilName, 'testTag', '读取缓存失败', JSON.stringify(e)) } } // 创建 RCP 会话配置 let response: string = '' const customHttpEventsHandler: rcp.HttpEventsHandler = { onDataReceive: async (incomingData: ArrayBuffer) => { response += this.buf2String(incomingData) //await this.backgroundManager.updateDataTransferContinuousTask(0,0,path) }, onDataEnd: () => { }, }; const tracingConfig: rcp.TracingConfiguration = { verbose: true, infoToCollect: { textual: true, incomingData: true, outgoingData: true, }, collectTimeInfo: true, httpEventsHandler: customHttpEventsHandler }; let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' } let reqCfg: rcp.Configuration = {} if (enableHttps) { reqCfg = { security: secCfg, tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration, transferMs: timeoutDuration } } } } else { reqCfg = { tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration } } } } let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg } let rcpSession = rcp.createSession(sessionCfg); // 构造 PROPFIND 请求体 const requestBody = ` `; // const requestBody = ` // // `; // 构造基本认证头部 const encodedCredentials = buffer .from(`${account}:${password}`) .toString("base64"); const headers: rcp.RequestHeaders = { "Depth": "1", "Content-Type": "application/xml", "Accept": "text/xml", "Authorization": `Basic ${encodedCredentials}`, }; let sendSinglePropfind = async (url: string, root: string): Promise => { return new Promise(async (resolve, reject) => { if (root.includes('%23recycle')) { resolve([]) return } // 发起请求 try { AppStorage.setOrCreate('CurrentPropfindInfinityRoot', root) response = '' const req = new rcp.Request(url, "PROPFIND", headers, requestBody) await rcpSession.fetch(req) .finally(async () => { if (response != '') { // 提取文件信息 let filesInfo = this.extractHrefContents(response, root, url); let folderInfos: FileInfo[] = [] for (const info of filesInfo) { if (this.isFileFolder(info.name)) { folderInfos.push(info) } } //console.info(UtilName,'testTag','PROPFIND执行完毕','根目录',root,'子目录数量',folderInfos.length) if (folderInfos.length > 0) { for (const folder of folderInfos) { let sub_url = merge2paths(url, folder.name) let sub_root = merge2paths(root, folder.name) let next_infos = await sendSinglePropfind(sub_url, sub_root) filesInfo = filesInfo.concat(next_infos) } } if (filesInfo.length !== 0) { resolve(filesInfo); } else { resolve([]) } } else { resolve([]) } }) } catch (err) { // 处理错误响应 console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`); let error = `错误码${err.code},错误信息${err.data}` resolve([]) } }) } // 创建请求对象 await sendSinglePropfind(url, path) .then(async (filesInfo: FileInfo[]) => { console.info(UtilName, 'testTag', 'PROPFIND目录递归结果', filesInfo.length) rcpSession?.close() if (saveCache) { let str = JSON.stringify(filesInfo) if (str !== cacheFileInfos_str) { try { await FileManager.writeStringToFilePath(str, cachePath) console.info(UtilName, 'testTag', '保存缓存成功') } catch (e) { console.info(UtilName, 'testTag', '保存缓存失败', JSON.stringify(e)) } } } resolve(filesInfo) }).catch((err: BusinessError) => { rcpSession?.close() console.error(UtilName, 'testTag', 'PROPFIND目录递归失败', JSON.stringify(err)) reject(err) }) }); } //ArrayBuffer转utf8字符串 buf2String(buf: ArrayBuffer) { let msgArray = new Uint8Array(buf); let textDecoder = util.TextDecoder.create("utf-8"); return textDecoder.decodeToString(msgArray) } // 提取XML extractXmlContent(httpResponse: string): string { const xmlStart = httpResponse.indexOf(' 标签的内容 extractHrefContents(xmlContent: string, rootpath: string, url: string): FileInfo[] { const filesInfo: FileInfo[] = []; // 匹配每个 const responseRegex = /]*>([\s\S]*?)<\/D:response>/gi; let responseMatch: RegExpExecArray | null; while ((responseMatch = responseRegex.exec(xmlContent)) !== null) { //console.info(UtilName,'testTag',JSON.stringify(responseMatch)) const responseBlock = responseMatch[1]; // 提取 内容 const hrefMatch = responseBlock.match(/(.*?)<\/D:href>/i); if (!hrefMatch) { continue; } // 获取完整的href路径 const fullHref = this.decodeXMLEntities(decodeURI(hrefMatch[1])); // 尝试提取 作为文件名 let displayNameMatch = responseBlock.match(/(.*?)<\/D:displayname>/i); if (!displayNameMatch) { displayNameMatch = responseBlock.match(/(.*?)<\/lp1:displayname>/i); } let name = ''; if (displayNameMatch && displayNameMatch[1]) { // 如果有 displayname,使用它 name = this.decodeXMLEntities(displayNameMatch[1]); console.info(UtilName, 'testTag', '使用displayname作为文件名:', name); } else { // 否则从href中提取文件名(最后一个/后的部分) name = fullHref; const lastSlashIndex = fullHref.lastIndexOf('/'); if (lastSlashIndex >= 0 && lastSlashIndex < fullHref.length - 1) { name = fullHref.substring(lastSlashIndex + 1); } else if (fullHref.endsWith('/')) { // 如果是目录(以/结尾),取倒数第二段 const withoutTrailingSlash = fullHref.substring(0, fullHref.length - 1); const secondLastSlash = withoutTrailingSlash.lastIndexOf('/'); if (secondLastSlash >= 0) { name = withoutTrailingSlash.substring(secondLastSlash + 1) + '/'; } } } // 跳过根目录本身 if (name === '' || name === '/') { continue; } // 提取 let sizeMatch = responseBlock.match(/(.*?)<\/lp1:getcontentlength>/i); if (!sizeMatch) { sizeMatch = responseBlock.match(/(.*?)<\/D:getcontentlength>/i); } // 提取 内容 let lastModifiedMatch = responseBlock.match(/(.*?)<\/D:getlastmodified>/i); if (!lastModifiedMatch) { lastModifiedMatch = responseBlock.match(/(.*?)<\/lp1:getlastmodified>/i); } const lastModified = lastModifiedMatch ? this.convertToUnixTimestamp(lastModifiedMatch[1]) : 0; const size = sizeMatch ? Number(sizeMatch[1]) : 0; // 创建FileInfo并设置WebDAV属性 const fileInfo = new FileInfo(rootpath, name, size, lastModified); fileInfo.href = fullHref; fileInfo.contentLength = size; // 判断是否为文件夹(以/结尾或没有contentLength) fileInfo.isDirectory = fullHref.endsWith('/') || size === 0; filesInfo.push(fileInfo); } return filesInfo; } /** * 获取文件列表 * @param host 主机名 * @param localHost 本地主机名 * @param isUseLocalHost 是否使用本地主机名 * @param port 端口号 * @param path 路径 * @param account 用户名 * @param password 密码 * @param enableHttps 是否启用HTTPS * @returns */ public async getFileList( host: string, localHost: string, isUseLocalHost: boolean, port: number, path: string, account: string, password: string, enableHttps: boolean ): Promise { const actualHost = isUseLocalHost ? localHost : host; return await this.RcpSendPropFind(actualHost, port, account, password, path, enableHttps); } // 订阅HTTP数据传输事件 public subscribeHTTPDataTransfer(callback: (event: string) => void): void { // 简化版:目前不实现具体订阅逻辑 console.info(UtilName, 'testTag', '订阅HTTP数据传输事件(占位)'); } // 获取文件列表(简化版包装方法) // 判断文件是否属于文件夹 /** * 判断文件是否属于文件夹 * @param filename 文件名 * @returns */ private isFileFolder(filename: string): boolean { return filename.toLowerCase().endsWith('/') } // 将 HTTP 日期字符串转换为 Unix 时间戳 private convertToUnixTimestamp(dateString: string): number { const date = new Date(dateString); if (isNaN(date.getTime())) { console.error(UtilName, 'testTag', "非法日期字符串:", dateString); return 0; } return Math.floor(date.getTime() / 1000); } private decodeXMLEntities(str: string): string { const entityMap: HashMap = new HashMap() entityMap.set('&', '&') entityMap.set('<', '<') entityMap.set('>', '>') entityMap.set('"', '"') entityMap.set(''', "'") // 先替换 XML 实体 let result = str.replace(/&(amp|lt|gt|quot|apos);/g, (match: string, entity: string) => { const decoded = entityMap.get(`&${entity};`); return decoded ? decoded : match; }); // 再进行 URL 解码 try { result = decodeURIComponent(result); } catch (error) { // 如果解码失败,保持原样 } return result; } /** * 上传文件到WebDAV服务器 * @param localPath 本地文件路径 * @param remotePath 远程文件路径 * @param host 主机地址 * @param port 端口 * @param account 账户名 * @param password 密码 * @param enableHttps 是否启用HTTPS * @param onProgress 进度回调 * @param maxRetries 最大重试次数,默认3次 */ public async uploadFile( localPath: string, remotePath: string, host: string, port: number, account: string, password: string, enableHttps: boolean, onProgress?: (uploaded: number, total: number) => void, maxRetries: number = 3 ): Promise { let retryCount = 0; let lastError: BusinessError | null = null; // 详细日志:上传开始 console.info(UtilName, 'testTag', '========== RcpSocket上传开始 =========='); console.info(UtilName, 'testTag', `本地路径: ${localPath}`); console.info(UtilName, 'testTag', `远程路径: ${remotePath}`); console.info(UtilName, 'testTag', `目标主机: ${host}:${port}`); console.info(UtilName, 'testTag', `使用HTTPS: ${enableHttps}`); console.info(UtilName, 'testTag', `最大重试次数: ${maxRetries}`); while (retryCount <= maxRetries) { try { if (retryCount > 0) { console.info(UtilName, 'testTag', `第${retryCount}次重试上传...`); } await this.uploadFileInternal( localPath, remotePath, host, port, account, password, enableHttps, onProgress ); console.info(UtilName, 'testTag', '========== RcpSocket上传成功 =========='); console.info(UtilName, 'testTag', `文件: ${remotePath}`); console.info(UtilName, 'testTag', `重试次数: ${retryCount}`); console.info(UtilName, 'testTag', '=========================================='); return; } catch (err) { lastError = err as BusinessError; retryCount++; // 详细错误日志 console.error(UtilName, 'testTag', '---------- 上传失败 ----------'); console.error(UtilName, 'testTag', `文件: ${remotePath}`); console.error(UtilName, 'testTag', `错误码: ${lastError.code}`); console.error(UtilName, 'testTag', `错误信息: ${lastError.message}`); console.error(UtilName, 'testTag', `当前重试次数: ${retryCount}/${maxRetries}`); if (retryCount <= maxRetries) { const delayMs = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); console.warn(UtilName, 'testTag', `将在${delayMs}ms后重试...`); // 等待一段时间后重试,使用指数退避策略 await this.delay(delayMs); } else { console.error(UtilName, 'testTag', '已达到最大重试次数,放弃上传'); } } } // 所有重试都失败 console.error(UtilName, 'testTag', '========== RcpSocket上传失败 =========='); console.error(UtilName, 'testTag', `文件: ${remotePath}`); console.error(UtilName, 'testTag', `已重试: ${maxRetries}次`); console.error(UtilName, 'testTag', `最终错误: ${lastError?.message || '未知错误'}`); console.error(UtilName, 'testTag', '=========================================='); if (lastError) { const error = new Error(lastError.message); throw error; } } /** * 内部上传文件实现 */ private async uploadFileInternal( localPath: string, remotePath: string, host: string, port: number, account: string, password: string, enableHttps: boolean, onProgress?: (uploaded: number, total: number) => void ): Promise { return new Promise(async (resolve, reject) => { const url = this.buildRequestUrl(host, port, remotePath, enableHttps); const timeoutDuration: number = 120000; // 上传超时时间设置为120秒 console.info(UtilName, 'testTag', '开始上传文件到:', url); let rcpSession: rcp.Session | null = null; try { // 检查文件是否存在 console.info(UtilName, 'testTag', '检查本地文件是否存在...'); const fileExists = await FileManager.isExist(localPath); if (!fileExists) { const errorMsg = `本地文件不存在: ${localPath}`; console.error(UtilName, 'testTag', `文件读取错误: ${errorMsg}`); const error = new Error(errorMsg); throw error; } console.info(UtilName, 'testTag', '本地文件存在,继续上传'); // 获取文件大小 console.info(UtilName, 'testTag', '获取文件大小...'); const fileSize = await FileManager.getFileSize(localPath); console.info(UtilName, 'testTag', `文件大小: ${fileSize} 字节 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`); if (fileSize === 0) { const errorMsg = `文件大小为0: ${localPath}`; console.error(UtilName, 'testTag', `文件读取错误: ${errorMsg}`); const error = new Error(errorMsg); throw error; } // 流式读取文件内容 console.info(UtilName, 'testTag', '读取文件内容...'); const fileContent = await this.readFileStream(localPath, fileSize); console.info(UtilName, 'testTag', '文件内容读取完成'); // 创建 RCP 会话配置 let uploadedSize = 0; let lastProgressTime = Date.now(); const progressThrottle = 500; // 进度更新节流,每500ms更新一次 let progressCallCount = 0; // 进度回调计数 const customHttpEventsHandler: rcp.HttpEventsHandler = { onDataReceive: async (incomingData: ArrayBuffer) => { // 上传响应数据接收 uploadedSize += incomingData.byteLength; progressCallCount++; const currentTime = Date.now(); const timeSinceLastUpdate = currentTime - lastProgressTime; const isComplete = uploadedSize >= fileSize; // 节流策略: // 1. 时间间隔超过阈值 // 2. 上传完成 // 3. 每100次回调强制更新一次(防止长时间无更新) const shouldUpdate = timeSinceLastUpdate >= progressThrottle || isComplete || (progressCallCount % 100 === 0); if (onProgress && shouldUpdate) { onProgress(uploadedSize, fileSize); lastProgressTime = currentTime; } // 后台任务更新也进行节流 if (timeSinceLastUpdate >= 1000) { await this.backgroundManager.updateDataTransferContinuousTask(); } }, onDataEnd: () => { console.info(UtilName, 'testTag', '文件数据传输完成'); // 确保最后一次进度更新 if (onProgress) { onProgress(fileSize, fileSize); } } }; const tracingConfig: rcp.TracingConfiguration = { verbose: true, infoToCollect: { textual: true, incomingData: true, outgoingData: true, }, collectTimeInfo: true, httpEventsHandler: customHttpEventsHandler }; let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }; let reqCfg: rcp.Configuration = {}; if (enableHttps) { reqCfg = { security: secCfg, tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration, transferMs: timeoutDuration } } }; } else { reqCfg = { tracing: tracingConfig, transfer: { timeout: { connectMs: timeoutDuration, transferMs: timeoutDuration } } }; } let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }; rcpSession = rcp.createSession(sessionCfg); // 构造基本认证头部 const encodedCredentials = buffer .from(`${account}:${password}`) .toString("base64"); const headers: rcp.RequestHeaders = { Authorization: `Basic ${encodedCredentials}`, 'Content-Type': 'application/octet-stream', 'Content-Length': fileSize.toString() }; // 创建PUT请求对象 const req = new rcp.Request(url, "PUT", headers, fileContent); // 发起上传请求 console.info(UtilName, 'testTag', '发起HTTP PUT请求...'); await rcpSession.fetch(req); console.info(UtilName, 'testTag', '上传请求完成,关闭会话'); if (rcpSession) { rcpSession.close(); } resolve(); } catch (err) { console.error(UtilName, 'testTag', '上传过程中发生错误'); if (rcpSession) { console.info(UtilName, 'testTag', '关闭RCP会话'); rcpSession.close(); } const error = err as BusinessError; // 详细错误分类和日志 if (error.code) { const errorCode = error.code.toString(); if (errorCode.includes('2300002') || errorCode.includes('2300003')) { // 网络连接错误 console.error(UtilName, 'testTag', `网络连接错误: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 网络不可达、主机不可达或连接超时'); } else if (errorCode.includes('2300008')) { // DNS解析错误 console.error(UtilName, 'testTag', `DNS解析错误: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 主机名无法解析'); } else if (errorCode.includes('2300028')) { // 连接超时 console.error(UtilName, 'testTag', `连接超时: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 服务器响应缓慢或网络不稳定'); } else if (errorCode.includes('401')) { // 认证失败 console.error(UtilName, 'testTag', `认证失败: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 用户名或密码错误'); } else if (errorCode.includes('403')) { // 权限不足 console.error(UtilName, 'testTag', `权限不足: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 没有写入权限'); } else if (errorCode.includes('404')) { // 路径不存在 console.error(UtilName, 'testTag', `路径不存在: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 目标路径不存在'); } else if (errorCode.includes('500') || errorCode.includes('503')) { // 服务器错误 console.error(UtilName, 'testTag', `服务器错误: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 服务器内部错误或服务不可用'); } else if (errorCode.includes('507')) { // 存储空间不足 console.error(UtilName, 'testTag', `存储空间不足: 错误码 ${error.code}`); console.error(UtilName, 'testTag', '可能原因: 服务器磁盘空间已满'); } else { console.error(UtilName, 'testTag', `未知错误: 错误码 ${error.code}`); } } console.error(UtilName, "testTag", `错误详情: ${JSON.stringify(error)}`); reject(error); } }); } /** * 流式读取文件 * @param filePath 文件路径 * @param fileSize 文件大小 */ private async readFileStream(filePath: string, fileSize: number): Promise { try { // 性能优化:根据文件大小选择合适的读取策略 const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024; // 50MB阈值 if (fileSize > LARGE_FILE_THRESHOLD) { // 大文件:使用流式读取避免内存溢出 console.info(UtilName, 'testTag', `使用流式读取大文件 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`); // 注意:当前实现仍使用FileManager.readFileToArrayBuffer // 在实际生产环境中,应该实现真正的分块流式读取 // 这里保留接口以便未来扩展 return await FileManager.readFileToArrayBuffer(filePath); } else { // 小文件:直接读取到内存 console.info(UtilName, 'testTag', `直接读取小文件 (${(fileSize / 1024).toFixed(2)} KB)`); return await FileManager.readFileToArrayBuffer(filePath); } } catch (err) { const error = err as Error; console.error(UtilName, 'testTag', `读取文件失败: ${error.message}`); throw error; } } /** * 延迟函数 * @param ms 延迟毫秒数 */ private delay(ms: number): Promise { return new Promise((resolve) => { setTimeout(() => { resolve(); }, ms); }); } }