RcpSocketUtil.ets 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  1. // rcp通信工具
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { buffer, HashMap, JSON, util, xml } from '@kit.ArkTS';
  4. import { FileInfo } from '../../viewmodel/FileInfo';
  5. import { rcp } from '@kit.RemoteCommunicationKit';
  6. import { BackgroundManager } from './BackgroundManager';
  7. import FileManager, { merge2paths } from './FileManager';
  8. const UtilName = "heanup RcpSocket"
  9. export class RcpSocket {
  10. private static instance: RcpSocket;
  11. public ErrorMessage: string | BusinessError = ''
  12. public filesInfo: FileInfo[] = []
  13. private backgroundManager = BackgroundManager.getInstance()
  14. //private rcpSession : rcp.Session | null = null
  15. constructor() {
  16. console.info(UtilName, 'testTag', 'RcpSocketUtil单例已创建')
  17. }
  18. private encodeUrlPath(path?: string): string {
  19. if (!path || path.length === 0) {
  20. return '/';
  21. }
  22. let normalized = path.replace(/\\/g, '/');
  23. if (!normalized.startsWith('/')) {
  24. normalized = `/${normalized}`;
  25. }
  26. normalized = normalized.replace(/\/+/g, '/');
  27. const segments = normalized.split('/').map((segment) => {
  28. if (!segment || segment.length === 0) {
  29. return '';
  30. }
  31. let decoded = segment;
  32. try {
  33. decoded = decodeURIComponent(segment);
  34. } catch (_err) {
  35. // ignore decode errors and keep raw segment
  36. }
  37. return encodeURIComponent(decoded);
  38. });
  39. let encodedPath = segments.join('/');
  40. if (!encodedPath.startsWith('/')) {
  41. encodedPath = `/${encodedPath}`;
  42. }
  43. if (encodedPath.length === 0) {
  44. encodedPath = '/';
  45. }
  46. return encodedPath;
  47. }
  48. private buildRequestUrl(host: string, port: number, path: string, enableHttps: boolean): string {
  49. const protocol = enableHttps ? "https" : "http";
  50. const encodedPath = this.encodeUrlPath(path);
  51. return `${protocol}://${host}:${port}${encodedPath}`;
  52. }
  53. static getInstance(): RcpSocket {
  54. if (!RcpSocket.instance) {
  55. RcpSocket.instance = new RcpSocket();
  56. }
  57. return RcpSocket.instance;
  58. }
  59. public RcpSendHead(host: string, port: number, account: string, password: string, path: string,
  60. enableHttps: boolean): Promise<number> {
  61. return new Promise<number>((resolve, reject) => {
  62. const url = this.buildRequestUrl(host, port, path, enableHttps);
  63. const timeoutDuration: number = 10000;
  64. const speedThreshold: number = 5000; // 设置速度测试的时间阈值
  65. console.info(UtilName, 'testTag', '发送HEAD的url:' + url)
  66. // 创建 RCP 会话配置
  67. let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
  68. let reqCfg: rcp.Configuration = {
  69. security: secCfg,
  70. transfer: {
  71. timeout: {
  72. connectMs: timeoutDuration
  73. }
  74. }
  75. }
  76. let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
  77. let rcpSession = rcp.createSession(sessionCfg);
  78. // 构造基本认证头部
  79. const encodedCredentials = buffer
  80. .from(`${account}:${password}`)
  81. .toString("base64");
  82. const headers: rcp.RequestHeaders = {
  83. Depth: "1",
  84. "Content-Type": "application/xml",
  85. Accept: "text/xml",
  86. Authorization: `Basic ${encodedCredentials}`,
  87. };
  88. // 创建请求对象
  89. const req = new rcp.Request(url, "HEAD", headers);
  90. const startTime = Date.now();
  91. // 连接
  92. try {
  93. rcpSession
  94. .fetch(req)
  95. .then((response) => {
  96. const endTime = Date.now();
  97. const elapsedTime = endTime - startTime;
  98. // 如果响应时间超过阈值,视为测速过慢
  99. if (elapsedTime > speedThreshold) {
  100. console.error(UtilName, "testTag", `${host}Response time too slow: ${elapsedTime}ms`);
  101. rcpSession?.close()
  102. reject("Test speed too slow");
  103. } else {
  104. console.info(UtilName, "testTag", host + " Connect and test speed succeed");
  105. const contentLength = response.headers['content-length'] || '0'
  106. let fileSize = 0
  107. if (contentLength) {
  108. if (Array.isArray(contentLength)) {
  109. const values = contentLength
  110. .map((value) => parseInt(value, 10))
  111. .filter((value) => !isNaN(value));
  112. if (values.length > 0) {
  113. // 取最大值
  114. fileSize = Math.max(...values);
  115. }
  116. } else {
  117. fileSize = parseInt(contentLength, 10);
  118. }
  119. } else {
  120. console.info(UtilName, 'testTag', 'Content-Length 头未找到');
  121. }
  122. rcpSession?.close();
  123. resolve(fileSize);
  124. }
  125. })
  126. .catch((err: BusinessError) => {
  127. console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
  128. rcpSession?.close()
  129. reject(err);
  130. });
  131. } catch (e) {
  132. console.error(UtilName, 'testTag', '发起连接失败', JSON.stringify(e))
  133. reject(e)
  134. }
  135. });
  136. }
  137. public RcpSendDelete(host: string, port: number, account: string, password: string, path: string,
  138. enableHttps: boolean): Promise<void> {
  139. return new Promise<void>((resolve, reject) => {
  140. const url = this.buildRequestUrl(host, port, path, enableHttps);
  141. const timeoutDuration: number = 10000;
  142. console.info(UtilName, 'testTag', '发送Delete的url:' + url)
  143. // 创建 RCP 会话配置
  144. let response = ""
  145. const customHttpEventsHandler: rcp.HttpEventsHandler = {
  146. onDataReceive: (incomingData: ArrayBuffer) => {
  147. response += this.buf2String(incomingData)
  148. },
  149. onDataEnd: () => {
  150. },
  151. };
  152. const tracingConfig: rcp.TracingConfiguration = {
  153. verbose: true,
  154. infoToCollect: {
  155. textual: true,
  156. incomingData: true,
  157. outgoingData: true,
  158. },
  159. collectTimeInfo: true,
  160. httpEventsHandler: customHttpEventsHandler
  161. };
  162. let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
  163. let reqCfg: rcp.Configuration = {
  164. security: secCfg,
  165. tracing: tracingConfig,
  166. transfer: {
  167. timeout: {
  168. connectMs: timeoutDuration
  169. }
  170. }
  171. }
  172. let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
  173. let rcpSession = rcp.createSession(sessionCfg);
  174. // 构造基本认证头部
  175. const encodedCredentials = buffer
  176. .from(`${account}:${password}`)
  177. .toString("base64");
  178. const headers: rcp.RequestHeaders = {
  179. Authorization: `Basic ${encodedCredentials}`,
  180. };
  181. // 创建请求对象
  182. const req = new rcp.Request(url, "DELETE", headers);
  183. // 连接
  184. rcpSession
  185. .fetch(req)
  186. .then(() => {
  187. console.info(UtilName, 'testTag', '执行删除的响应', JSON.stringify(response))
  188. rcpSession.close()
  189. resolve()
  190. })
  191. .catch((err: BusinessError) => {
  192. console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
  193. rcpSession?.close()
  194. reject(err);
  195. });
  196. });
  197. }
  198. public RcpSendMove(host: string, port: number, account: string, password: string, path: string, enableHttps: boolean,
  199. newPath: string): Promise<void> {
  200. return new Promise<void>((resolve, reject) => {
  201. const url = this.buildRequestUrl(host, port, path, enableHttps);
  202. const destinationUrl = this.buildRequestUrl(host, port, newPath, enableHttps)
  203. const timeoutDuration: number = 10000;
  204. console.info(UtilName, 'testTag', '发送MOVE的url:' + url)
  205. // 创建 RCP 会话配置
  206. let response = ""
  207. const customHttpEventsHandler: rcp.HttpEventsHandler = {
  208. onDataReceive: (incomingData: ArrayBuffer) => {
  209. response += this.buf2String(incomingData)
  210. },
  211. onDataEnd: () => {
  212. },
  213. };
  214. const tracingConfig: rcp.TracingConfiguration = {
  215. verbose: true,
  216. infoToCollect: {
  217. textual: true,
  218. incomingData: true,
  219. outgoingData: true,
  220. },
  221. collectTimeInfo: true,
  222. httpEventsHandler: customHttpEventsHandler
  223. };
  224. let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
  225. let reqCfg: rcp.Configuration = {
  226. security: secCfg,
  227. tracing: tracingConfig,
  228. transfer: {
  229. timeout: {
  230. connectMs: timeoutDuration
  231. }
  232. }
  233. }
  234. let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
  235. let rcpSession = rcp.createSession(sessionCfg);
  236. // 构造基本认证头部
  237. const encodedCredentials = buffer
  238. .from(`${account}:${password}`)
  239. .toString("base64");
  240. const headers: rcp.RequestHeaders = {
  241. Authorization: `Basic ${encodedCredentials}`,
  242. Destination: destinationUrl
  243. };
  244. // 创建请求对象
  245. const req = new rcp.Request(url, "MOVE", headers);
  246. // 连接
  247. rcpSession
  248. .fetch(req)
  249. .then(() => {
  250. console.info(UtilName, 'testTag', 'move的响应结果', JSON.stringify(response))
  251. rcpSession.close()
  252. resolve()
  253. })
  254. .catch((err: BusinessError) => {
  255. console.error(UtilName, "testTag", `${host}请求失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
  256. rcpSession.close()
  257. reject(err);
  258. });
  259. });
  260. }
  261. // rcp方法
  262. public async RcpSendPropFind(
  263. host: string,
  264. port: number,
  265. account: string,
  266. password: string,
  267. path: string,
  268. enableHttps: boolean
  269. ): Promise<FileInfo[]> {
  270. return new Promise(async (resolve, reject) => {
  271. const url = this.buildRequestUrl(host, port, path, enableHttps);
  272. const timeoutDuration: number = 10000;
  273. console.info(UtilName, 'testTag', '发送PROPFIND请求的url:' + url)
  274. // 创建 RCP 会话配置
  275. let response: string = ''
  276. const customHttpEventsHandler: rcp.HttpEventsHandler = {
  277. onDataReceive: async (incomingData: ArrayBuffer) => {
  278. response += this.buf2String(incomingData)
  279. await this.backgroundManager.updateDataTransferContinuousTask()
  280. },
  281. onDataEnd: () => {
  282. },
  283. };
  284. const tracingConfig: rcp.TracingConfiguration = {
  285. verbose: true,
  286. infoToCollect: {
  287. textual: true,
  288. incomingData: true,
  289. outgoingData: true,
  290. },
  291. collectTimeInfo: true,
  292. httpEventsHandler: customHttpEventsHandler
  293. };
  294. let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
  295. let reqCfg: rcp.Configuration = {}
  296. if (enableHttps) {
  297. reqCfg = {
  298. security: secCfg,
  299. tracing: tracingConfig,
  300. transfer: {
  301. timeout: {
  302. connectMs: timeoutDuration,
  303. transferMs: timeoutDuration
  304. }
  305. }
  306. }
  307. } else {
  308. reqCfg = {
  309. tracing: tracingConfig,
  310. transfer: {
  311. timeout: {
  312. connectMs: timeoutDuration
  313. }
  314. }
  315. }
  316. }
  317. let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
  318. let rcpSession = rcp.createSession(sessionCfg);
  319. // 构造 PROPFIND 请求体
  320. const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
  321. <D:propfind xmlns:D="DAV:">
  322. <D:prop>
  323. <D:displayname/> <!-- 请求文件名 -->
  324. <D:getcontentlength/> <!-- 请求文件大小 -->
  325. <D:getlastmodified/> <!-- 请求文件最后修改时间 -->
  326. </D:prop>
  327. </D:propfind>`;
  328. // 构造基本认证头部
  329. const encodedCredentials = buffer
  330. .from(`${account}:${password}`)
  331. .toString("base64");
  332. const headers: rcp.RequestHeaders = {
  333. "Depth": "1",
  334. "Content-Type": "application/xml",
  335. "Accept": "text/xml",
  336. "Authorization": `Basic ${encodedCredentials}`,
  337. };
  338. // 创建请求对象
  339. const req = new rcp.Request(url, "PROPFIND", headers, requestBody);
  340. // 发起请求
  341. try {
  342. await rcpSession.fetch(req)
  343. .finally(() => {
  344. console.info(UtilName, 'testTag', 'PROPFIND执行完毕')
  345. if (response != '') {
  346. console.info(UtilName, 'testTag', 'WebDAV响应内容长度:', response.length.toString());
  347. console.info(UtilName, 'testTag', 'WebDAV响应前500字符:', response.substring(0, 500));
  348. // 提取文件信息
  349. const filesInfo = this.extractHrefContents(response, path, url);
  350. console.info(UtilName, 'testTag', '解析出文件数量:', filesInfo.length.toString());
  351. if (filesInfo.length !== 0) {
  352. console.info(UtilName, 'testTag', '请求成功')
  353. rcpSession.close()
  354. resolve(filesInfo);
  355. } else {
  356. let message = `请求${host}失败,响应信息:${response}`
  357. console.error(UtilName, 'testTag', message)
  358. rcpSession.close()
  359. reject(message)
  360. }
  361. } else {
  362. let error = `服务器响应信息为空,请求失败`
  363. console.error(UtilName, 'testTag', error)
  364. rcpSession.close()
  365. reject(error);
  366. }
  367. })
  368. // 处理成功响应
  369. //console.info(UtilName, 'testTag', JSON.stringify(res));
  370. } catch (err) {
  371. // 处理错误响应
  372. console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
  373. let error = `错误码${err.code},错误信息${err.data}`
  374. rcpSession.close()
  375. reject(error)
  376. }
  377. });
  378. }
  379. // PROPFIND递归方法
  380. public async RcpSendPropFindInfinity(
  381. host: string,
  382. port: number,
  383. account: string,
  384. password: string,
  385. path: string,
  386. enableHttps: boolean,
  387. useCache: boolean,
  388. saveCache: boolean,
  389. cachePath: string
  390. ): Promise<FileInfo[]> {
  391. return new Promise(async (resolve, reject) => {
  392. const url = this.buildRequestUrl(host, port, path, enableHttps);
  393. const timeoutDuration: number = 10000;
  394. console.info(UtilName, 'testTag', '发送PROPFIND递归请求的url:' + url)
  395. let cacheFileInfos_str: string = ''
  396. if (useCache) {
  397. try {
  398. cacheFileInfos_str = await FileManager.readFileToString(cachePath)
  399. if (cacheFileInfos_str) {
  400. let files = JSON.parse(cacheFileInfos_str) as FileInfo[]
  401. console.info(UtilName, 'testTag', '使用缓存')
  402. resolve(files)
  403. return
  404. }
  405. } catch (e) {
  406. console.error(UtilName, 'testTag', '读取缓存失败', JSON.stringify(e))
  407. }
  408. }
  409. // 创建 RCP 会话配置
  410. let response: string = ''
  411. const customHttpEventsHandler: rcp.HttpEventsHandler = {
  412. onDataReceive: async (incomingData: ArrayBuffer) => {
  413. response += this.buf2String(incomingData)
  414. //await this.backgroundManager.updateDataTransferContinuousTask(0,0,path)
  415. },
  416. onDataEnd: () => {
  417. },
  418. };
  419. const tracingConfig: rcp.TracingConfiguration = {
  420. verbose: true,
  421. infoToCollect: {
  422. textual: true,
  423. incomingData: true,
  424. outgoingData: true,
  425. },
  426. collectTimeInfo: true,
  427. httpEventsHandler: customHttpEventsHandler
  428. };
  429. let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' }
  430. let reqCfg: rcp.Configuration = {}
  431. if (enableHttps) {
  432. reqCfg = {
  433. security: secCfg,
  434. tracing: tracingConfig,
  435. transfer: {
  436. timeout: {
  437. connectMs: timeoutDuration,
  438. transferMs: timeoutDuration
  439. }
  440. }
  441. }
  442. } else {
  443. reqCfg = {
  444. tracing: tracingConfig,
  445. transfer: {
  446. timeout: {
  447. connectMs: timeoutDuration
  448. }
  449. }
  450. }
  451. }
  452. let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg }
  453. let rcpSession = rcp.createSession(sessionCfg);
  454. // 构造 PROPFIND 请求体
  455. const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
  456. <D:propfind xmlns:D="DAV:">
  457. <D:prop>
  458. <D:displayname/> <!-- 请求文件名 -->
  459. <D:getcontentlength/> <!-- 请求文件大小 -->
  460. <D:getlastmodified/> <!-- 请求文件最后修改时间 -->
  461. </D:prop>
  462. </D:propfind>`;
  463. // const requestBody = `<D:propfind xmlns:D="DAV:">
  464. // <D:allprop/>
  465. // </D:propfind>`;
  466. // 构造基本认证头部
  467. const encodedCredentials = buffer
  468. .from(`${account}:${password}`)
  469. .toString("base64");
  470. const headers: rcp.RequestHeaders = {
  471. "Depth": "1",
  472. "Content-Type": "application/xml",
  473. "Accept": "text/xml",
  474. "Authorization": `Basic ${encodedCredentials}`,
  475. };
  476. let sendSinglePropfind = async (url: string, root: string): Promise<FileInfo[]> => {
  477. return new Promise<FileInfo[]>(async (resolve, reject) => {
  478. if (root.includes('%23recycle')) {
  479. resolve([])
  480. return
  481. }
  482. // 发起请求
  483. try {
  484. AppStorage.setOrCreate('CurrentPropfindInfinityRoot', root)
  485. response = ''
  486. const req = new rcp.Request(url, "PROPFIND", headers, requestBody)
  487. await rcpSession.fetch(req)
  488. .finally(async () => {
  489. if (response != '') {
  490. // 提取文件信息
  491. let filesInfo = this.extractHrefContents(response, root, url);
  492. let folderInfos: FileInfo[] = []
  493. for (const info of filesInfo) {
  494. if (this.isFileFolder(info.name)) {
  495. folderInfos.push(info)
  496. }
  497. }
  498. //console.info(UtilName,'testTag','PROPFIND执行完毕','根目录',root,'子目录数量',folderInfos.length)
  499. if (folderInfos.length > 0) {
  500. for (const folder of folderInfos) {
  501. let sub_url = merge2paths(url, folder.name)
  502. let sub_root = merge2paths(root, folder.name)
  503. let next_infos = await sendSinglePropfind(sub_url, sub_root)
  504. filesInfo = filesInfo.concat(next_infos)
  505. }
  506. }
  507. if (filesInfo.length !== 0) {
  508. resolve(filesInfo);
  509. } else {
  510. resolve([])
  511. }
  512. } else {
  513. resolve([])
  514. }
  515. })
  516. } catch (err) {
  517. // 处理错误响应
  518. console.error(UtilName, "testTag", `请求${url}失败:错误码 ${err.code},错误信息:${JSON.stringify(err)}`);
  519. let error = `错误码${err.code},错误信息${err.data}`
  520. resolve([])
  521. }
  522. })
  523. }
  524. // 创建请求对象
  525. await sendSinglePropfind(url, path)
  526. .then(async (filesInfo: FileInfo[]) => {
  527. console.info(UtilName, 'testTag', 'PROPFIND目录递归结果', filesInfo.length)
  528. rcpSession?.close()
  529. if (saveCache) {
  530. let str = JSON.stringify(filesInfo)
  531. if (str !== cacheFileInfos_str) {
  532. try {
  533. await FileManager.writeStringToFilePath(str, cachePath)
  534. console.info(UtilName, 'testTag', '保存缓存成功')
  535. } catch (e) {
  536. console.info(UtilName, 'testTag', '保存缓存失败', JSON.stringify(e))
  537. }
  538. }
  539. }
  540. resolve(filesInfo)
  541. }).catch((err: BusinessError) => {
  542. rcpSession?.close()
  543. console.error(UtilName, 'testTag', 'PROPFIND目录递归失败', JSON.stringify(err))
  544. reject(err)
  545. })
  546. });
  547. }
  548. //ArrayBuffer转utf8字符串
  549. buf2String(buf: ArrayBuffer) {
  550. let msgArray = new Uint8Array(buf);
  551. let textDecoder = util.TextDecoder.create("utf-8");
  552. return textDecoder.decodeToString(msgArray)
  553. }
  554. // 提取XML
  555. extractXmlContent(httpResponse: string): string {
  556. const xmlStart = httpResponse.indexOf('<?xml');
  557. if (xmlStart !== -1) {
  558. return httpResponse.substring(xmlStart);
  559. }
  560. return '';
  561. }
  562. stringToNumber(str: string): number {
  563. let result: number = 0;
  564. for (let i = 0; i < str.length; i++) {
  565. result += str.charCodeAt(i);
  566. }
  567. return result;
  568. }
  569. // 使用正则表达式提取所有 <D:href> 标签的内容
  570. extractHrefContents(xmlContent: string, rootpath: string, url: string): FileInfo[] {
  571. const filesInfo: FileInfo[] = [];
  572. // 匹配每个 <D:response>
  573. const responseRegex = /<D:response\b[^>]*>([\s\S]*?)<\/D:response>/gi;
  574. let responseMatch: RegExpExecArray | null;
  575. while ((responseMatch = responseRegex.exec(xmlContent)) !== null) {
  576. //console.info(UtilName,'testTag',JSON.stringify(responseMatch))
  577. const responseBlock = responseMatch[1];
  578. // 提取 <D:href> 内容
  579. const hrefMatch = responseBlock.match(/<D:href>(.*?)<\/D:href>/i);
  580. if (!hrefMatch) {
  581. continue;
  582. }
  583. // 获取完整的href路径
  584. const fullHref = this.decodeXMLEntities(decodeURI(hrefMatch[1]));
  585. // 尝试提取 <D:displayname> 作为文件名
  586. let displayNameMatch = responseBlock.match(/<D:displayname>(.*?)<\/D:displayname>/i);
  587. if (!displayNameMatch) {
  588. displayNameMatch = responseBlock.match(/<lp1:displayname>(.*?)<\/lp1:displayname>/i);
  589. }
  590. let name = '';
  591. if (displayNameMatch && displayNameMatch[1]) {
  592. // 如果有 displayname,使用它
  593. name = this.decodeXMLEntities(displayNameMatch[1]);
  594. console.info(UtilName, 'testTag', '使用displayname作为文件名:', name);
  595. } else {
  596. // 否则从href中提取文件名(最后一个/后的部分)
  597. name = fullHref;
  598. const lastSlashIndex = fullHref.lastIndexOf('/');
  599. if (lastSlashIndex >= 0 && lastSlashIndex < fullHref.length - 1) {
  600. name = fullHref.substring(lastSlashIndex + 1);
  601. } else if (fullHref.endsWith('/')) {
  602. // 如果是目录(以/结尾),取倒数第二段
  603. const withoutTrailingSlash = fullHref.substring(0, fullHref.length - 1);
  604. const secondLastSlash = withoutTrailingSlash.lastIndexOf('/');
  605. if (secondLastSlash >= 0) {
  606. name = withoutTrailingSlash.substring(secondLastSlash + 1) + '/';
  607. }
  608. }
  609. }
  610. // 跳过根目录本身
  611. if (name === '' || name === '/') {
  612. continue;
  613. }
  614. // 提取<lp1:getcontentlength> 或 <D:getcontentlength>
  615. let sizeMatch = responseBlock.match(/<lp1:getcontentlength>(.*?)<\/lp1:getcontentlength>/i);
  616. if (!sizeMatch) {
  617. sizeMatch = responseBlock.match(/<D:getcontentlength>(.*?)<\/D:getcontentlength>/i);
  618. }
  619. // 提取<D:getlastmodified> 内容
  620. let lastModifiedMatch = responseBlock.match(/<D:getlastmodified>(.*?)<\/D:getlastmodified>/i);
  621. if (!lastModifiedMatch) {
  622. lastModifiedMatch = responseBlock.match(/<lp1:getlastmodified>(.*?)<\/lp1:getlastmodified>/i);
  623. }
  624. const lastModified = lastModifiedMatch ? this.convertToUnixTimestamp(lastModifiedMatch[1]) : 0;
  625. const size = sizeMatch ? Number(sizeMatch[1]) : 0;
  626. // 创建FileInfo并设置WebDAV属性
  627. const fileInfo = new FileInfo(rootpath, name, size, lastModified);
  628. fileInfo.href = fullHref;
  629. fileInfo.contentLength = size;
  630. // 判断是否为文件夹(以/结尾或没有contentLength)
  631. fileInfo.isDirectory = fullHref.endsWith('/') || size === 0;
  632. filesInfo.push(fileInfo);
  633. }
  634. return filesInfo;
  635. }
  636. /**
  637. * 获取文件列表
  638. * @param host 主机名
  639. * @param localHost 本地主机名
  640. * @param isUseLocalHost 是否使用本地主机名
  641. * @param port 端口号
  642. * @param path 路径
  643. * @param account 用户名
  644. * @param password 密码
  645. * @param enableHttps 是否启用HTTPS
  646. * @returns
  647. */
  648. public async getFileList(
  649. host: string,
  650. localHost: string,
  651. isUseLocalHost: boolean,
  652. port: number,
  653. path: string,
  654. account: string,
  655. password: string,
  656. enableHttps: boolean
  657. ): Promise<FileInfo[]> {
  658. const actualHost = isUseLocalHost ? localHost : host;
  659. return await this.RcpSendPropFind(actualHost, port, account, password, path, enableHttps);
  660. }
  661. // 订阅HTTP数据传输事件
  662. public subscribeHTTPDataTransfer(callback: (event: string) => void): void {
  663. // 简化版:目前不实现具体订阅逻辑
  664. console.info(UtilName, 'testTag', '订阅HTTP数据传输事件(占位)');
  665. }
  666. // 获取文件列表(简化版包装方法)
  667. // 判断文件是否属于文件夹
  668. /**
  669. * 判断文件是否属于文件夹
  670. * @param filename 文件名
  671. * @returns
  672. */
  673. private isFileFolder(filename: string): boolean {
  674. return filename.toLowerCase().endsWith('/')
  675. }
  676. // 将 HTTP 日期字符串转换为 Unix 时间戳
  677. private convertToUnixTimestamp(dateString: string): number {
  678. const date = new Date(dateString);
  679. if (isNaN(date.getTime())) {
  680. console.error(UtilName, 'testTag', "非法日期字符串:", dateString);
  681. return 0;
  682. }
  683. return Math.floor(date.getTime() / 1000);
  684. }
  685. private decodeXMLEntities(str: string): string {
  686. const entityMap: HashMap<string, string> = new HashMap()
  687. entityMap.set('&amp;', '&')
  688. entityMap.set('&lt;', '<')
  689. entityMap.set('&gt;', '>')
  690. entityMap.set('&quot;', '"')
  691. entityMap.set('&apos;', "'")
  692. // 先替换 XML 实体
  693. let result = str.replace(/&(amp|lt|gt|quot|apos);/g, (match: string, entity: string) => {
  694. const decoded = entityMap.get(`&${entity};`);
  695. return decoded ? decoded : match;
  696. });
  697. // 再进行 URL 解码
  698. try {
  699. result = decodeURIComponent(result);
  700. } catch (error) {
  701. // 如果解码失败,保持原样
  702. }
  703. return result;
  704. }
  705. /**
  706. * 上传文件到WebDAV服务器
  707. * @param localPath 本地文件路径
  708. * @param remotePath 远程文件路径
  709. * @param host 主机地址
  710. * @param port 端口
  711. * @param account 账户名
  712. * @param password 密码
  713. * @param enableHttps 是否启用HTTPS
  714. * @param onProgress 进度回调
  715. * @param maxRetries 最大重试次数,默认3次
  716. */
  717. public async uploadFile(
  718. localPath: string,
  719. remotePath: string,
  720. host: string,
  721. port: number,
  722. account: string,
  723. password: string,
  724. enableHttps: boolean,
  725. onProgress?: (uploaded: number, total: number) => void,
  726. maxRetries: number = 3
  727. ): Promise<void> {
  728. let retryCount = 0;
  729. let lastError: BusinessError | null = null;
  730. // 详细日志:上传开始
  731. console.info(UtilName, 'testTag', '========== RcpSocket上传开始 ==========');
  732. console.info(UtilName, 'testTag', `本地路径: ${localPath}`);
  733. console.info(UtilName, 'testTag', `远程路径: ${remotePath}`);
  734. console.info(UtilName, 'testTag', `目标主机: ${host}:${port}`);
  735. console.info(UtilName, 'testTag', `使用HTTPS: ${enableHttps}`);
  736. console.info(UtilName, 'testTag', `最大重试次数: ${maxRetries}`);
  737. while (retryCount <= maxRetries) {
  738. try {
  739. if (retryCount > 0) {
  740. console.info(UtilName, 'testTag', `第${retryCount}次重试上传...`);
  741. }
  742. await this.uploadFileInternal(
  743. localPath,
  744. remotePath,
  745. host,
  746. port,
  747. account,
  748. password,
  749. enableHttps,
  750. onProgress
  751. );
  752. console.info(UtilName, 'testTag', '========== RcpSocket上传成功 ==========');
  753. console.info(UtilName, 'testTag', `文件: ${remotePath}`);
  754. console.info(UtilName, 'testTag', `重试次数: ${retryCount}`);
  755. console.info(UtilName, 'testTag', '==========================================');
  756. return;
  757. } catch (err) {
  758. lastError = err as BusinessError;
  759. retryCount++;
  760. // 详细错误日志
  761. console.error(UtilName, 'testTag', '---------- 上传失败 ----------');
  762. console.error(UtilName, 'testTag', `文件: ${remotePath}`);
  763. console.error(UtilName, 'testTag', `错误码: ${lastError.code}`);
  764. console.error(UtilName, 'testTag', `错误信息: ${lastError.message}`);
  765. console.error(UtilName, 'testTag', `当前重试次数: ${retryCount}/${maxRetries}`);
  766. if (retryCount <= maxRetries) {
  767. const delayMs = Math.min(1000 * Math.pow(2, retryCount - 1), 10000);
  768. console.warn(UtilName, 'testTag', `将在${delayMs}ms后重试...`);
  769. // 等待一段时间后重试,使用指数退避策略
  770. await this.delay(delayMs);
  771. } else {
  772. console.error(UtilName, 'testTag', '已达到最大重试次数,放弃上传');
  773. }
  774. }
  775. }
  776. // 所有重试都失败
  777. console.error(UtilName, 'testTag', '========== RcpSocket上传失败 ==========');
  778. console.error(UtilName, 'testTag', `文件: ${remotePath}`);
  779. console.error(UtilName, 'testTag', `已重试: ${maxRetries}次`);
  780. console.error(UtilName, 'testTag', `最终错误: ${lastError?.message || '未知错误'}`);
  781. console.error(UtilName, 'testTag', '==========================================');
  782. if (lastError) {
  783. const error = new Error(lastError.message);
  784. throw error;
  785. }
  786. }
  787. /**
  788. * 内部上传文件实现
  789. */
  790. private async uploadFileInternal(
  791. localPath: string,
  792. remotePath: string,
  793. host: string,
  794. port: number,
  795. account: string,
  796. password: string,
  797. enableHttps: boolean,
  798. onProgress?: (uploaded: number, total: number) => void
  799. ): Promise<void> {
  800. return new Promise<void>(async (resolve, reject) => {
  801. const url = this.buildRequestUrl(host, port, remotePath, enableHttps);
  802. const timeoutDuration: number = 120000; // 上传超时时间设置为120秒
  803. console.info(UtilName, 'testTag', '开始上传文件到:', url);
  804. let rcpSession: rcp.Session | null = null;
  805. try {
  806. // 检查文件是否存在
  807. console.info(UtilName, 'testTag', '检查本地文件是否存在...');
  808. const fileExists = await FileManager.isExist(localPath);
  809. if (!fileExists) {
  810. const errorMsg = `本地文件不存在: ${localPath}`;
  811. console.error(UtilName, 'testTag', `文件读取错误: ${errorMsg}`);
  812. const error = new Error(errorMsg);
  813. throw error;
  814. }
  815. console.info(UtilName, 'testTag', '本地文件存在,继续上传');
  816. // 获取文件大小
  817. console.info(UtilName, 'testTag', '获取文件大小...');
  818. const fileSize = await FileManager.getFileSize(localPath);
  819. console.info(UtilName, 'testTag', `文件大小: ${fileSize} 字节 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`);
  820. if (fileSize === 0) {
  821. const errorMsg = `文件大小为0: ${localPath}`;
  822. console.error(UtilName, 'testTag', `文件读取错误: ${errorMsg}`);
  823. const error = new Error(errorMsg);
  824. throw error;
  825. }
  826. // 流式读取文件内容
  827. console.info(UtilName, 'testTag', '读取文件内容...');
  828. const fileContent = await this.readFileStream(localPath, fileSize);
  829. console.info(UtilName, 'testTag', '文件内容读取完成');
  830. // 创建 RCP 会话配置
  831. let uploadedSize = 0;
  832. let lastProgressTime = Date.now();
  833. const progressThrottle = 500; // 进度更新节流,每500ms更新一次
  834. let progressCallCount = 0; // 进度回调计数
  835. const customHttpEventsHandler: rcp.HttpEventsHandler = {
  836. onDataReceive: async (incomingData: ArrayBuffer) => {
  837. // 上传响应数据接收
  838. uploadedSize += incomingData.byteLength;
  839. progressCallCount++;
  840. const currentTime = Date.now();
  841. const timeSinceLastUpdate = currentTime - lastProgressTime;
  842. const isComplete = uploadedSize >= fileSize;
  843. // 节流策略:
  844. // 1. 时间间隔超过阈值
  845. // 2. 上传完成
  846. // 3. 每100次回调强制更新一次(防止长时间无更新)
  847. const shouldUpdate = timeSinceLastUpdate >= progressThrottle ||
  848. isComplete ||
  849. (progressCallCount % 100 === 0);
  850. if (onProgress && shouldUpdate) {
  851. onProgress(uploadedSize, fileSize);
  852. lastProgressTime = currentTime;
  853. }
  854. // 后台任务更新也进行节流
  855. if (timeSinceLastUpdate >= 1000) {
  856. await this.backgroundManager.updateDataTransferContinuousTask();
  857. }
  858. },
  859. onDataEnd: () => {
  860. console.info(UtilName, 'testTag', '文件数据传输完成');
  861. // 确保最后一次进度更新
  862. if (onProgress) {
  863. onProgress(fileSize, fileSize);
  864. }
  865. }
  866. };
  867. const tracingConfig: rcp.TracingConfiguration = {
  868. verbose: true,
  869. infoToCollect: {
  870. textual: true,
  871. incomingData: true,
  872. outgoingData: true,
  873. },
  874. collectTimeInfo: true,
  875. httpEventsHandler: customHttpEventsHandler
  876. };
  877. let secCfg: rcp.SecurityConfiguration = { remoteValidation: 'skip' };
  878. let reqCfg: rcp.Configuration = {};
  879. if (enableHttps) {
  880. reqCfg = {
  881. security: secCfg,
  882. tracing: tracingConfig,
  883. transfer: {
  884. timeout: {
  885. connectMs: timeoutDuration,
  886. transferMs: timeoutDuration
  887. }
  888. }
  889. };
  890. } else {
  891. reqCfg = {
  892. tracing: tracingConfig,
  893. transfer: {
  894. timeout: {
  895. connectMs: timeoutDuration,
  896. transferMs: timeoutDuration
  897. }
  898. }
  899. };
  900. }
  901. let sessionCfg: rcp.SessionConfiguration = { requestConfiguration: reqCfg };
  902. rcpSession = rcp.createSession(sessionCfg);
  903. // 构造基本认证头部
  904. const encodedCredentials = buffer
  905. .from(`${account}:${password}`)
  906. .toString("base64");
  907. const headers: rcp.RequestHeaders = {
  908. Authorization: `Basic ${encodedCredentials}`,
  909. 'Content-Type': 'application/octet-stream',
  910. 'Content-Length': fileSize.toString()
  911. };
  912. // 创建PUT请求对象
  913. const req = new rcp.Request(url, "PUT", headers, fileContent);
  914. // 发起上传请求
  915. console.info(UtilName, 'testTag', '发起HTTP PUT请求...');
  916. await rcpSession.fetch(req);
  917. console.info(UtilName, 'testTag', '上传请求完成,关闭会话');
  918. if (rcpSession) {
  919. rcpSession.close();
  920. }
  921. resolve();
  922. } catch (err) {
  923. console.error(UtilName, 'testTag', '上传过程中发生错误');
  924. if (rcpSession) {
  925. console.info(UtilName, 'testTag', '关闭RCP会话');
  926. rcpSession.close();
  927. }
  928. const error = err as BusinessError;
  929. // 详细错误分类和日志
  930. if (error.code) {
  931. const errorCode = error.code.toString();
  932. if (errorCode.includes('2300002') || errorCode.includes('2300003')) {
  933. // 网络连接错误
  934. console.error(UtilName, 'testTag', `网络连接错误: 错误码 ${error.code}`);
  935. console.error(UtilName, 'testTag', '可能原因: 网络不可达、主机不可达或连接超时');
  936. } else if (errorCode.includes('2300008')) {
  937. // DNS解析错误
  938. console.error(UtilName, 'testTag', `DNS解析错误: 错误码 ${error.code}`);
  939. console.error(UtilName, 'testTag', '可能原因: 主机名无法解析');
  940. } else if (errorCode.includes('2300028')) {
  941. // 连接超时
  942. console.error(UtilName, 'testTag', `连接超时: 错误码 ${error.code}`);
  943. console.error(UtilName, 'testTag', '可能原因: 服务器响应缓慢或网络不稳定');
  944. } else if (errorCode.includes('401')) {
  945. // 认证失败
  946. console.error(UtilName, 'testTag', `认证失败: 错误码 ${error.code}`);
  947. console.error(UtilName, 'testTag', '可能原因: 用户名或密码错误');
  948. } else if (errorCode.includes('403')) {
  949. // 权限不足
  950. console.error(UtilName, 'testTag', `权限不足: 错误码 ${error.code}`);
  951. console.error(UtilName, 'testTag', '可能原因: 没有写入权限');
  952. } else if (errorCode.includes('404')) {
  953. // 路径不存在
  954. console.error(UtilName, 'testTag', `路径不存在: 错误码 ${error.code}`);
  955. console.error(UtilName, 'testTag', '可能原因: 目标路径不存在');
  956. } else if (errorCode.includes('500') || errorCode.includes('503')) {
  957. // 服务器错误
  958. console.error(UtilName, 'testTag', `服务器错误: 错误码 ${error.code}`);
  959. console.error(UtilName, 'testTag', '可能原因: 服务器内部错误或服务不可用');
  960. } else if (errorCode.includes('507')) {
  961. // 存储空间不足
  962. console.error(UtilName, 'testTag', `存储空间不足: 错误码 ${error.code}`);
  963. console.error(UtilName, 'testTag', '可能原因: 服务器磁盘空间已满');
  964. } else {
  965. console.error(UtilName, 'testTag', `未知错误: 错误码 ${error.code}`);
  966. }
  967. }
  968. console.error(UtilName, "testTag", `错误详情: ${JSON.stringify(error)}`);
  969. reject(error);
  970. }
  971. });
  972. }
  973. /**
  974. * 流式读取文件
  975. * @param filePath 文件路径
  976. * @param fileSize 文件大小
  977. */
  978. private async readFileStream(filePath: string, fileSize: number): Promise<ArrayBuffer> {
  979. try {
  980. // 性能优化:根据文件大小选择合适的读取策略
  981. const LARGE_FILE_THRESHOLD = 50 * 1024 * 1024; // 50MB阈值
  982. if (fileSize > LARGE_FILE_THRESHOLD) {
  983. // 大文件:使用流式读取避免内存溢出
  984. console.info(UtilName, 'testTag', `使用流式读取大文件 (${(fileSize / 1024 / 1024).toFixed(2)} MB)`);
  985. // 注意:当前实现仍使用FileManager.readFileToArrayBuffer
  986. // 在实际生产环境中,应该实现真正的分块流式读取
  987. // 这里保留接口以便未来扩展
  988. return await FileManager.readFileToArrayBuffer(filePath);
  989. } else {
  990. // 小文件:直接读取到内存
  991. console.info(UtilName, 'testTag', `直接读取小文件 (${(fileSize / 1024).toFixed(2)} KB)`);
  992. return await FileManager.readFileToArrayBuffer(filePath);
  993. }
  994. } catch (err) {
  995. const error = err as Error;
  996. console.error(UtilName, 'testTag', `读取文件失败: ${error.message}`);
  997. throw error;
  998. }
  999. }
  1000. /**
  1001. * 延迟函数
  1002. * @param ms 延迟毫秒数
  1003. */
  1004. private delay(ms: number): Promise<void> {
  1005. return new Promise<void>((resolve) => {
  1006. setTimeout(() => {
  1007. resolve();
  1008. }, ms);
  1009. });
  1010. }
  1011. }