WebdavManager.ets 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. // WebdavManager - WebDAV管理器(简化版)
  2. import { Song } from '../../viewmodel/Song';
  3. import { common } from '@kit.AbilityKit';
  4. import { FileInfo } from '../../viewmodel/FileInfo';
  5. import FileManager, { merge2paths } from './FileManager';
  6. import { BusinessError } from '@kit.BasicServicesKit';
  7. import { WebdavManagerStates } from '../enums/WebdavManagerStates';
  8. import { SongType } from '../enums/SongType';
  9. import { RcpSocket } from './RcpSocketUtil';
  10. import { DataBaseUtil } from './DataBaseUtil';
  11. import { WebDavAccount } from '../../viewmodel/WebDavAccount';
  12. import { relationalStore } from '@kit.ArkData';
  13. import { Constants } from '../../Constants';
  14. import Logger from './Logger';
  15. import { buffer } from '@kit.ArkTS';
  16. const TAG = 'heanup WebdavManager';
  17. // WebDAV认证信息接口
  18. export interface WebDavAuthInfo {
  19. headers: Record<string, string>;
  20. url: string;
  21. }
  22. // 流媒体认证信息接口
  23. export interface StreamAuthInfo {
  24. url: string;
  25. headers: Record<string, string>;
  26. }
  27. export interface TransferTask {
  28. song: Song;
  29. account: WebDavAccount;
  30. }
  31. @Observed
  32. export class WebdavManager {
  33. public rcpSocket: RcpSocket = RcpSocket.getInstance();
  34. private dataBaseUtil = DataBaseUtil.getInstance();
  35. public observers: Array<(event: string) => void> = [];
  36. public static instance: WebdavManager;
  37. public context: common.Context | undefined;
  38. public DownloadDirectoryFilePath: string = '';
  39. public audioExtensions = Constants.AUDIO_EXTENSIONS;
  40. public lyricExtensions = Constants.LYRIC_EXTENSIONS;
  41. public imageExtensions = Constants.IMAGE_EXTENSIONS;
  42. // 数据表名称
  43. public preferenceName = Constants.WEBDAV_PREFERENCE_NAME;
  44. public webDavTable = 'WebDavAccount';
  45. public receivedSize: number = 0;
  46. public totalSize: number = 0;
  47. public webDavAccounts: WebDavAccount[] = [];
  48. public webDavSongs: Song[] = [];
  49. public webDavFiles: FileInfo[] = []; // 当前目录的所有文件(包括文件夹)
  50. // 路径导航
  51. public currentPath: string = ''; // 当前浏览的路径
  52. public pathHistory: string[] = []; // 路径历史记录
  53. // 错误信息
  54. public ErrorMessage: string | BusinessError = '';
  55. // 下载队列
  56. public downloadQueue: TransferTask[] = [];
  57. public finishDownloadQueue: TransferTask[] = [];
  58. public isProcessingQueue: boolean = false;
  59. public currentDownloadTask: TransferTask | null = null;
  60. public isPauseDownload: boolean = true;
  61. // 上传队列
  62. public uploadQueue: TransferTask[] = [];
  63. public finishUploadQueue: TransferTask[] = [];
  64. public isProcessingUploadQueue: boolean = false;
  65. public currentUploadTask: TransferTask | null = null;
  66. public isPauseUpload: boolean = true;
  67. public constructor() {
  68. this.rcpSocket.subscribeHTTPDataTransfer((event: string) => {
  69. this.notifyObservers(event);
  70. });
  71. }
  72. public static getInstance(): WebdavManager {
  73. if (!WebdavManager.instance) {
  74. WebdavManager.instance = new WebdavManager();
  75. }
  76. return WebdavManager.instance;
  77. }
  78. public setContext(context: common.Context): void {
  79. this.context = context;
  80. this.DownloadDirectoryFilePath = merge2paths(context.filesDir, 'Download');
  81. }
  82. // ==================== 观察者模式 ====================
  83. public subscribe(callback: (event: string) => void): void {
  84. this.observers.push(callback);
  85. }
  86. public unsubscribe(callback: (event: string) => void): void {
  87. const index = this.observers.indexOf(callback);
  88. if (index > -1) {
  89. this.observers.splice(index, 1);
  90. }
  91. }
  92. public notifyObservers(event: string): void {
  93. Logger.info(TAG, '通知观察者:', event);
  94. Logger.info(TAG, '观察者数量:', this.observers.length.toString());
  95. for (let i = 0; i < this.observers.length; i++) {
  96. const observer = this.observers[i];
  97. Logger.info(TAG, '调用观察者', i.toString(), ',事件:', event);
  98. observer(event);
  99. }
  100. }
  101. // ==================== 数据库操作 ====================
  102. // 创建WebDAV账户表
  103. public createWebDavTableInDB(): Promise<void> {
  104. const sql = `CREATE TABLE IF NOT EXISTS ${this.webDavTable} (
  105. id INTEGER PRIMARY KEY AUTOINCREMENT,
  106. name TEXT,
  107. isActivate INTEGER DEFAULT 1,
  108. host TEXT,
  109. localHost TEXT,
  110. isUseLocalHost INTEGER DEFAULT 0,
  111. port INTEGER,
  112. filepath TEXT,
  113. imageFilePath TEXT,
  114. lyricFilePath TEXT,
  115. uploadFilePath TEXT,
  116. account TEXT,
  117. password TEXT,
  118. enableHttps INTEGER DEFAULT 0
  119. )`;
  120. return this.dataBaseUtil.executeSql(sql)
  121. .then(() => {
  122. Logger.info(TAG, 'WebDAV账户表创建成功');
  123. })
  124. .catch((err: Error) => {
  125. Logger.error(TAG, '创建WebDAV账户表失败:', err.message);
  126. throw err;
  127. });
  128. }
  129. // 从数据库查询所有账户
  130. public async queryWebDavAccountsFromDB(): Promise<void> {
  131. try {
  132. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  133. const columns = ['id', 'name', 'isActivate', 'host', 'localHost', 'isUseLocalHost',
  134. 'port', 'filepath', 'imageFilePath', 'lyricFilePath', 'uploadFilePath',
  135. 'account', 'password', 'enableHttps'];
  136. const resultSet = await this.dataBaseUtil.queryData(this.webDavTable, columns, predicates);
  137. this.webDavAccounts = [];
  138. while (resultSet.goToNextRow()) {
  139. const account = new WebDavAccount();
  140. account.id = resultSet.getLong(resultSet.getColumnIndex('id'));
  141. account.name = resultSet.getString(resultSet.getColumnIndex('name'));
  142. account.isActivate = resultSet.getLong(resultSet.getColumnIndex('isActivate')) === 1;
  143. account.host = resultSet.getString(resultSet.getColumnIndex('host'));
  144. account.localHost = resultSet.getString(resultSet.getColumnIndex('localHost'));
  145. account.isUseLocalHost = resultSet.getLong(resultSet.getColumnIndex('isUseLocalHost')) === 1;
  146. account.port = resultSet.getLong(resultSet.getColumnIndex('port'));
  147. account.filepath = resultSet.getString(resultSet.getColumnIndex('filepath'));
  148. account.imageFilePath = resultSet.getString(resultSet.getColumnIndex('imageFilePath'));
  149. account.lyricFilePath = resultSet.getString(resultSet.getColumnIndex('lyricFilePath'));
  150. account.uploadFilePath = resultSet.getString(resultSet.getColumnIndex('uploadFilePath'));
  151. account.account = resultSet.getString(resultSet.getColumnIndex('account'));
  152. account.password = resultSet.getString(resultSet.getColumnIndex('password'));
  153. account.enableHttps = resultSet.getLong(resultSet.getColumnIndex('enableHttps')) === 1;
  154. this.webDavAccounts.push(account);
  155. }
  156. resultSet.close();
  157. Logger.info(TAG, '从数据库加载了', this.webDavAccounts.length.toString(), '个WebDAV账户');
  158. this.notifyObservers(WebdavManagerStates.QueryAccountsSucceed);
  159. } catch (err) {
  160. const error = err as Error;
  161. Logger.error(TAG, '查询WebDAV账户失败:', error.message);
  162. this.notifyObservers(WebdavManagerStates.QueryAccountsFailed);
  163. throw error;
  164. }
  165. }
  166. // 插入新账户
  167. public async insertAccount(
  168. name: string,
  169. host: string,
  170. localHost: string,
  171. isUseLocalHost: boolean,
  172. port: number,
  173. filepath: string,
  174. lyricFilePath: string,
  175. uploadFilePath: string,
  176. imageFilePath: string,
  177. account: string,
  178. password: string,
  179. enableHttps: boolean
  180. ): Promise<void> {
  181. try {
  182. const values: relationalStore.ValuesBucket = {
  183. 'name': name,
  184. 'isActivate': 1,
  185. 'host': host,
  186. 'localHost': localHost,
  187. 'isUseLocalHost': isUseLocalHost ? 1 : 0,
  188. 'port': port,
  189. 'filepath': filepath,
  190. 'imageFilePath': imageFilePath,
  191. 'lyricFilePath': lyricFilePath,
  192. 'uploadFilePath': uploadFilePath,
  193. 'account': account,
  194. 'password': password,
  195. 'enableHttps': enableHttps ? 1 : 0
  196. };
  197. await this.dataBaseUtil.insertData(this.webDavTable, values);
  198. Logger.info(TAG, '插入WebDAV账户成功:', name);
  199. await this.queryWebDavAccountsFromDB();
  200. this.notifyObservers(WebdavManagerStates.InsertAccountSucceed);
  201. } catch (err) {
  202. const error = err as Error;
  203. Logger.error(TAG, '插入WebDAV账户失败:', error.message);
  204. this.notifyObservers(WebdavManagerStates.InsertAccountFailed);
  205. throw error;
  206. }
  207. }
  208. // 编辑账户
  209. public async editAccount(account: WebDavAccount): Promise<void> {
  210. try {
  211. const values: relationalStore.ValuesBucket = {
  212. 'name': account.name,
  213. 'isActivate': account.isActivate ? 1 : 0,
  214. 'host': account.host,
  215. 'localHost': account.localHost,
  216. 'isUseLocalHost': account.isUseLocalHost ? 1 : 0,
  217. 'port': account.port,
  218. 'filepath': account.filepath,
  219. 'imageFilePath': account.imageFilePath,
  220. 'lyricFilePath': account.lyricFilePath,
  221. 'uploadFilePath': account.uploadFilePath,
  222. 'account': account.account,
  223. 'password': account.password,
  224. 'enableHttps': account.enableHttps ? 1 : 0
  225. };
  226. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  227. predicates.equalTo('id', account.id);
  228. await this.dataBaseUtil.updateData(this.webDavTable, values, predicates);
  229. Logger.info(TAG, '更新WebDAV账户成功:', account.name);
  230. await this.queryWebDavAccountsFromDB();
  231. this.notifyObservers(WebdavManagerStates.EditAccountSucceed);
  232. } catch (err) {
  233. const error = err as Error;
  234. Logger.error(TAG, '更新WebDAV账户失败:', error.message);
  235. this.notifyObservers(WebdavManagerStates.EditAccountFailed);
  236. throw error;
  237. }
  238. }
  239. // 删除账户
  240. public async removeAccount(account: WebDavAccount): Promise<void> {
  241. try {
  242. const predicates = new relationalStore.RdbPredicates(this.webDavTable);
  243. predicates.equalTo('id', account.id);
  244. await this.dataBaseUtil.deleteData(predicates);
  245. Logger.info(TAG, '删除WebDAV账户成功:', account.name);
  246. await this.queryWebDavAccountsFromDB();
  247. this.notifyObservers(WebdavManagerStates.RemoveAccountSucceed);
  248. } catch (err) {
  249. const error = err as Error;
  250. Logger.error(TAG, '删除WebDAV账户失败:', error.message);
  251. this.notifyObservers(WebdavManagerStates.RemoveAccountFailed);
  252. throw error;
  253. }
  254. }
  255. // 获取所有账户
  256. public getAllWebDavAccounts(): WebDavAccount[] {
  257. return this.webDavAccounts;
  258. }
  259. // 获取激活的账户
  260. public getActivatedWebDavAccount(): WebDavAccount | null {
  261. for (let i = 0; i < this.webDavAccounts.length; i++) {
  262. const account = this.webDavAccounts[i];
  263. if (account.isActivate) {
  264. return account;
  265. }
  266. }
  267. return null;
  268. }
  269. // ==================== 文件操作 ====================
  270. // 从WebDAV加载文件列表
  271. public async loadFilesInfoFromWebdav(customPath?: string): Promise<void> {
  272. const account = this.getActivatedWebDavAccount();
  273. if (!account) {
  274. Logger.error(TAG, '没有激活的WebDAV账户');
  275. this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
  276. return;
  277. }
  278. try {
  279. this.notifyObservers(WebdavManagerStates.LoadFilesInfoStart);
  280. // 使用自定义路径或账户默认路径
  281. const path = customPath !== undefined ? customPath : account.filepath;
  282. this.currentPath = path;
  283. const files = await this.rcpSocket.getFileList(
  284. account.host,
  285. account.localHost,
  286. account.isUseLocalHost,
  287. account.port,
  288. path,
  289. account.account,
  290. account.password,
  291. account.enableHttps
  292. );
  293. // 保存所有文件(包括文件夹)
  294. // 直接使用从RcpSocketUtil返回的FileInfo对象
  295. this.webDavFiles = [];
  296. for (let i = 0; i < files.length; i++) {
  297. const file = files[i];
  298. // 直接添加原始文件对象
  299. this.webDavFiles.push(file);
  300. }
  301. // 调试:输出获取到的文件总数
  302. Logger.info(TAG, '从WebDAV获取到 ' + files.length + ' 个文件/文件夹');
  303. // 分别统计文件夹和音频文件
  304. let folderCount = 0;
  305. let audioCount = 0;
  306. // 过滤音频文件
  307. this.webDavSongs = [];
  308. for (let i = 0; i < files.length; i++) {
  309. const file = files[i];
  310. const fileName = file.fileName;
  311. if (file.isDirectory) {
  312. folderCount++;
  313. } else if (this.isAudioFile(fileName)) {
  314. audioCount++;
  315. const song = this.fileInfoToSong(file, account);
  316. this.webDavSongs.push(song);
  317. }
  318. }
  319. this.notifyObservers(WebdavManagerStates.LoadFilesInfoSucceed);
  320. } catch (error) {
  321. this.ErrorMessage = error as BusinessError;
  322. this.notifyObservers(WebdavManagerStates.LoadFilesInfoFailed);
  323. }
  324. }
  325. // 将FileInfo转换为Song
  326. private fileInfoToSong(fileInfo: FileInfo, account: WebDavAccount): Song {
  327. const song = new Song(-1, '');
  328. song.title = this.getFileNameWithoutExtension(fileInfo.fileName);
  329. song.artist = Constants.UNKNOWN_ARTIST;
  330. song.name = fileInfo.fileName;
  331. // 构建安全的WebDAV URL(不包含认证信息)
  332. const protocol = account.enableHttps ? 'https' : 'http';
  333. const host = account.isUseLocalHost ? account.localHost : account.host;
  334. const port = account.port;
  335. // 构建基础URL,不包含认证信息
  336. song.src = `${protocol}://${host}:${port}${fileInfo.href}`;
  337. song.songType = SongType.WebDav;
  338. song.webDavAccountId = account.id;
  339. song.webFilePath = fileInfo.href;
  340. song.fileSize = fileInfo.contentLength;
  341. song.time = fileInfo.time;
  342. song.img = Constants.COMMON_SONG_DEFAULT_IMAGE;
  343. return song;
  344. }
  345. // 判断是否为音频文件
  346. private isAudioFile(fileName: string): boolean {
  347. const ext = this.getFileExtension(fileName).toLowerCase();
  348. for (let i = 0; i < this.audioExtensions.length; i++) {
  349. if (ext === this.audioExtensions[i]) {
  350. return true;
  351. }
  352. }
  353. return false;
  354. }
  355. // 获取文件扩展名
  356. private getFileExtension(fileName: string): string {
  357. const lastDotIndex = fileName.lastIndexOf('.');
  358. if (lastDotIndex === -1) {
  359. return '';
  360. }
  361. return fileName.substring(lastDotIndex);
  362. }
  363. // 获取不带扩展名的文件名
  364. private getFileNameWithoutExtension(fileName: string): string {
  365. const lastDotIndex = fileName.lastIndexOf('.');
  366. if (lastDotIndex === -1) {
  367. return fileName;
  368. }
  369. return fileName.substring(0, lastDotIndex);
  370. }
  371. // 进入文件夹
  372. public async enterFolder(folder: FileInfo): Promise<void> {
  373. if (!folder.isDirectory) {
  374. Logger.error(TAG, '不是文件夹,无法进入');
  375. return;
  376. }
  377. Logger.info(TAG, '准备进入文件夹:', folder.fileName);
  378. Logger.info(TAG, '当前路径:', this.currentPath);
  379. Logger.info(TAG, '目标路径:', folder.href);
  380. // 保存当前路径到历史记录
  381. this.pathHistory.push(this.currentPath);
  382. Logger.info(TAG, '路径历史:', JSON.stringify(this.pathHistory));
  383. // 加载文件夹内容
  384. await this.loadFilesInfoFromWebdav(folder.href);
  385. }
  386. // 返回上级目录
  387. public async goBack(): Promise<void> {
  388. if (this.pathHistory.length === 0) {
  389. Logger.info(TAG, '已经在根目录,无法返回');
  390. return;
  391. }
  392. // 从历史记录中取出上一级路径
  393. const previousPath = this.pathHistory.pop();
  394. if (previousPath !== undefined) {
  395. await this.loadFilesInfoFromWebdav(previousPath);
  396. }
  397. }
  398. // 获取面包屑路径数组
  399. public getBreadcrumbs(): string[] {
  400. if (!this.currentPath || this.currentPath === '/') {
  401. return ['根目录'];
  402. }
  403. const parts = this.currentPath.split('/').filter(part => part !== '');
  404. const breadcrumbs = ['根目录'];
  405. for (let i = 0; i < parts.length; i++) {
  406. breadcrumbs.push(parts[i]);
  407. }
  408. return breadcrumbs;
  409. }
  410. // 是否可以返回上级
  411. public canGoBack(): boolean {
  412. return this.pathHistory.length > 0;
  413. }
  414. // ==================== Preferences操作 ====================
  415. // 加载历史数据(从Preferences迁移)
  416. public async loadInfo(): Promise<void> {
  417. try {
  418. // 这里可以添加从Preferences加载历史配置的逻辑
  419. Logger.info(TAG, '从Preferences加载配置');
  420. } catch (error) {
  421. Logger.error(TAG, '从Preferences加载配置失败:', error.toString());
  422. }
  423. }
  424. // 保存配置到Preferences
  425. public async saveInfo(): Promise<void> {
  426. try {
  427. // 这里可以添加保存配置到Preferences的逻辑
  428. Logger.info(TAG, '保存配置到Preferences');
  429. } catch (error) {
  430. Logger.error(TAG, '保存配置到Preferences失败:', error.toString());
  431. }
  432. }
  433. // ==================== 下载队列管理 ====================
  434. // 添加到下载队列
  435. public addToDownloadQueue(song: Song, account: WebDavAccount): void {
  436. const task: TransferTask = { song, account };
  437. this.downloadQueue.push(task);
  438. Logger.info(TAG, '添加到下载队列:', song.title);
  439. this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
  440. }
  441. // 从下载队列移除
  442. public removeFromDownloadQueue(index: number): void {
  443. if (index >= 0 && index < this.downloadQueue.length) {
  444. const task = this.downloadQueue[index];
  445. this.downloadQueue.splice(index, 1);
  446. Logger.info(TAG, '从下载队列移除:', task.song.title);
  447. this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
  448. }
  449. }
  450. // 清空下载队列
  451. public clearDownloadQueue(): void {
  452. this.downloadQueue = [];
  453. Logger.info(TAG, '清空下载队列');
  454. this.notifyObservers(WebdavManagerStates.DownloadQueueChanged);
  455. }
  456. // ==================== 上传队列管理 ====================
  457. // 添加到上传队列
  458. public addToUploadQueue(song: Song, account: WebDavAccount): void {
  459. const task: TransferTask = { song, account };
  460. this.uploadQueue.push(task);
  461. Logger.info(TAG, '添加到上传队列:', song.title);
  462. this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
  463. }
  464. // 从上传队列移除
  465. public removeFromUploadQueue(index: number): void {
  466. if (index >= 0 && index < this.uploadQueue.length) {
  467. const task = this.uploadQueue[index];
  468. this.uploadQueue.splice(index, 1);
  469. Logger.info(TAG, '从上传队列移除:', task.song.title);
  470. this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
  471. }
  472. }
  473. // 清空上传队列
  474. public clearUploadQueue(): void {
  475. this.uploadQueue = [];
  476. Logger.info(TAG, '清空上传队列');
  477. this.notifyObservers(WebdavManagerStates.UploadQueueChanged);
  478. }
  479. // ==================== 安全认证方法 ====================
  480. // 获取WebDAV认证信息(用于播放器)
  481. public getWebDavAuthHeaders(accountId: number): WebDavAuthInfo | null {
  482. const account = this.getWebDavAccountById(accountId);
  483. if (!account || !account.account || !account.password) {
  484. Logger.error(TAG, '无效的WebDAV账户或缺少认证信息');
  485. return null;
  486. }
  487. // 构建基础URL
  488. const protocol = account.enableHttps ? 'https' : 'http';
  489. const host = account.isUseLocalHost ? account.localHost : account.host;
  490. const port = account.port;
  491. // 构建认证头
  492. const credentials = buffer
  493. .from(`${account.account}:${account.password}`)
  494. .toString("base64");
  495. const authInfo: WebDavAuthInfo = {
  496. headers: {
  497. 'Authorization': `Basic ${credentials}`,
  498. 'User-Agent': 'TTMusic/1.0'
  499. },
  500. url: `${protocol}://${host}:${port}`
  501. };
  502. return authInfo;
  503. }
  504. // 根据ID获取WebDAV账户
  505. private getWebDavAccountById(accountId: number): WebDavAccount | null {
  506. for (let i = 0; i < this.webDavAccounts.length; i++) {
  507. const account = this.webDavAccounts[i];
  508. if (account.id === accountId) {
  509. return account;
  510. }
  511. }
  512. return null;
  513. }
  514. // 获取安全的播放URL(不包含认证信息)
  515. public getSecurePlayUrl(song: Song): string | null {
  516. if (song.songType !== SongType.WebDav || !song.webDavAccountId) {
  517. return song.src;
  518. }
  519. const authInfo = this.getWebDavAuthHeaders(song.webDavAccountId);
  520. if (!authInfo) {
  521. return null;
  522. }
  523. return authInfo.url + song.webFilePath;
  524. }
  525. }