Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
119 lines
3.6 KiB
JavaScript
119 lines
3.6 KiB
JavaScript
const { EventEmitter } = require('events');
|
|
const path = require('path');
|
|
const chokidar = require('chokidar');
|
|
|
|
class FolderMonitor extends EventEmitter {
|
|
constructor({ watch = chokidar.watch } = {}) {
|
|
super();
|
|
this._watch = watch;
|
|
this._watcher = null;
|
|
this._settings = null;
|
|
this._seenFiles = new Set();
|
|
this._batchBuffer = [];
|
|
this._batchTimer = null;
|
|
this._initialScopes = new Set();
|
|
}
|
|
|
|
get running() {
|
|
return !!this._watcher;
|
|
}
|
|
|
|
start(settings) {
|
|
this.stop();
|
|
this._settings = settings;
|
|
|
|
const folderPath = String(settings.folderPath || '').trim();
|
|
if (!folderPath) throw new Error('Kein Ordnerpfad angegeben');
|
|
|
|
const scope = JSON.stringify([
|
|
path.resolve(folderPath).toLowerCase(),
|
|
!!settings.recursive,
|
|
String(settings.filterMode || 'include'),
|
|
String(settings.extensions || '').trim().toLowerCase()
|
|
]);
|
|
const includeInitial = !!settings.includeExisting && !this._initialScopes.has(scope);
|
|
if (includeInitial) this._initialScopes.add(scope);
|
|
|
|
const watchOptions = {
|
|
persistent: true,
|
|
ignoreInitial: !includeInitial,
|
|
depth: settings.recursive ? undefined : 0,
|
|
awaitWriteFinish: {
|
|
stabilityThreshold: Math.max(1000, (settings.delaySec || 3) * 1000),
|
|
pollInterval: 500
|
|
}
|
|
};
|
|
|
|
this._watcher = this._watch(folderPath, watchOptions);
|
|
if (includeInitial) {
|
|
this._watcher.once('ready', () => this.emit('initial-scan-complete'));
|
|
}
|
|
this._watcher.on('add', (filePath) => this._onNewFile(filePath));
|
|
this._watcher.on('unlink', (filePath) => {
|
|
// Allow re-added files (e.g. re-encoded) to be detected again
|
|
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
this._seenFiles.delete(normalized);
|
|
});
|
|
this._watcher.on('error', (err) => this.emit('error', err));
|
|
return { includesExisting: includeInitial };
|
|
}
|
|
|
|
stop() {
|
|
if (this._watcher) {
|
|
this._watcher.close().catch(() => {});
|
|
this._watcher = null;
|
|
}
|
|
if (this._batchTimer) {
|
|
clearTimeout(this._batchTimer);
|
|
this._batchTimer = null;
|
|
}
|
|
this._batchBuffer = [];
|
|
this._seenFiles = new Set();
|
|
}
|
|
|
|
status() {
|
|
return {
|
|
running: this.running,
|
|
folderPath: this._settings ? this._settings.folderPath : '',
|
|
seenCount: this._seenFiles.size
|
|
};
|
|
}
|
|
|
|
_onNewFile(filePath) {
|
|
const settings = this._settings;
|
|
if (!settings) return;
|
|
|
|
// Extension filter
|
|
const ext = path.extname(filePath).replace(/^\./, '').toLowerCase();
|
|
const rawExtensions = String(settings.extensions || '').trim();
|
|
if (rawExtensions) {
|
|
const extList = rawExtensions.split(',').map(e => e.trim().toLowerCase().replace(/^\./, '')).filter(Boolean);
|
|
if (extList.length > 0) {
|
|
const matches = extList.includes(ext);
|
|
if (settings.filterMode === 'include' && !matches) return;
|
|
if (settings.filterMode === 'exclude' && matches) return;
|
|
}
|
|
}
|
|
|
|
// Skip duplicates (session-based)
|
|
if (settings.skipDuplicates) {
|
|
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
if (this._seenFiles.has(normalized)) return;
|
|
this._seenFiles.add(normalized);
|
|
}
|
|
|
|
// Batch: collect files over 200ms window then emit together
|
|
this._batchBuffer.push(filePath);
|
|
if (this._batchTimer) clearTimeout(this._batchTimer);
|
|
this._batchTimer = setTimeout(() => {
|
|
const files = this._batchBuffer.splice(0);
|
|
this._batchTimer = null;
|
|
if (files.length > 0) {
|
|
this.emit('new-files', files);
|
|
}
|
|
}, 200);
|
|
}
|
|
}
|
|
|
|
module.exports = FolderMonitor;
|