create FileSystemWatcher function/object

This commit is contained in:
joshuaboud 2022-05-30 13:20:12 -03:00
parent 3802bcf1c7
commit e37d5b048d
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
2 changed files with 249 additions and 87 deletions

View File

@ -26,6 +26,7 @@ import DirectoryEntry from './DirectoryEntry.vue';
import getDirListing from '../functions/getDirListing';
import getDirEntryObjects from '../functions/getDirEntryObjects';
import { RECORD_SEPARATOR, UNIT_SEPARATOR } from '../constants';
import FileSystemWatcher from '../functions/fileSystemWatcher';
export default {
name: 'DirectoryEntryList',
@ -135,89 +136,6 @@ export default {
emit('stopProcessing');
}
}
const host = undefined;
let fsListChannel = null;
let fsListJobQueue = [];
const fsListCallback = async (event, data) => {
const eventObj = JSON.parse(data);
const entryName = eventObj.path.replace(props.path, '').replace(/^\//, '');
let generateJob = null;
switch (eventObj.event) {
case 'created':
generateJob = (entryName) => async () => {
console.log(eventObj.event, entryName);
const [entry] = await getDirEntryObjects(
[entryName],
props.path,
(message) => notifications.value.constructNotification("Failed to parse file name", message, 'error')
);
if (!entry)
return; // temp file deleted too fast
if (entry.type === 'symbolic link')
await processLinks([entry.target]);
entries.value = [...entries.value, reactive(entry)].sort(sortCallbackComputed.value);
}
break;
case 'attribute-changed':
case 'changed':
generateJob = (entryName) => async () => {
console.log(eventObj.event, entryName);
const entry = entries.value.find(entry => entry.name === entryName);
const [newContent] = await getDirEntryObjects([entryName], props.path);
if (entry) {
const attrsChanged = ["name", "owner", "group", "size", "ctime", "mtime", "atime"].map(key => String(entry[key]) !== String(newContent[key])).includes(true);
Object.assign(entry, newContent);
if (attrsChanged) sortEntries();
}
else
console.error("Failed to find entry for update", entryName);
}
break;
case 'deleted':
generateJob = (entryName) => async () => {
console.log(eventObj.event, entryName);
entries.value = entries.value.filter(entry => entry.name !== entryName);
}
break;
case 'present':
return;
default:
console.warn(eventObj.event, entryName, "(not handled)");
return;
}
fsListJobQueue.push(generateJob(entryName));
}
const fsListJobRunner = async () => {
while (true) {
while (fsListJobQueue.length) {
try {
await fsListJobQueue.shift()();
} catch (error) {
console.error("fslist1 job error", error);
}
}
await new Promise(resolve => setTimeout(resolve, 100));
}
}
fsListJobRunner();
const setUpChannel = () => {
fsListChannel = cockpit.channel({
payload: "fslist1",
command: "open",
path: props.path,
watch: true,
superuser: 'try',
host
});
fsListChannel.onmessage = fsListCallback;
}
const takeDownChannel = () => {
fsListChannel?.close?.();
}
const processLinks = (linkTargets) => {
if (linkTargets.length === 0)
@ -304,10 +222,54 @@ export default {
const entryFilterCallback = (entry) =>
(!/^\./.test(entry.name) || settings?.directoryView?.showHidden)
&& (props.searchFilterRegExp?.test(entry.name) ?? true);
const host = undefined;
const fileSystemWatcher = FileSystemWatcher(props.path, { superuser: 'try', host, ignoreSelf: true });
fileSystemWatcher.onCreated = async (eventObj) => {
const entryName = eventObj.path.replace(props.path, '').replace(/^\//, '');
console.log(eventObj.event, entryName);
const [entry] = await getDirEntryObjects(
[entryName],
props.path,
(message) => notifications.value.constructNotification("Failed to parse file name", message, 'error')
);
if (!entry)
return; // temp file deleted too fast
if (entry.type === 'symbolic link')
await processLinks([entry.target]);
entries.value = [...entries.value, reactive(entry)].sort(sortCallbackComputed.value);
}
fileSystemWatcher.onChanged = async (eventObj) => {
const entryName = eventObj.path.replace(props.path, '').replace(/^\//, '');
console.log(eventObj.event, entryName);
const entry = entries.value.find(entry => entry.name === entryName);
const [newContent] = await getDirEntryObjects([entryName], props.path);
if (entry) {
const attrsChanged = ["name", "owner", "group", "size", "ctime", "mtime", "atime"].map(key => String(entry[key]) !== String(newContent[key])).includes(true);
Object.assign(entry, newContent);
if (attrsChanged) sortEntries();
}
else
console.error("Failed to find entry for update", entryName);
}
fileSystemWatcher.onAttributeChanged = fileSystemWatcher.onChanged;
fileSystemWatcher.onDeleted = async (eventObj) => {
const entryName = eventObj.path.replace(props.path, '').replace(/^\//, '');
console.log(eventObj.event, entryName);
entries.value = entries.value.filter(entry => entry.name !== entryName);
}
fileSystemWatcher.onError = (error) => notifications.value.constructNotification("File System Watcher Error", errorStringHTML(error), 'error');
onBeforeUnmount(() => {
processingHandler.resolveDangling();
takeDownChannel();
fileSystemWatcher.stop();
});
watch(() => props.sortCallback, sortEntries);
@ -318,11 +280,9 @@ export default {
})
watch(() => props.path, (current, old) => {
takeDownChannel();
setUpChannel();
if (current === old)
return;
getEntries();
getEntries().then(() => fileSystemWatcher.path = current);
}, { immediate: true });
return {

View File

@ -0,0 +1,202 @@
import '../globalTypedefs';
/**
* Watch path or directory for changes. If path, changes are watch on all files in that directory.
*
* @param {String} path - Path to directory or file to watch
* @param {FileSystemWatchOptions} options - Options for cockpit.channel
* @param {Object} handlers - Callbacks for file system changes
* @property {FileSystemEventCallback} onCreated - callback for file Created
* @property {FileSystemEventCallback} onChanged - callback for file Changed
* @property {FileSystemEventCallback} onDeleted - callback for file Deleted
* @property {FileSystemEventCallback} onMoved - callback for file Moved
* @property {FileSystemEventCallback} onAttributeChanged - callback for file AttributeChanged
* @property {FileSystemEventCallback} onDoneHint - callback for file DoneHint
* @property {FileSystemEventCallback} onError - Error callback, defaults to logging to console.error
* @returns {FileSystemWatchObj} {@link FileSystemWatchObj}
*/
function FileSystemWatcher(path, options = {}, handlers = {}) {
let fsWatchChannel = null;
let fsWatchJobQueue = [];
let unhandledEventQueue = [];
let running = true;
const self = {
host: options.host ?? null,
path,
onCreated: handlers?.onCreated ?? null,
onChanged: handlers?.onChanged ?? null,
onDeleted: handlers?.onDeleted ?? null,
onMoved: handlers?.onMoved ?? null,
onAttributeChanged: handlers?.onAttributeChanged ?? null,
onDoneHint: handlers?.onDoneHint ?? null,
onError: handlers?.onError ?? (error => console.error(error)),
};
const handleEvent = (eventObj) => {
if (options.ignoreSelf && eventObj.path === self.path)
return;
switch (eventObj.event) {
case 'created':
if (self.onCreated)
fsWatchJobQueue.push(() => self.onCreated(eventObj));
else
unhandledEventQueue.push(eventObj);
break;
case 'attribute-changed':
if (self.onAttributeChanged)
fsWatchJobQueue.push(() => self.onAttributeChanged(eventObj));
else
unhandledEventQueue.push(eventObj);
break;
case 'changed':
if (self.onChanged)
fsWatchJobQueue.push(() => self.onChanged(eventObj));
else
unhandledEventQueue.push(eventObj);
break;
case 'deleted':
if (self.onDeleted)
fsWatchJobQueue.push(() => self.onDeleted(eventObj));
else
unhandledEventQueue.push(eventObj);
break;
case 'moved':
if (self.onMoved)
fsWatchJobQueue.push(() => self.onMoved(eventObj));
else
unhandledEventQueue.push(eventObj);
break;
case 'done-hint':
if (self.onDoneHint)
fsWatchJobQueue.push(() => self.onDoneHint(eventObj));
else
unhandledEventQueue.push(eventObj);
break;
default:
const error = new Error(`File System Watcher: ${eventObj.event} ${eventObj.path}: not handled internally`)
self.onError(error);
break;
}
}
/**
* Ensures all jobs from FsWatch1 are executed in order
*/
const fsWatchJobRunner = async () => {
while (running) {
while (fsWatchJobQueue.length) {
try {
await fsWatchJobQueue.shift()();
} catch (error) {
self.onError(error);
}
}
await new Promise(resolve => setTimeout(resolve, 100));
}
}
const setUpChannel = () => {
fsWatchChannel = cockpit.channel({
payload: "fswatch1",
command: "open",
watch: true,
superuser: options.superuser ?? 'try',
host: self.host,
path: self.path,
});
fsWatchChannel.onmessage = (event, data) => handleEvent(JSON.parse(data));
fsWatchChannel.onclose = (event, { problem, message }) => {
if (problem) {
const error = new Error(`File system watch error: ${message ?? problem}: Restarting watcher.`)
self.onError(error);
setUpChannel();
}
};
}
const takeDownChannel = () => {
fsWatchChannel?.close?.();
}
self.stop = () => {
running = false;
takeDownChannel();
};
fsWatchJobRunner(); // start runner
setUpChannel();
return new Proxy(self, {
get: (target, prop) => target[prop],
set: (target, prop, value) => {
let restartChannel = ((prop === 'path' || prop === 'host') && target[prop] !== value);
if (restartChannel) {
takeDownChannel();
}
target[prop] = value;
if (restartChannel) {
setUpChannel();
}
if (
[
'onCreated',
'onChanged',
'onDeleted',
'onMoved',
'onAttributeChanged',
].includes(prop)
) {
let tmpUnhandledEvents = [...unhandledEventQueue];
unhandledEventQueue = [];
while (tmpUnhandledEvents.length)
handleEvent(tmpUnhandledEvents.shift());
}
return true;
}
});
}
export default FileSystemWatcher;
/**
* Object controlling a file system watch
*
* @typedef {Object} FileSystemWatchObj
* @property {String} host - host to watch on, can be changed whenever and will reopen channel
* @property {String} path - path to watch, can be changed whenever and will reopen channel
* @property {FileSystemEventCallback} onCreated - callback for file Created
* @property {FileSystemEventCallback} onChanged - callback for file Changed
* @property {FileSystemEventCallback} onDeleted - callback for file Deleted
* @property {FileSystemEventCallback} onMoved - callback for file Moved
* @property {FileSystemEventCallback} onAttributeChanged - callback for file AttributeChanged
* @property {FileSystemEventCallback} onDoneHint - callback for file DoneHint
* @property {ErrorCallback} onError - Error callback, defaults to logging to console.error
* @property {Function} stop - Stop the file system watcher
*/
/**
* @typedef {Object} FileSystemEventObj
* @property {String} event - String describing what happened: "changed", "deleted", "created", "attribute-changed", "moved", or "done-hint"
* @property {String} path - Absolute path to file that has changed
* @property {String} other - The absolute path name of the other file in case of a "moved" event
* @property {String} type - If the event was created this contains the type of the new file. Will be one of: "file", "directory", "link", "special" or "unknown".
*/
/**
* @callback FileSystemEventCallback
* @param {FileSystemEventObj} eventObj - The object containing information about the file change
* @returns {Promise}
*/
/**
* @typedef {Object} FileSystemWatchOptions
* @property {String} host - Host to watch on
* @property {String} superuser - 'try' or 'require' or undefined
* @property {Boolean} ignoreSelf - Ignore changes for provided path (i.e. only watch sub-entries of directory)
*/