FileDeletionWatcher.ets 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import { fileIo, WatchEvent } from '@kit.CoreFileKit';
  2. import { FileUtil, PreferencesUtil } from '@pura/harmony-utils';
  3. import Logger from './Logger';
  4. import MediaTable from './MediaTable';
  5. import { VideoItem } from '../../viewmodel/VideoItem';
  6. import { Utility } from './Utility';
  7. import { CommonConstants } from '../constants/CommonConstants';
  8. const TAG = 'heanup FileDeletionWatcher';
  9. const WATCH_EVENT_MASK = 0x200 | 0x400 | 0x40 | 0x80 | 0x100; // 删除、目录自删、移出、移入和新建
  10. const DELETE_EVENT_MASK = 0x200 | 0x400 | 0x40;
  11. const CREATE_EVENT_MASK = 0x100;
  12. const MOVED_TO_EVENT_MASK = 0x80;
  13. const CONSISTENCY_SCAN_INTERVAL = 10000;
  14. const CONSISTENCY_BATCH_SIZE = 5;
  15. /**
  16. * 负责监听指定目录下的文件删除事件,并同步更新数据库。
  17. */
  18. export default class FileDeletionWatcher {
  19. private watchers: Map<string, fileIo.Watcher> = new Map();
  20. private dbReadyPromise?: Promise<void>;
  21. private context: Context;
  22. private mediaTable: MediaTable;
  23. private scanTimer?: number;
  24. private scanning: boolean = false;
  25. private nextScanIndex: number = 0;
  26. constructor(context: Context, mediaTable: MediaTable) {
  27. this.context = context;
  28. this.mediaTable = mediaTable;
  29. }
  30. /**
  31. * 启动目录监听,重复调用会重新建立监听,确保路径列表更新生效。
  32. */
  33. async watchDirectories(paths: string[]): Promise<void> {
  34. const targets = Array.from(new Set(
  35. paths
  36. .map(path => this.normalizePath(path))
  37. .filter(path => !!path)
  38. ));
  39. if (targets.length === 0) {
  40. return;
  41. }
  42. await this.ensureDbReady();
  43. this.stop();
  44. targets.forEach(path => this.registerRecursive(path));
  45. this.scheduleConsistencyCheck();
  46. await this.runConsistencyBatch(); // 启动时先做一次全量校验,确保数据库与实际文件同步
  47. }
  48. /**
  49. * 停止所有监听。
  50. */
  51. stop(): void {
  52. if (this.scanTimer) {
  53. clearInterval(this.scanTimer);
  54. this.scanTimer = undefined;
  55. }
  56. this.scanning = false;
  57. this.nextScanIndex = 0;
  58. this.watchers.forEach((watcher, path) => {
  59. try {
  60. watcher.stop();
  61. Logger.info(TAG, `停止监听目录: ${path}`);
  62. console.info(`[FileWatcher] 停止监听目录: ${path}`);
  63. } catch (error) {
  64. Logger.warn(TAG, `停止监听目录失败: ${path}, ${(error as Error).message}`);
  65. console.warn(`[FileWatcher] 停止监听目录失败: ${path}, ${(error as Error).message}`);
  66. }
  67. });
  68. this.watchers.clear();
  69. }
  70. private async ensureDbReady(): Promise<void> {
  71. if (!this.dbReadyPromise) {
  72. this.dbReadyPromise = new Promise<void>(resolve => {
  73. this.mediaTable.getRdbStore(this.context, () => resolve());
  74. });
  75. }
  76. return this.dbReadyPromise;
  77. }
  78. private registerRecursive(rawPath: string): void {
  79. const path = this.normalizePath(rawPath);
  80. if (!path || this.watchers.has(path) || !this.canAccess(path)) {
  81. return;
  82. }
  83. try {
  84. const watcher = fileIo.createWatcher(path, WATCH_EVENT_MASK, (event: WatchEvent) => {
  85. this.handleWatchEvent(event);
  86. });
  87. watcher.start();
  88. this.watchers.set(path, watcher);
  89. Logger.info(TAG, `开始监听目录: ${path}`);
  90. console.info(`[FileWatcher] 开始监听目录: ${path}`);
  91. } catch (error) {
  92. Logger.error(TAG, `监听目录失败: ${path}, ${(error as Error).message}`);
  93. console.error(`[FileWatcher] 监听目录失败: ${path}, ${(error as Error).message}`);
  94. return;
  95. }
  96. // 递归监听已有的子目录,避免漏掉深层目录的删除事件
  97. try {
  98. const entries = FileUtil.listFileSync(path);
  99. entries.forEach(name => {
  100. const childPath = `${path}/${name}`;
  101. if (this.isDirectory(childPath)) {
  102. this.registerRecursive(childPath);
  103. }
  104. });
  105. } catch (error) {
  106. Logger.warn(TAG, `遍历目录失败: ${path}, ${(error as Error).message}`);
  107. }
  108. }
  109. private handleWatchEvent(event: WatchEvent): void {
  110. const targetPath: string = this.normalizePath(event.fileName ?? '');
  111. const mask: number = event.event ?? 0;
  112. if (!targetPath) {
  113. return;
  114. }
  115. this.logWatchEvent(targetPath, mask);
  116. if ((mask & CREATE_EVENT_MASK) !== 0 || (mask & MOVED_TO_EVENT_MASK) !== 0) {
  117. this.handlePotentialAddition(targetPath);
  118. return;
  119. }
  120. if ((mask & DELETE_EVENT_MASK) !== 0) {
  121. this.onFileDeleted(targetPath, mask);
  122. }
  123. }
  124. private tryRegisterNewDirectory(path: string): void {
  125. if (this.isDirectory(path)) {
  126. this.registerRecursive(path);
  127. }
  128. }
  129. private handlePotentialAddition(path: string): void {
  130. if (this.isDirectory(path)) {
  131. this.registerRecursive(path);
  132. return;
  133. }
  134. this.handleFileAdded(path).catch((error: Error) => {
  135. Logger.error(TAG, `新增文件处理失败: ${path}, ${error.message}`);
  136. console.error(`[FileWatcher] 新增文件处理失败: ${path}, ${error.message}`);
  137. });
  138. }
  139. private onFileDeleted(path: string, mask: number): void {
  140. if (this.watchers.has(path) || (mask & 0x400) !== 0) {
  141. // 目录被删除,停止监听并删除其下所有记录
  142. this.stopWatcher(path);
  143. this.deleteByParentPath(path);
  144. return;
  145. }
  146. this.deleteByFilePath(path);
  147. }
  148. private deleteByFilePath(path: string): void {
  149. this.mediaTable.deleteDataFilePath(path, (success: boolean) => {
  150. if (success) {
  151. this.logRemoval(path, 'watch');
  152. } else {
  153. Logger.warn(TAG, `删除文件记录失败或不存在: ${path}`);
  154. console.warn(`[FileWatcher] 删除文件记录失败或不存在: ${path}`);
  155. }
  156. });
  157. }
  158. private deleteByParentPath(path: string): void {
  159. this.mediaTable.deleteDataForParentPath(path, (success: boolean) => {
  160. if (success) {
  161. Logger.info(TAG, `数据库已清理目录下的所有文件: ${path}`);
  162. console.info(`[FileWatcher] 数据库已清理目录下的所有文件: ${path}`);
  163. } else {
  164. Logger.warn(TAG, `未找到需要删除的目录记录: ${path}`);
  165. console.warn(`[FileWatcher] 未找到需要删除的目录记录: ${path}`);
  166. }
  167. });
  168. }
  169. private stopWatcher(path: string): void {
  170. const watcher = this.watchers.get(path);
  171. if (!watcher) {
  172. return;
  173. }
  174. try {
  175. watcher.stop();
  176. Logger.info(TAG, `目录被删除,停止监听: ${path}`);
  177. console.info(`[FileWatcher] 目录被删除,停止监听: ${path}`);
  178. } catch (error) {
  179. Logger.warn(TAG, `停止监听失败: ${path}, ${(error as Error).message}`);
  180. console.warn(`[FileWatcher] 停止监听失败: ${path}, ${(error as Error).message}`);
  181. }
  182. this.watchers.delete(path);
  183. }
  184. private canAccess(path: string): boolean {
  185. try {
  186. return FileUtil.accessSync(path);
  187. } catch (error) {
  188. Logger.warn(TAG, `路径不可访问: ${path}, ${(error as Error).message}`);
  189. console.warn(`[FileWatcher] 路径不可访问: ${path}, ${(error as Error).message}`);
  190. return false;
  191. }
  192. }
  193. private isDirectory(path: string): boolean {
  194. try {
  195. return FileUtil.isDirectory(path);
  196. } catch (error) {
  197. return false;
  198. }
  199. }
  200. private normalizePath(path: string): string {
  201. if (!path) {
  202. return '';
  203. }
  204. let normalized = path.trim();
  205. try {
  206. normalized = FileUtil.getFileUri(normalized).path;
  207. } catch (error) {
  208. // ignore, path 可能已经是普通路径
  209. }
  210. if (normalized.length === 0) {
  211. return '';
  212. }
  213. if (!normalized.startsWith('/')) {
  214. normalized = `/${normalized}`;
  215. }
  216. while (normalized.endsWith('/') && normalized.length > 1) {
  217. normalized = normalized.substring(0, normalized.length - 1);
  218. }
  219. return normalized;
  220. }
  221. private logWatchEvent(path: string, mask: number): void {
  222. const hexMask = `0x${mask.toString(16)}`;
  223. const message = `[FileWatcher] 监听回调: path=${path}, event=${hexMask}`;
  224. Logger.info(TAG, message);
  225. console.info(message);
  226. }
  227. private logRemoval(path: string, source: string): void {
  228. const message = `[FileWatcher:${source}] 检测到文件被删除并同步数据库: ${path}`;
  229. Logger.info(TAG, message);
  230. console.info(message);
  231. }
  232. private scheduleConsistencyCheck(): void {
  233. if (this.scanTimer) {
  234. return;
  235. }
  236. this.scanTimer = setInterval(() => {
  237. this.runConsistencyBatch();
  238. }, CONSISTENCY_SCAN_INTERVAL);
  239. }
  240. private async runConsistencyBatch(): Promise<void> {
  241. if (this.scanning) {
  242. return;
  243. }
  244. const dirs = Array.from(this.watchers.keys());
  245. if (dirs.length === 0) {
  246. return;
  247. }
  248. this.scanning = true;
  249. try {
  250. const batchSize = Math.min(CONSISTENCY_BATCH_SIZE, dirs.length);
  251. for (let i = 0; i < batchSize; i++) {
  252. const index = (this.nextScanIndex + i) % dirs.length;
  253. const dir = dirs[index];
  254. await this.ensureDirectoryConsistency(dir);
  255. }
  256. this.nextScanIndex = (this.nextScanIndex + batchSize) % dirs.length;
  257. } finally {
  258. this.scanning = false;
  259. }
  260. }
  261. private async ensureDirectoryConsistency(dir: string): Promise<void> {
  262. await new Promise<void>((resolve) => {
  263. this.mediaTable.queryByParentPath(dir, async (items: Array<VideoItem>) => {
  264. for (let i = 0; i < items.length; i++) {
  265. await this.removeIfMissing(items[i].filePath);
  266. }
  267. resolve();
  268. });
  269. });
  270. }
  271. private async removeIfMissing(filePath?: string): Promise<void> {
  272. if (!filePath) {
  273. return;
  274. }
  275. let exists = true;
  276. try {
  277. exists = FileUtil.accessSync(filePath);
  278. } catch (_) {
  279. exists = false;
  280. }
  281. if (exists) {
  282. return;
  283. }
  284. await new Promise<void>((resolve) => {
  285. this.mediaTable.deleteDataFilePath(filePath, (success: boolean) => {
  286. if (success) {
  287. this.logRemoval(filePath, 'scan');
  288. } else {
  289. Logger.warn(TAG, `清理不存在的文件记录失败: ${filePath}`);
  290. console.warn(`[FileWatcher] 清理不存在的文件记录失败: ${filePath}`);
  291. }
  292. resolve();
  293. });
  294. });
  295. }
  296. private async handleFileAdded(filePath?: string): Promise<void> {
  297. if (!filePath) {
  298. return;
  299. }
  300. const normalizedPath = this.normalizePath(filePath);
  301. if (!normalizedPath || !this.canAccess(normalizedPath) || this.isDirectory(normalizedPath)) {
  302. return;
  303. }
  304. if (!Utility.isMeidaByExtension(normalizedPath)) {
  305. return;
  306. }
  307. const autoParse = PreferencesUtil.getBooleanSync('autoParseMusicName', true);
  308. const coverApi = PreferencesUtil.getStringSync('COVER_API', CommonConstants.COVER_API);
  309. try {
  310. const mediaItem = await Utility.uriGetMusicAssetsFromFile(
  311. this.context,
  312. normalizedPath,
  313. CommonConstants.TYPE_LOCAL,
  314. autoParse
  315. );
  316. await new Promise<void>((resolve) => {
  317. this.mediaTable.insert(mediaItem, () => resolve(), coverApi);
  318. });
  319. this.logAddition(normalizedPath);
  320. } catch (error) {
  321. Logger.error(TAG, `新增文件解析或入库失败: ${normalizedPath}, ${(error as Error).message}`);
  322. console.error(`[FileWatcher] 新增文件解析或入库失败: ${normalizedPath}, ${(error as Error).message}`);
  323. }
  324. }
  325. private logAddition(path: string): void {
  326. const message = `[FileWatcher] 检测到新文件添加并入库: ${path}`;
  327. Logger.info(TAG, message);
  328. console.info(message);
  329. }
  330. }