RcpSocketUtil.ets 42 KB

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