| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- import { fileIo, WatchEvent } from '@kit.CoreFileKit';
- import { FileUtil, PreferencesUtil } from '@pura/harmony-utils';
- import Logger from './Logger';
- import MediaTable from './MediaTable';
- import { VideoItem } from '../../viewmodel/VideoItem';
- import { Utility } from './Utility';
- import { CommonConstants } from '../constants/CommonConstants';
- const TAG = 'heanup FileDeletionWatcher';
- const WATCH_EVENT_MASK = 0x200 | 0x400 | 0x40 | 0x80 | 0x100; // 删除、目录自删、移出、移入和新建
- const DELETE_EVENT_MASK = 0x200 | 0x400 | 0x40;
- const CREATE_EVENT_MASK = 0x100;
- const MOVED_TO_EVENT_MASK = 0x80;
- const CONSISTENCY_SCAN_INTERVAL = 10000;
- const CONSISTENCY_BATCH_SIZE = 5;
- /**
- * 负责监听指定目录下的文件删除事件,并同步更新数据库。
- */
- export default class FileDeletionWatcher {
- private watchers: Map<string, fileIo.Watcher> = new Map();
- private dbReadyPromise?: Promise<void>;
- private context: Context;
- private mediaTable: MediaTable;
- private scanTimer?: number;
- private scanning: boolean = false;
- private nextScanIndex: number = 0;
- constructor(context: Context, mediaTable: MediaTable) {
- this.context = context;
- this.mediaTable = mediaTable;
- }
- /**
- * 启动目录监听,重复调用会重新建立监听,确保路径列表更新生效。
- */
- async watchDirectories(paths: string[]): Promise<void> {
- const targets = Array.from(new Set(
- paths
- .map(path => this.normalizePath(path))
- .filter(path => !!path)
- ));
- if (targets.length === 0) {
- return;
- }
- await this.ensureDbReady();
- this.stop();
- targets.forEach(path => this.registerRecursive(path));
- this.scheduleConsistencyCheck();
- await this.runConsistencyBatch(); // 启动时先做一次全量校验,确保数据库与实际文件同步
- }
- /**
- * 停止所有监听。
- */
- stop(): void {
- if (this.scanTimer) {
- clearInterval(this.scanTimer);
- this.scanTimer = undefined;
- }
- this.scanning = false;
- this.nextScanIndex = 0;
- this.watchers.forEach((watcher, path) => {
- try {
- watcher.stop();
- Logger.info(TAG, `停止监听目录: ${path}`);
- console.info(`[FileWatcher] 停止监听目录: ${path}`);
- } catch (error) {
- Logger.warn(TAG, `停止监听目录失败: ${path}, ${(error as Error).message}`);
- console.warn(`[FileWatcher] 停止监听目录失败: ${path}, ${(error as Error).message}`);
- }
- });
- this.watchers.clear();
- }
- private async ensureDbReady(): Promise<void> {
- if (!this.dbReadyPromise) {
- this.dbReadyPromise = new Promise<void>(resolve => {
- this.mediaTable.getRdbStore(this.context, () => resolve());
- });
- }
- return this.dbReadyPromise;
- }
- private registerRecursive(rawPath: string): void {
- const path = this.normalizePath(rawPath);
- if (!path || this.watchers.has(path) || !this.canAccess(path)) {
- return;
- }
- try {
- const watcher = fileIo.createWatcher(path, WATCH_EVENT_MASK, (event: WatchEvent) => {
- this.handleWatchEvent(event);
- });
- watcher.start();
- this.watchers.set(path, watcher);
- Logger.info(TAG, `开始监听目录: ${path}`);
- console.info(`[FileWatcher] 开始监听目录: ${path}`);
- } catch (error) {
- Logger.error(TAG, `监听目录失败: ${path}, ${(error as Error).message}`);
- console.error(`[FileWatcher] 监听目录失败: ${path}, ${(error as Error).message}`);
- return;
- }
- // 递归监听已有的子目录,避免漏掉深层目录的删除事件
- try {
- const entries = FileUtil.listFileSync(path);
- entries.forEach(name => {
- const childPath = `${path}/${name}`;
- if (this.isDirectory(childPath)) {
- this.registerRecursive(childPath);
- }
- });
- } catch (error) {
- Logger.warn(TAG, `遍历目录失败: ${path}, ${(error as Error).message}`);
- }
- }
- private handleWatchEvent(event: WatchEvent): void {
- const targetPath: string = this.normalizePath(event.fileName ?? '');
- const mask: number = event.event ?? 0;
- if (!targetPath) {
- return;
- }
- this.logWatchEvent(targetPath, mask);
- if ((mask & CREATE_EVENT_MASK) !== 0 || (mask & MOVED_TO_EVENT_MASK) !== 0) {
- this.handlePotentialAddition(targetPath);
- return;
- }
- if ((mask & DELETE_EVENT_MASK) !== 0) {
- this.onFileDeleted(targetPath, mask);
- }
- }
- private tryRegisterNewDirectory(path: string): void {
- if (this.isDirectory(path)) {
- this.registerRecursive(path);
- }
- }
- private handlePotentialAddition(path: string): void {
- if (this.isDirectory(path)) {
- this.registerRecursive(path);
- return;
- }
- this.handleFileAdded(path).catch((error: Error) => {
- Logger.error(TAG, `新增文件处理失败: ${path}, ${error.message}`);
- console.error(`[FileWatcher] 新增文件处理失败: ${path}, ${error.message}`);
- });
- }
- private onFileDeleted(path: string, mask: number): void {
- if (this.watchers.has(path) || (mask & 0x400) !== 0) {
- // 目录被删除,停止监听并删除其下所有记录
- this.stopWatcher(path);
- this.deleteByParentPath(path);
- return;
- }
- this.deleteByFilePath(path);
- }
- private deleteByFilePath(path: string): void {
- this.mediaTable.deleteDataFilePath(path, (success: boolean) => {
- if (success) {
- this.logRemoval(path, 'watch');
- } else {
- Logger.warn(TAG, `删除文件记录失败或不存在: ${path}`);
- console.warn(`[FileWatcher] 删除文件记录失败或不存在: ${path}`);
- }
- });
- }
- private deleteByParentPath(path: string): void {
- this.mediaTable.deleteDataForParentPath(path, (success: boolean) => {
- if (success) {
- Logger.info(TAG, `数据库已清理目录下的所有文件: ${path}`);
- console.info(`[FileWatcher] 数据库已清理目录下的所有文件: ${path}`);
- } else {
- Logger.warn(TAG, `未找到需要删除的目录记录: ${path}`);
- console.warn(`[FileWatcher] 未找到需要删除的目录记录: ${path}`);
- }
- });
- }
- private stopWatcher(path: string): void {
- const watcher = this.watchers.get(path);
- if (!watcher) {
- return;
- }
- try {
- watcher.stop();
- Logger.info(TAG, `目录被删除,停止监听: ${path}`);
- console.info(`[FileWatcher] 目录被删除,停止监听: ${path}`);
- } catch (error) {
- Logger.warn(TAG, `停止监听失败: ${path}, ${(error as Error).message}`);
- console.warn(`[FileWatcher] 停止监听失败: ${path}, ${(error as Error).message}`);
- }
- this.watchers.delete(path);
- }
- private canAccess(path: string): boolean {
- try {
- return FileUtil.accessSync(path);
- } catch (error) {
- Logger.warn(TAG, `路径不可访问: ${path}, ${(error as Error).message}`);
- console.warn(`[FileWatcher] 路径不可访问: ${path}, ${(error as Error).message}`);
- return false;
- }
- }
- private isDirectory(path: string): boolean {
- try {
- return FileUtil.isDirectory(path);
- } catch (error) {
- return false;
- }
- }
- private normalizePath(path: string): string {
- if (!path) {
- return '';
- }
- let normalized = path.trim();
- try {
- normalized = FileUtil.getFileUri(normalized).path;
- } catch (error) {
- // ignore, path 可能已经是普通路径
- }
- if (normalized.length === 0) {
- return '';
- }
- if (!normalized.startsWith('/')) {
- normalized = `/${normalized}`;
- }
- while (normalized.endsWith('/') && normalized.length > 1) {
- normalized = normalized.substring(0, normalized.length - 1);
- }
- return normalized;
- }
- private logWatchEvent(path: string, mask: number): void {
- const hexMask = `0x${mask.toString(16)}`;
- const message = `[FileWatcher] 监听回调: path=${path}, event=${hexMask}`;
- Logger.info(TAG, message);
- console.info(message);
- }
- private logRemoval(path: string, source: string): void {
- const message = `[FileWatcher:${source}] 检测到文件被删除并同步数据库: ${path}`;
- Logger.info(TAG, message);
- console.info(message);
- }
- private scheduleConsistencyCheck(): void {
- if (this.scanTimer) {
- return;
- }
- this.scanTimer = setInterval(() => {
- this.runConsistencyBatch();
- }, CONSISTENCY_SCAN_INTERVAL);
- }
- private async runConsistencyBatch(): Promise<void> {
- if (this.scanning) {
- return;
- }
- const dirs = Array.from(this.watchers.keys());
- if (dirs.length === 0) {
- return;
- }
- this.scanning = true;
- try {
- const batchSize = Math.min(CONSISTENCY_BATCH_SIZE, dirs.length);
- for (let i = 0; i < batchSize; i++) {
- const index = (this.nextScanIndex + i) % dirs.length;
- const dir = dirs[index];
- await this.ensureDirectoryConsistency(dir);
- }
- this.nextScanIndex = (this.nextScanIndex + batchSize) % dirs.length;
- } finally {
- this.scanning = false;
- }
- }
- private async ensureDirectoryConsistency(dir: string): Promise<void> {
- await new Promise<void>((resolve) => {
- this.mediaTable.queryByParentPath(dir, async (items: Array<VideoItem>) => {
- for (let i = 0; i < items.length; i++) {
- await this.removeIfMissing(items[i].filePath);
- }
- resolve();
- });
- });
- }
- private async removeIfMissing(filePath?: string): Promise<void> {
- if (!filePath) {
- return;
- }
- let exists = true;
- try {
- exists = FileUtil.accessSync(filePath);
- } catch (_) {
- exists = false;
- }
- if (exists) {
- return;
- }
- await new Promise<void>((resolve) => {
- this.mediaTable.deleteDataFilePath(filePath, (success: boolean) => {
- if (success) {
- this.logRemoval(filePath, 'scan');
- } else {
- Logger.warn(TAG, `清理不存在的文件记录失败: ${filePath}`);
- console.warn(`[FileWatcher] 清理不存在的文件记录失败: ${filePath}`);
- }
- resolve();
- });
- });
- }
- private async handleFileAdded(filePath?: string): Promise<void> {
- if (!filePath) {
- return;
- }
- const normalizedPath = this.normalizePath(filePath);
- if (!normalizedPath || !this.canAccess(normalizedPath) || this.isDirectory(normalizedPath)) {
- return;
- }
- if (!Utility.isMeidaByExtension(normalizedPath)) {
- return;
- }
- const autoParse = PreferencesUtil.getBooleanSync('autoParseMusicName', true);
- const coverApi = PreferencesUtil.getStringSync('COVER_API', CommonConstants.COVER_API);
- try {
- const mediaItem = await Utility.uriGetMusicAssetsFromFile(
- this.context,
- normalizedPath,
- CommonConstants.TYPE_LOCAL,
- autoParse
- );
- await new Promise<void>((resolve) => {
- this.mediaTable.insert(mediaItem, () => resolve(), coverApi);
- });
- this.logAddition(normalizedPath);
- } catch (error) {
- Logger.error(TAG, `新增文件解析或入库失败: ${normalizedPath}, ${(error as Error).message}`);
- console.error(`[FileWatcher] 新增文件解析或入库失败: ${normalizedPath}, ${(error as Error).message}`);
- }
- }
- private logAddition(path: string): void {
- const message = `[FileWatcher] 检测到新文件添加并入库: ${path}`;
- Logger.info(TAG, message);
- console.info(message);
- }
- }
|