mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-30 17:15:16 +02:00
start optimizing with dir and stat
This commit is contained in:
parent
d7bffc2519
commit
042af27bb1
@ -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
|
|
||||||
} 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')
|
.split('\n')
|
||||||
.filter(line => !/^(?:\s*$|total)/.test(line)) // remove empty lines
|
.filter(name => name)
|
||||||
.map(record => {
|
.map(escaped => {
|
||||||
try {
|
try {
|
||||||
if (cwd !== props.path)
|
return JSON.parse(escaped);
|
||||||
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) {
|
} catch (error) {
|
||||||
notifications.value.constructNotification(`Error while gathering info for ${entry.path ?? record}`, errorStringHTML(error), 'error');
|
notifications.constructNotification("Failed to parse file name", `${errorStringHTML(error)}\ncaused by ${escaped}`, 'error');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(entry => entry !== null)
|
.filter(entry => entry !== null);
|
||||||
?? [];
|
const fields = [
|
||||||
|
'%n', // path
|
||||||
|
'%f', // mode (raw hex)
|
||||||
|
'%A', // modeStr
|
||||||
|
'%s', // size
|
||||||
|
'%U', // owner
|
||||||
|
'%G', // group
|
||||||
|
'%W', // ctime
|
||||||
|
'%Y', // mtime
|
||||||
|
'%X', // atime
|
||||||
|
'%F', // type
|
||||||
|
'%N', // quoted name with symlink
|
||||||
|
]
|
||||||
|
entries.value =
|
||||||
|
entryNames.length
|
||||||
|
? (await useSpawn(['stat', `--printf=${fields.join(US)}${RS}`, ...entryNames], { superuser: 'try', directory: cwd }).promise()).stdout
|
||||||
|
.split(RS)
|
||||||
|
.filter(record => record) // remove empty lines
|
||||||
|
.map(record => {
|
||||||
|
try {
|
||||||
|
let [name, mode, modeStr, size, owner, group, ctime, mtime, atime, type, symlinkStr] = record.split(US);
|
||||||
|
[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)
|
||||||
|
.then(() => {
|
||||||
emitStats();
|
emitStats();
|
||||||
sortEntries();
|
sortEntries();
|
||||||
}).finally(() => processingHandler.stop());
|
})
|
||||||
|
.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++;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user