UNPKG

mira-app-server

Version:

Mira Server - standalone server application using mira-app-core

301 lines 12.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LibraryWatcher = void 0; const chokidar = __importStar(require("chokidar")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const SyncFilter_1 = require("./sync/SyncFilter"); class LibraryWatcher { constructor(libraryService, webSocketServer) { this.watcher = null; this.pendingUnlinks = new Map(); this.ignoredPaths = new Set(); this.usePolling = false; this.libraryService = libraryService; this.webSocketServer = webSocketServer; this.libraryPath = libraryService.config.customFields?.path || libraryService.config.path || ''; this.libraryId = libraryService.getLibraryId(); // 读取库配置里的黑白名单(多行文本),构造统一的过滤函数 this.shouldSync = (0, SyncFilter_1.createSyncFilter)(libraryService.config.customFields); } async start() { if (!this.libraryPath || !fs_1.default.existsSync(this.libraryPath)) { console.warn(`[Watcher] Library path does not exist: ${this.libraryPath}`); return; } this.startWatcher(false); // 启动时扫描已有文件,同步未入库的 this.initialSync(); console.log(`[Watcher] Started watching: ${this.libraryPath}${this.usePolling ? ' (polling)' : ''}`); } startWatcher(polling) { this.usePolling = polling; this.watcher = chokidar.watch(this.libraryPath, { ignoreInitial: true, ignored: (filePath) => { const rel = path_1.default.relative(this.libraryPath, filePath).replace(/\\/g, '/'); if (rel === '') return false; // shouldSync 返回 true 表示要同步 → chokidar 的 ignored 语义相反 return !this.shouldSync(rel); }, awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 200, }, persistent: false, depth: 10, usePolling: polling, interval: polling ? 3000 : undefined, binaryInterval: polling ? 3000 : undefined, }); this.watcher.on('add', (filePath) => this.handleNewFile(filePath)); this.watcher.on('unlink', (filePath) => this.handleUnlink(filePath)); this.watcher.on('error', (error) => { const msg = error instanceof Error ? error.message : String(error); if (msg.includes('ENOSPC') && !this.usePolling) { console.warn(`[Watcher] ENOSPC: inotify limit reached, falling back to polling for library ${this.libraryId}`); this.watcher.close(); this.startWatcher(true); this.initialSync(); return; } console.error(`[Watcher] Error for library ${this.libraryId}:`, error); }); console.log(`[Watcher] Started watching: ${this.libraryPath}${this.usePolling ? ' (polling)' : ''}`); } // 启动时扫描文件夹,把未入库的文件导入 async initialSync() { try { const files = await this.scanDir(this.libraryPath); let imported = 0; for (const filePath of files) { try { const stat = fs_1.default.statSync(filePath); if (stat.size === 0) continue; const rows = await this.libraryService.getSql('SELECT id FROM files WHERE path = ? LIMIT 1', [filePath]); if (rows.length > 0) continue; const folderId = await this.resolveFolder(filePath); const fileData = {}; if (folderId) fileData.folder_id = folderId; const result = await this.libraryService.createFileFromPath(filePath, fileData, { importType: 'link' }); imported++; this.webSocketServer.broadcastPluginEvent('file::created', { message: { type: 'file', action: 'create' }, result, libraryId: this.libraryId, }); this.webSocketServer.broadcastLibraryEvent(this.libraryId, 'file::created', { ...result, libraryId: this.libraryId, }); } catch (e) { console.error(`[Watcher] Initial sync failed for ${filePath}:`, e); } } if (imported > 0) { console.log(`[Watcher] Initial sync: imported ${imported} files`); this.webSocketServer.broadcastLibraryEvent(this.libraryId, 'file::synced', { libraryId: this.libraryId, imported, }); } } catch (e) { console.error(`[Watcher] Initial sync error:`, e); } } async scanDir(dir) { const results = []; const entries = fs_1.default.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path_1.default.join(dir, entry.name); const rel = path_1.default.relative(this.libraryPath, fullPath).replace(/\\/g, '/'); if (this.shouldIgnore(rel)) continue; if (entry.isDirectory()) { results.push(...await this.scanDir(fullPath)); } else if (entry.isFile()) { results.push(fullPath); } } return results; } /** 文件是否应被忽略(不参与扫描/同步)。规则来自 createSyncFilter()。 */ shouldIgnore(rel) { if (rel === '') return false; return !this.shouldSync(rel); } ignorePath(filePath) { this.ignoredPaths.add(filePath); setTimeout(() => this.ignoredPaths.delete(filePath), 10000); } isIgnored(filePath) { return this.ignoredPaths.has(filePath); } async stop() { if (this.watcher) { await this.watcher.close(); this.watcher = null; } // 清理 pending timers for (const [, entry] of this.pendingUnlinks) { clearTimeout(entry.timer); } this.pendingUnlinks.clear(); console.log(`[Watcher] Stopped for library ${this.libraryId}`); } // 文件删除/移走:暂存记录,若短时间内没有对应 add 则从 DB 删除 handleUnlink(filePath) { if (this.isIgnored(filePath)) return; this.libraryService.getSql('SELECT * FROM files WHERE path = ? LIMIT 1', [filePath]) .then((rows) => { if (rows.length === 0) return; const file = rows[0]; const timer = setTimeout(() => { this.pendingUnlinks.delete(filePath); // 超时未匹配到 add → 确认是删除,从 DB 移除 this.libraryService.deleteFile(file.id).catch((e) => { console.error(`[Watcher] Failed to delete record:`, e); }); this.webSocketServer.broadcastLibraryEvent(this.libraryId, 'file::deleted', { libraryId: this.libraryId, fileId: file.id, }); }, 3000); this.pendingUnlinks.set(filePath, { id: file.id, size: file.size, data: file, timer }); }) .catch((e) => console.error(`[Watcher] unlink lookup failed:`, e)); } async handleNewFile(filePath) { if (this.isIgnored(filePath)) return; try { const stat = fs_1.default.statSync(filePath); if (stat.size === 0) return; // 检查是否已在 DB 中 const existing = await this.libraryService.getSql('SELECT id FROM files WHERE path = ? LIMIT 1', [filePath]); if (existing.length > 0) return; // 检查是否是移动/重命名:匹配最近 unlink 的文件(按大小匹配) let moved; for (const [oldPath, entry] of this.pendingUnlinks) { if (entry.size === stat.size) { moved = entry; clearTimeout(entry.timer); this.pendingUnlinks.delete(oldPath); break; } } if (moved) { // 移动/重命名 → 更新已有记录 const folderId = await this.resolveFolder(filePath); const { oldData } = await this.libraryService.updateFile(moved.id, { path: filePath, name: path_1.default.basename(filePath), folder_id: folderId, }); console.log(`[Watcher] Moved: ${path_1.default.basename(filePath)}`); this.webSocketServer.broadcastLibraryEvent(this.libraryId, 'file::updated', { libraryId: this.libraryId, fileId: moved.id, path: filePath, name: path_1.default.basename(filePath), folder_id: folderId, old_data: oldData, }); } else { // 全新文件 → 导入 const folderId = await this.resolveFolder(filePath); const fileData = {}; if (folderId) fileData.folder_id = folderId; const result = await this.libraryService.createFileFromPath(filePath, fileData, { importType: 'link' }); console.log(`[Watcher] Imported: ${path_1.default.basename(filePath)}`); this.webSocketServer.broadcastPluginEvent('file::created', { message: { type: 'file', action: 'create' }, result, libraryId: this.libraryId, }); this.webSocketServer.broadcastLibraryEvent(this.libraryId, 'file::created', { ...result, libraryId: this.libraryId, }); } } catch (error) { console.error(`[Watcher] Failed to process ${filePath}:`, error); } } async resolveFolder(filePath) { const rel = path_1.default.relative(this.libraryPath, path_1.default.dirname(filePath)); if (!rel) return null; const parts = rel.replace(/\\/g, '/').split('/'); let parentId = null; for (const part of parts) { if (!part) continue; let folder = await this.libraryService.findFolderByName(part, parentId); if (!folder) { const id = await this.libraryService.createFolder({ title: part, parent_id: parentId, color: 0, icon: '', }); folder = { id }; } parentId = folder.id; } return parentId; } } exports.LibraryWatcher = LibraryWatcher; //# sourceMappingURL=LibraryWatcher.js.map