mira-app-server
Version:
Mira Server - standalone server application using mira-app-core
301 lines • 12.9 kB
JavaScript
"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