start optimizing with dir and stat

This commit is contained in:
joshuaboud 2022-05-17 12:57:41 -03:00
parent d7bffc2519
commit 042af27bb1
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E

View File

@ -59,8 +59,8 @@ export default {
const sortCallbackComputed = computed(() => { const sortCallbackComputed = computed(() => {
return (a, b) => { return (a, b) => {
if (settings.directoryView?.separateDirs) { if (settings.directoryView?.separateDirs) {
const checkA = a.type === 'link' ? (a.target?.type ?? null) : a.type; const checkA = a.type === 'symbolic link' ? (a.target?.type ?? null) : a.type;
const checkB = b.type === 'link' ? (b.target?.type ?? null) : b.type; const checkB = b.type === 'symbolic link' ? (b.target?.type ?? null) : b.type;
if (checkA === null || checkB === null) if (checkA === null || checkB === null)
return 0; return 0;
if (checkA === 'directory' && checkB !== 'directory') if (checkA === 'directory' && checkB !== 'directory')
@ -89,204 +89,287 @@ export default {
} }
} }
const parseModeStr = (cwd, entry, modeStr, linkTargetRaw = null) => { // const parseModeStr = (cwd, entry, modeStr, linkTargetRaw = null) => {
const procs = []; // const procs = [];
Object.assign(entry, { // Object.assign(entry, {
permissions: { // permissions: {
owner: { // owner: {
read: modeStr[1] !== '-', // read: modeStr[1] !== '-',
write: modeStr[2] !== '-', // write: modeStr[2] !== '-',
execute: modeStr[3] !== '-', // execute: modeStr[3] !== '-',
}, // },
group: { // group: {
read: modeStr[4] !== '-', // read: modeStr[4] !== '-',
write: modeStr[5] !== '-', // write: modeStr[5] !== '-',
execute: modeStr[6] !== '-', // execute: modeStr[6] !== '-',
}, // },
other: { // other: {
read: modeStr[7] !== '-', // read: modeStr[7] !== '-',
write: modeStr[8] !== '-', // write: modeStr[8] !== '-',
execute: modeStr[9] !== '-', // execute: modeStr[9] !== '-',
}, // },
acl: modeStr[10] === '+' ? {} : null, // acl: modeStr[10] === '+' ? {} : null,
} // }
}); // });
switch (modeStr[0]) { // switch (modeStr[0]) {
case 'd': // case 'd':
entry.type = 'directory'; // entry.type = 'directory';
break; // break;
case '-': // case '-':
entry.type = 'file'; // entry.type = 'file';
break; // break;
case 'p': // case 'p':
entry.type = 'pipe'; // entry.type = 'pipe';
break; // break;
case 'l': // case 'l':
entry.type = 'link'; // entry.type = 'link';
if (linkTargetRaw) { // if (linkTargetRaw) {
entry.target = { // entry.target = {
rawPath: linkTargetRaw, // rawPath: linkTargetRaw,
path: canonicalPath(linkTargetRaw.replace(/^(?!\/)/, cwd + '/')), // path: canonicalPath(linkTargetRaw.replace(/^(?!\/)/, cwd + '/')),
}; // };
procs.push( // procs.push(
useSpawn(['stat', '-c', '%A', entry.target.path]).promise() // useSpawn(['stat', '-c', '%A', entry.target.path]).promise()
.then(state => { // .then(state => {
parseModeStr(cwd, entry.target, state.stdout.trim()); // parseModeStr(cwd, entry.target, state.stdout.trim());
entry.target.broken = false; // entry.target.broken = false;
}) // })
.catch(() => { // .catch(() => {
entry.target.broken = true; // entry.target.broken = true;
}) // })
); // );
} // }
break; // break;
case 's': // case 's':
entry.type = 'socket'; // entry.type = 'socket';
break; // break;
case 'c': // case 'c':
entry.type = 'character'; // entry.type = 'character';
break; // break;
case 'b': // case 'b':
entry.type = 'block'; // entry.type = 'block';
break; // break;
default: // default:
entry.type = 'unk'; // entry.type = 'unk';
break; // break;
} // }
if (entry.permissions.acl && entry.rawPath === undefined) { // skip for link targets // if (entry.permissions.acl && entry.rawPath === undefined) { // skip for link targets
procs.push(useSpawn(['getfacl', '--omit-header', '--no-effective', entry.path], { superuser: 'try' }).promise() // procs.push(useSpawn(['getfacl', '--omit-header', '--no-effective', entry.path], { superuser: 'try' }).promise()
.then(state => { // .then(state => {
entry.permissions.acl = state.stdout // entry.permissions.acl = state.stdout
.split('\n') // .split('\n')
.filter(line => line && !/^\s*(?:#|$)/.test(line)) // .filter(line => line && !/^\s*(?:#|$)/.test(line))
.reduce((acl, line) => { // .reduce((acl, line) => {
const match = line.match(/^([^:]*):([^:]+)?:(.*)$/).slice(1); // const match = line.match(/^([^:]*):([^:]+)?:(.*)$/).slice(1);
acl[match[0]] = acl[match[0]] ?? {}; // acl[match[0]] = acl[match[0]] ?? {};
acl[match[0]][match[1] ?? '*'] = { // acl[match[0]][match[1] ?? '*'] = {
r: match[2][0] !== '-', // r: match[2][0] !== '-',
w: match[2][1] !== '-', // w: match[2][1] !== '-',
x: match[2][2] !== '-', // x: match[2][2] !== '-',
} // }
return acl; // return acl;
}, {}); // }, {});
}) // })
.catch(state => { // .catch(state => {
console.error(`failed to get ACL for ${entry.path}:`, errorString(state)); // console.error(`failed to get ACL for ${entry.path}:`, errorString(state));
}) // })
); // );
} // }
return Promise.all(procs); // return Promise.all(procs);
} // }
const getAsyncEntryStats = () => { // const getAsyncEntryStats = () => {
const callback = (state, resolver) => { // const callback = (state, resolver) => {
state.stdout.trim().split('\n') // state.stdout.trim().split('\n')
.map(line => { // .map(line => {
try { // try {
// birth:modification:access // // birth:modification:access
const [path, ctime, mtime, atime] = line.trim().split(':') // const [path, ctime, mtime, atime] = line.trim().split(':')
.map(str => isNaN(Number(str)) ? str : Number(str)) // .map(str => isNaN(Number(str)) ? str : Number(str))
.map(ts => typeof ts === 'number' ? (ts ? new Date(ts * 1000) : null) : ts); // .map(ts => typeof ts === 'number' ? (ts ? new Date(ts * 1000) : null) : ts);
return { // return {
path, // path,
result: { // result: {
ctime, // ctime,
mtime, // mtime,
atime, // atime,
} // }
} // }
} catch (error) { // } catch (error) {
console.error(error); // console.error(error);
return { // return {
path: "", // path: "",
result: { // result: {
ctime: null, // ctime: null,
mtime: null, // mtime: null,
atime: null, // atime: null,
} // }
} // }
} // }
}) // })
.map(({ path, result: metadata }, index) => { // .map(({ path, result: metadata }, index) => {
let target = entries.value[index]; // let target = entries.value[index];
if (!target || target.path !== path) { // if (!target || target.path !== path) {
console.error(`Had to reverse lookup entry for ${path}, index did not match`); // console.error(`Had to reverse lookup entry for ${path}, index did not match`);
target = entries.value.find(entry => entry.path === path); // target = entries.value.find(entry => entry.path === path);
} // }
if (!target) { // if (!target) {
console.error(`Could not reverse lookup ${path} to assign stats`); // console.error(`Could not reverse lookup ${path} to assign stats`);
} else { // } else {
Object.assign(target, metadata) // Object.assign(target, metadata)
} // }
}); // });
resolver(); // resolver();
} // }
return new Promise((resolve, reject) => { // return new Promise((resolve, reject) => {
useSpawn(['stat', '-c', '%n:%W:%Y:%X', '--', ...entries.value.map(({ path }) => path)], { superuser: 'try', err: 'out' }).promise() // useSpawn(['stat', '-c', '%n:%W:%Y:%X', '--', ...entries.value.map(({ path }) => path)], { superuser: 'try', err: 'out' }).promise()
.then(state => callback(state, resolve)) // .then(state => callback(state, resolve))
.catch(state => callback(state, resolve)); // ignore errors to keep list order, err:out for stderr as placeholder // .catch(state => callback(state, resolve)); // ignore errors to keep list order, err:out for stderr as placeholder
}); // });
} // }
const getEntries = async () => { const getEntries = async () => {
processingHandler.start(); processingHandler.start();
const readLink = (target, cwd, symlinkStr) => {
return new Promise((resolve, reject) => {
const linkTargetRaw = symlinkStr.split(/\s*->\s*/)[1].trim().replace(/^['"]|['"]$/g, '');
Object.assign(target, {
rawPath: linkTargetRaw,
path: canonicalPath(linkTargetRaw.replace(/^(?!\/)/, `${cwd}/`)),
});
useSpawn(['stat', '-c', '%F', target.path], { superuser: 'try' }).promise()
.then(state => {
target.type = state.stdout.trim();
target.broken = false;
})
.catch(() => {
target.broken = true;
})
.finally(resolve);
})
}
const US = '\x1F';
const RS = '\x1E';
try { try {
const cwd = props.path; const cwd = props.path;
const procs = []; const procs = [];
procs.push(entryRefs.value.map(entryRef => entryRef.getEntries())); procs.push(...entryRefs.value.filter(entryRef => entryRef.showEntries).map(entryRef => entryRef.getEntries()));
let lsOutput; const entryNames =
try { (await useSpawn(['dir', '--almost-all', '--dereference-command-line-symlink-to-dir', '--quoting-style=c', '-1', cwd], { superuser: 'try' }).promise()).stdout
lsOutput = (await useSpawn(['ls', '-al', '--color=never', '--time-style=full-iso', '--quote-name', '--dereference-command-line-symlink-to-dir', cwd], { superuser: 'try' }).promise()).stdout .split('\n')
} catch (state) { .filter(name => name)
if (state.exit_code === 1) .map(escaped => {
lsOutput = state.stdout; // non-fatal ls error try {
else return JSON.parse(escaped);
throw new Error(state.stderr); } catch (error) {
} notifications.constructNotification("Failed to parse file name", `${errorStringHTML(error)}\ncaused by ${escaped}`, 'error');
entries.value = lsOutput
.split('\n')
.filter(line => !/^(?:\s*$|total)/.test(line)) // remove empty lines
.map(record => {
try {
if (cwd !== props.path)
return null;
const entry = reactive({});
const fields = record.match(/^([a-z-]+\+?)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+(?:,\s+\d+)?)\s+([^"]+)"([^"]+)"(?:\s+->\s+"([^"]+)")?/)?.slice(1);
if (!fields) {
console.error('regex failed to match on', record);
return null; return null;
} }
entry.name = fields[6]; })
if (entry.name === '.' || entry.name === '..') .filter(entry => entry !== null);
return null; const fields = [
entry.path = canonicalPath(cwd + `/${entry.name}`); '%n', // path
entry.modeStr = fields[0]; '%f', // mode (raw hex)
entry.hardlinkCount = parseInt(fields[1]); '%A', // modeStr
entry.owner = fields[2]; '%s', // size
entry.group = fields[3]; '%U', // owner
if (/,/.test(fields[4])) { '%G', // group
[entry.major, entry.minor] = fields[4].split(/,\s+/); '%W', // ctime
entry.size = null; '%Y', // mtime
} else { '%X', // atime
entry.size = parseInt(fields[4]); '%F', // type
entry.sizeHuman = cockpit.format_bytes(entry.size, 1000).replace(/(?<!B)$/, ' B'); '%N', // quoted name with symlink
entry.major = entry.minor = null; ]
} entries.value =
procs.push(parseModeStr(cwd, entry, entry.modeStr, fields[7])); entryNames.length
return entry; ? (await useSpawn(['stat', `--printf=${fields.join(US)}${RS}`, ...entryNames], { superuser: 'try', directory: cwd }).promise()).stdout
} catch (error) { .split(RS)
notifications.value.constructNotification(`Error while gathering info for ${entry.path ?? record}`, errorStringHTML(error), 'error'); .filter(record => record) // remove empty lines
return null; .map(record => {
} try {
}) let [name, mode, modeStr, size, owner, group, ctime, mtime, atime, type, symlinkStr] = record.split(US);
.filter(entry => entry !== null) [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 = reactive({
name,
path: canonicalPath(`/${cwd}/${name}`),
mode,
modeStr,
size,
sizeHuman: cockpit.format_bytes(size, 1000).replace(/(?<!B)$/, ' B'),
owner,
group,
ctime,
mtime,
atime,
type,
target: {},
});
if (type === 'symbolic link')
procs.push(readLink(entry.target, cwd, symlinkStr));
return entry;
} catch (error) {
console.error(errorString(error));
return null;
}
}).filter(entry => entry !== null)
: [];
// let lsOutput;
// try {
// lsOutput = (await useSpawn(['dir', '-al', '--color=never', '--time-style=full-iso', '--quote-name', '--dereference-command-line-symlink-to-dir', cwd], { superuser: 'try' }).promise()).stdout
// } catch (state) {
// if (state.exit_code === 1)
// lsOutput = state.stdout; // non-fatal ls error
// else
// throw new Error(state.stderr);
// }
// entries.value = lsOutput
// .split('\n')
// .filter(line => !/^(?:\s*$|total)/.test(line)) // remove empty lines
// .map(record => {
// try {
// if (cwd !== props.path)
// return null;
// const entry = reactive({});
// const fields = record.match(/^([a-z-]+\+?)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+(?:,\s+\d+)?)\s+([^"]+)"([^"]+)"(?:\s+->\s+"([^"]+)")?/)?.slice(1);
// if (!fields) {
// console.error('regex failed to match on', record);
// return null;
// }
// entry.name = fields[6];
// if (entry.name === '.' || entry.name === '..')
// return null;
// entry.path = canonicalPath(cwd + `/${entry.name}`);
// entry.modeStr = fields[0];
// entry.hardlinkCount = parseInt(fields[1]);
// entry.owner = fields[2];
// entry.group = fields[3];
// if (/,/.test(fields[4])) {
// [entry.major, entry.minor] = fields[4].split(/,\s+/);
// entry.size = null;
// } else {
// entry.size = parseInt(fields[4]);
// entry.sizeHuman = cockpit.format_bytes(entry.size, 1000).replace(/(?<!B)$/, ' B');
// entry.major = entry.minor = null;
// }
// procs.push(parseModeStr(cwd, entry, entry.modeStr, fields[7]));
// return entry;
// } catch (error) {
// notifications.value.constructNotification(`Error while gathering info for ${entry.path ?? record}`, errorStringHTML(error), 'error');
// return null;
// }
// })
// .filter(entry => entry !== null)
// ?? [];
processingHandler.start(); processingHandler.start();
procs.push(getAsyncEntryStats()); console.log("resolving", procs.length, 'symlinks');
return Promise.all(procs).then(() => { return Promise.all(procs)
emitStats(); .then(() => {
sortEntries(); emitStats();
}).finally(() => processingHandler.stop()); sortEntries();
})
.finally(() => processingHandler.stop());
} catch (error) { } catch (error) {
entries.value = []; entries.value = [];
notifications.value.constructNotification("Error getting directory entries", errorStringHTML(error), 'error'); notifications.value.constructNotification("Error getting directory entries", errorStringHTML(error), 'error');
@ -297,7 +380,7 @@ export default {
const emitStats = () => { const emitStats = () => {
emit('updateStats', entries.value.reduce((stats, entry) => { emit('updateStats', entries.value.reduce((stats, entry) => {
if (entry.type === 'directory' || (entry.type === 'link' && entry.target?.type === 'directory')) if (entry.type === 'directory' || (entry.type === 'symbolic link' && entry.target?.type === 'directory'))
stats.dirs++; stats.dirs++;
else else
stats.files++; stats.files++;