| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148 |
- // 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<number> {
- return new Promise<number>((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<void> {
- return new Promise<void>((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 createDirectory(host: string, port: number, account: string, password: string, path: string,
- enableHttps: boolean): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- const url = this.buildRequestUrl(host, port, path, enableHttps);
- const timeoutDuration: number = 10000;
- console.info(UtilName, 'testTag', '发送MKCOL的url:' + url);
- 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 = {
- Authorization: `Basic ${encodedCredentials}`,
- 'Content-Type': 'application/xml'
- };
- const req = new rcp.Request(url, "MKCOL", headers);
- rcpSession
- .fetch(req)
- .then(() => {
- rcpSession.close();
- resolve();
- })
- .catch((err: BusinessError) => {
- console.error(UtilName, "testTag", `${host} MKCOL失败:错误码 ${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<void> {
- return new Promise<void>((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<FileInfo[]> {
- return new Promise(async (resolve, reject) => {
- const url = this.buildRequestUrl(host, port, path, enableHttps);
- const timeoutDuration: number = 10000;
- // 创建 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 = `<?xml version="1.0" encoding="UTF-8"?>
- <D:propfind xmlns:D="DAV:">
- <D:prop>
- <D:displayname/> <!-- 请求文件名 -->
- <D:getcontentlength/> <!-- 请求文件大小 -->
- <D:getlastmodified/> <!-- 请求文件最后修改时间 -->
- </D:prop>
- </D:propfind>`;
- // 构造基本认证头部
- 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(() => {
- if (response != '') {
- // 提取文件信息
- const filesInfo = this.extractHrefContents(response, path, url);
- if (filesInfo.length !== 0) {
- 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<FileInfo[]> {
- 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 = `<?xml version="1.0" encoding="UTF-8"?>
- <D:propfind xmlns:D="DAV:">
- <D:prop>
- <D:displayname/> <!-- 请求文件名 -->
- <D:getcontentlength/> <!-- 请求文件大小 -->
- <D:getlastmodified/> <!-- 请求文件最后修改时间 -->
- </D:prop>
- </D:propfind>`;
- // const requestBody = `<D:propfind xmlns:D="DAV:">
- // <D:allprop/>
- // </D:propfind>`;
- // 构造基本认证头部
- 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<FileInfo[]> => {
- return new Promise<FileInfo[]>(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('<?xml');
- if (xmlStart !== -1) {
- return httpResponse.substring(xmlStart);
- }
- return '';
- }
- stringToNumber(str: string): number {
- let result: number = 0;
- for (let i = 0; i < str.length; i++) {
- result += str.charCodeAt(i);
- }
- return result;
- }
- // 使用正则表达式提取所有 <D:href> 标签的内容
- extractHrefContents(xmlContent: string, rootpath: string, url: string): FileInfo[] {
- const filesInfo: FileInfo[] = [];
- // 匹配每个 <D:response>
- const responseRegex = /<D:response\b[^>]*>([\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];
- // 提取 <D:href> 内容
- const hrefMatch = responseBlock.match(/<D:href>(.*?)<\/D:href>/i);
- if (!hrefMatch) {
- continue;
- }
- // 获取完整的href路径
- const fullHref = this.decodeXMLEntities(decodeURI(hrefMatch[1]));
- // 尝试提取 <D:displayname> 作为文件名
- let displayNameMatch = responseBlock.match(/<D:displayname>(.*?)<\/D:displayname>/i);
- if (!displayNameMatch) {
- displayNameMatch = responseBlock.match(/<lp1:displayname>(.*?)<\/lp1:displayname>/i);
- }
- let name = '';
- if (displayNameMatch && displayNameMatch[1]) {
- // 如果有 displayname,使用它
- name = this.decodeXMLEntities(displayNameMatch[1]);
- } 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;
- }
- // 提取<lp1:getcontentlength> 或 <D:getcontentlength>
- let sizeMatch = responseBlock.match(/<lp1:getcontentlength>(.*?)<\/lp1:getcontentlength>/i);
- if (!sizeMatch) {
- sizeMatch = responseBlock.match(/<D:getcontentlength>(.*?)<\/D:getcontentlength>/i);
- }
- // 提取<D:getlastmodified> 内容
- let lastModifiedMatch = responseBlock.match(/<D:getlastmodified>(.*?)<\/D:getlastmodified>/i);
- if (!lastModifiedMatch) {
- lastModifiedMatch = responseBlock.match(/<lp1:getlastmodified>(.*?)<\/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<FileInfo[]> {
- 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<string, string> = 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<void> {
- 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<void> {
- return new Promise<void>(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 = {
- onUploadProgress: async (total: number, uploaded: number) => {
- // 上传进度监控回调
- uploadedSize = uploaded;
- progressCallCount++;
- const currentTime = Date.now();
- const timeSinceLastUpdate = currentTime - lastProgressTime;
- // 更严格的上传完成判断:需要实际传输数据大于文件大小且回调次数足够多
- const isActuallyTransferring = progressCallCount > 5; // 至少要有几次回调
- const isComplete = isActuallyTransferring && uploadedSize >= fileSize && uploadedSize >= total;
- // 节流策略:
- // 1. 时间间隔超过阈值
- // 2. 确实上传完成(需要多次回调验证)
- // 3. 每50次回调强制更新一次(防止长时间无更新)
- const shouldUpdate = timeSinceLastUpdate >= progressThrottle ||
- (isActuallyTransferring && isComplete) ||
- (progressCallCount % 50 === 0);
- if (onProgress && shouldUpdate) {
- // 对于大文件,更加保守地显示进度
- let displayProgress = uploadedSize;
- let progressPercentage = Math.floor((uploadedSize / fileSize) * 100);
- // 如果进度超过99%但还没有完成,保守显示99%
- if (progressPercentage >= 99 && !isComplete) {
- progressPercentage = 99;
- displayProgress = Math.min(uploadedSize, fileSize - 1);
- }
- console.info(UtilName, 'testTag', `上传进度更新: ${progressPercentage}% (${displayProgress}/${fileSize} 字节), 回调次数: ${progressCallCount}`);
- onProgress(displayProgress, fileSize);
- lastProgressTime = currentTime;
- }
- // 后台任务更新也进行节流
- if (timeSinceLastUpdate >= 1000) {
- await this.backgroundManager.updateDataTransferContinuousTask();
- }
- },
- onDataReceive: async (incomingData: ArrayBuffer) => {
- // 接收服务器响应(这里主要用于监控上传完成后的响应)
- console.info(UtilName, 'testTag', `接收到响应数据: ${incomingData.byteLength} 字节`);
- },
- onDataEnd: () => {
- console.info(UtilName, 'testTag', '文件数据传输完成,回调次数:', progressCallCount);
- // 确保最后一次进度更新设置为100%
- if (onProgress) {
- console.info(UtilName, 'testTag', `最终上传完成: ${fileSize} 字节,设置为100%`);
- 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<ArrayBuffer> {
- 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<void> {
- return new Promise<void>((resolve) => {
- setTimeout(() => {
- resolve();
- }, ms);
- });
- }
- }
|