mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-31 01:24:37 +02:00
create FileSystemWatcher function/object
This commit is contained in:
parent
3802bcf1c7
commit
e37d5b048d
@ -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 {
|
||||
|
202
navigator/src/functions/fileSystemWatcher.js
Normal file
202
navigator/src/functions/fileSystemWatcher.js
Normal 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)
|
||||
*/
|
Loading…
x
Reference in New Issue
Block a user