| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096 |
- // 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 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;
- 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 = `<?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(() => {
- 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<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]);
- 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;
- }
- // 提取<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 = {
- 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<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);
- });
- }
- }
|