From 71b96b3f8b4cd13d14346aa2622d68db658f01b7 Mon Sep 17 00:00:00 2001 From: joshuaboud Date: Tue, 31 May 2022 17:18:12 -0300 Subject: [PATCH] use find instead of dir+stat --- navigator/src/components/DirectoryEntry.vue | 8 +- .../src/components/DirectoryEntryList.vue | 16 +- navigator/src/components/DirectoryView.vue | 2 +- navigator/src/functions/getDirEntryObjects.js | 162 +++++++++--------- 4 files changed, 94 insertions(+), 94 deletions(-) diff --git a/navigator/src/components/DirectoryEntry.vue b/navigator/src/components/DirectoryEntry.vue index 1a36c55..5426ab9 100644 --- a/navigator/src/components/DirectoryEntry.vue +++ b/navigator/src/components/DirectoryEntry.vue @@ -12,7 +12,7 @@
@@ -21,7 +21,7 @@
-
+
@@ -161,7 +161,7 @@ export default { } watch(props.entry, () => { - if (props.entry.type === 'directory' || (props.entry.type === 'symbolic link' && props.entry.target?.type === 'directory')) { + if (props.entry.type === 'd' || (props.entry.type === 'l' && props.entry.target?.type === 'd')) { icon.value = FolderIcon; directoryLike.value = true; } else { diff --git a/navigator/src/components/DirectoryEntryList.vue b/navigator/src/components/DirectoryEntryList.vue index 73aee6a..42ef157 100644 --- a/navigator/src/components/DirectoryEntryList.vue +++ b/navigator/src/components/DirectoryEntryList.vue @@ -23,7 +23,6 @@ import { ref, reactive, computed, inject, watch, onBeforeUnmount, onMounted } fr import { useSpawn, errorString, errorStringHTML, canonicalPath } from '@45drives/cockpit-helpers'; import { notificationsInjectionKey, settingsInjectionKey, clipboardInjectionKey } from '../keys'; import DirectoryEntry from './DirectoryEntry.vue'; -import getDirListing from '../functions/getDirListing'; import getDirEntryObjects from '../functions/getDirEntryObjects'; import FileSystemWatcher from '../functions/fileSystemWatcher'; @@ -61,13 +60,13 @@ export default { const sortCallbackComputed = computed(() => { return (a, b) => { if (settings.directoryView?.separateDirs) { - const checkA = a.type === 'symbolic link' ? (a.target?.type ?? null) : a.type; - const checkB = b.type === 'symbolic link' ? (b.target?.type ?? null) : b.type; + const checkA = a.type === 'l' ? (a.target?.type ?? null) : a.type; + const checkB = b.type === 'l' ? (b.target?.type ?? null) : b.type; if (checkA === null || checkB === null) return 0; - if (checkA === 'directory' && checkB !== 'directory') + if (checkA === 'd' && checkB !== 'd') return -1; - else if (checkA !== 'directory' && checkB === 'directory') + else if (checkA !== 'd' && checkB === 'd') return 1; } return props.sortCallback(a, b); @@ -152,10 +151,10 @@ export default { processingHandler.start(); try { const cwd = props.path; - const entryNames = await getDirListing(cwd, props.host, (message) => notifications.value.constructNotification("Failed to parse file name", message, 'error')); + // const entryNames = await getDirListing(cwd, props.host, (message) => notifications.value.constructNotification("Failed to parse file name", message, 'error')); + console.time('getEntries'); const tmpEntries = ( await getDirEntryObjects( - entryNames, cwd, props.host, (message) => notifications.value.constructNotification("Failed to parse file name", message, 'error') @@ -164,6 +163,7 @@ export default { if (props.path !== cwd) return; // changed directory before could finish entries.value = [...tmpEntries.sort(sortCallbackComputed.value)].map(entry => reactive({...entry, cut: clipboard.content.find(a => a.path === entry.path && a.host === entry.host) ?? false})); + console.timeEnd('getEntries'); } catch (error) { entries.value = []; notifications.value.constructNotification("Error getting directory entries", errorStringHTML(error), 'error'); @@ -184,7 +184,7 @@ export default { const emitStats = () => { emit('updateStats', visibleEntries.value.reduce((stats, entry) => { - if (entry.type === 'directory' || (entry.type === 'symbolic link' && entry.target?.type === 'directory')) + if (entry.type === 'd' || (entry.type === 'l' && entry.target?.type === 'd')) stats.dirs++; else stats.files++; diff --git a/navigator/src/components/DirectoryView.vue b/navigator/src/components/DirectoryView.vue index b303957..abe5211 100644 --- a/navigator/src/components/DirectoryView.vue +++ b/navigator/src/components/DirectoryView.vue @@ -142,7 +142,7 @@ export default { let destination; if (selected.length === 1) { destination = selected[0]; - if (destination.type !== 'directory' && !(destination.type === 'symbolic link' && destination.target.type === 'directory')) { + if (destination.type !== 'd' && !(destination.type === 'l' && destination.target.type === 'd')) { notifications.value.constructNotification("Paste Failed", 'Cannot paste to non-directory.', 'error'); return; } diff --git a/navigator/src/functions/getDirEntryObjects.js b/navigator/src/functions/getDirEntryObjects.js index 247e3ba..6626dd8 100644 --- a/navigator/src/functions/getDirEntryObjects.js +++ b/navigator/src/functions/getDirEntryObjects.js @@ -1,97 +1,98 @@ import { useSpawn, errorString } from "@45drives/cockpit-helpers"; import { UNIT_SEPARATOR, RECORD_SEPARATOR } from "../constants"; -async function processLinks(linkTargets, host) { - if (linkTargets.length === 0) - return; - ( - await useSpawn( - // Separate by newline so errors will slot in - ['stat', `--printf=%F${UNIT_SEPARATOR}%f\n`, ...linkTargets.map(target => target.path)], - { superuser: 'try', err: 'out', host } // stderr >&stdout for maintaining index order - ).promise() - .catch(state => state) // ignore errors, error message will maintain index order - ).stdout - .trim() - .split('\n') - .filter(record => record) - .map((record, index) => { - if (record.includes(UNIT_SEPARATOR)) { - const [type, mode] = record.split(UNIT_SEPARATOR); - linkTargets[index].type = type; - linkTargets[index].mode = mode; - linkTargets[index].broken = false; - } else { // error - linkTargets[index].broken = true; - } - }); -} - /** * Get list of directory entry objects from list of directory entry names * - * @param {String[]} dirListing - List of entry names - * @param {String} cwd - Working directory to run stat in - * @param {String} host - Host to run stat on + * find -H path -maxdepth 1 -mindepth 1 -printf '%f:%m:%M:%s:%u:%g:%B@:%T@:%A@:%y:%Y:%l\n' + * + * @param {String} cwd - Working directory to run find in + * @param {String} host - Host to run find on * @param {getDirEntryObjectsFailCallback} failCallback - Callback function for handling errors, receives {String} message * @param {ByteFormatter} byteFormatter - Function to format bytes * @returns {Promise} Array of DirectoryEntryObj objects */ -async function getDirEntryObjects(dirListing, cwd, host, failCallback, byteFormatter = cockpit.format_bytes) { +async function getDirEntryObjects(cwd, host, failCallback, byteFormatter = cockpit.format_bytes) { const fields = [ - '%n', // path - '%f', // mode (raw hex) - '%A', // modeStr + '%f', // name + '%p', // full path + '%m', // mode (octal) + '%M', // modeStr '%s', // size - '%U', // owner - '%G', // group - '%W', // ctime - '%Y', // mtime - '%X', // atime - '%F', // type - '%N', // quoted name with symlink + '%u', // owner + '%g', // group + '%B@', // ctime + '%T@', // mtime + '%A@', // atime + '%y', // type + '%Y', // symlink target type or type if not symlink + '%l', // symlink target name or '' if not symlink ] - const entries = dirListing.length - ? parseRawEntryStats( - ( - await useSpawn([ - 'stat', - `--printf=${fields.join(UNIT_SEPARATOR)}${RECORD_SEPARATOR}`, - ...dirListing - ], { superuser: 'try', directory: cwd, host } - ) - .promise() - .catch(state => state) // ignore errors - ).stdout, cwd, host, failCallback, byteFormatter) - : []; - await processLinks(entries.filter(entry => entry.type === 'symbolic link').map(entry => entry.target), host); - return entries; + return parseRawEntryStats( + await getDirEntryStats(cwd, host, fields, [], false), + cwd, + host, + failCallback, + byteFormatter + ) } /** - * Parse raw output of `stat` call from {@link getDirEntryObjects()} * - * @param {String} raw - Raw output of `stat` call from {@link getDirEntryObjects()} - * @param {String} cwd - Path to working directory to run stat in - * @param {String} host - Host to run stat on + * @param {String} cwd - Starting point for find + * @param {String} host - Host to run find on + * @param {String[]} outputFormat - -printf format fields array (man find(1)) + * @param {String[]} extraFindArguments - extra tests and actions to run on files + * @param {Boolean} recursive - if false, -maxdepth is 1 + * @returns {Promise} Array of resultant output + */ +async function getDirEntryStats(cwd, host, outputFormat, extraFindArguments = [], recursive = false) { + const UNIT_SEPARATOR_ESC = `\\${UNIT_SEPARATOR.charCodeAt(0).toString(8).padStart(3, '0')}`; + const RECORD_SEPARATOR_ESC = `\\${RECORD_SEPARATOR.charCodeAt(0).toString(8).padStart(3, '0')}`; + const argv = [ + 'find', + '-H', + cwd, + '-mindepth', + '1', + ]; + if (!recursive) + argv.push('-maxdepth', '1'); + argv.push(...extraFindArguments); + if (outputFormat.length) + argv.push('-printf', `${outputFormat.join(UNIT_SEPARATOR_ESC)}${RECORD_SEPARATOR_ESC}`); + return new TextDecoder().decode( + ( // make sure any possible nul bytes don't break cockpit's protocol by using binary + await useSpawn( + argv, + { superuser: 'try', host, binary: true } + ).promise() + ).stdout + ).split(RECORD_SEPARATOR) + .slice(0, -1) // remove last empty array element from split since all entries end with RECORD_SEPARATOR + .map(record => record.split(UNIT_SEPARATOR)); +} + +/** + * Parse raw output of `find` call from {@link getDirEntryObjects()} + * + * @param {String[][]} records - Raw output of `find` call from {@link getDirEntryObjects()} + * @param {String} cwd - Path to working directory that find was ran in + * @param {String} host - Host that find was ran on * @param {getDirEntryObjectsFailCallback} failCallback - Callback function for handling errors, receives {String} message * @param {ByteFormatter} byteFormatter - Function to format bytes * @returns {DirectoryEntryObj[]} */ -function parseRawEntryStats(raw, cwd, host, failCallback, byteFormatter = cockpit.format_bytes) { - const UNIT_SEPARATOR = '\x1F'; // "Unit Separator" - ASCII field delimiter - const RECORD_SEPARATOR = '\x1E'; // "Record Separator" - ASCII record delimiter - return raw.split(RECORD_SEPARATOR) - .filter(record => record) // remove empty lines - .map(record => { +function parseRawEntryStats(records, cwd, host, failCallback, byteFormatter = cockpit.format_bytes) { + return records.map(fields => { try { - let [name, mode, modeStr, size, owner, group, ctime, mtime, atime, type, symlinkStr] = record.split(UNIT_SEPARATOR); + let [name, path, mode, modeStr, size, owner, group, ctime, mtime, atime, type, symlinkTargetType, symlinkTargetName] = fields; [size, ctime, mtime, atime] = [size, ctime, mtime, atime].map(num => parseInt(num)); [ctime, mtime, atime] = [ctime, mtime, atime].map(ts => ts ? new Date(ts * 1000) : null); - mode = parseInt(mode, 16); - const entry = { + mode = parseInt(mode, 8); + return { name, - path: `${cwd}/${name}`.replace(/\/+/g, '/'), + path, mode, modeStr, size, @@ -102,26 +103,24 @@ function parseRawEntryStats(raw, cwd, host, failCallback, byteFormatter = cockpi mtime, atime, type, - target: {}, + target: { + type: symlinkTargetType, + rawPath: symlinkTargetName, + path: type === 'l' ? symlinkTargetName.replace(/^(?!\/)/, `${cwd}/`) : '', + broken: ['L', 'N', '?'].includes(symlinkTargetType), // L: loop N: nonexistent ?: error + }, selected: false, host, cut: false, }; - if (type === 'symbolic link') { - entry.target.rawPath = [ - ...symlinkStr.split(/\s*->\s*/)[1].trim().matchAll(/\$?'([^']+)'/g) - ].map(group => JSON.parse(`"${group[1]}"`)).join(''); - entry.target.path = entry.target.rawPath.replace(/^(?!\/)/, `${cwd}/`); - } - return entry; } catch (error) { - failCallback(errorString(error) + `\ncaused by: ${record}`); + failCallback(errorString(error) + `\ncaused by: ${fields.toString()}`); return null; } }).filter(entry => entry !== null) } -export { parseRawEntryStats }; +export { getDirEntryObjects, getDirEntryStats, parseRawEntryStats }; export default getDirEntryObjects; @@ -155,10 +154,11 @@ export default getDirEntryObjects; * @property {Date} ctime - Creation time * @property {Date} mtime - Last Modified time * @property {Date} atime - Last Accessed time - * @property {String} type - Type of inode returned by stat + * @property {String} type - Type of inode returned by find * @property {Object} target - Object for symlink target - * @property {String} target.rawPath - Symlink target path directly grabbed from stat + * @property {String} target.rawPath - Symlink target path directly grabbed from find * @property {String} target.path - Resolved symlink target path + * @property {Boolean} target.broken - Whether or not the link is broken * @property {Boolean} selected - Whether or not the user has selected this entry in the browser * @property {String} host - host that owns entry * @property {Boolean} cut - whether or not the file is going to be cut