Skip to content

Commit

Permalink
Use cockpit's new 'fsinfo' channel
Browse files Browse the repository at this point in the history
Substantially rework navigator to be based on the new 'fsinfo' channel
provided by Cockpit.

This new channel is only on the Python bridge, and has yet to appear in
a released version of Cockpit, so (unconditionally) re-engage our
existing code to install a version of the wheel from cockpit git.

We also temporarily host some code here (fsinfo.ts) which will
eventually make its way into cockpit.js.

In addition to being a simplification of the code, this *dramatically*
improves the performance of cockpit-navigator, making it possible to
open large directories, such as /usr/lib64.  Some quick measurements on
that directory show that it takes only 58ms until we get the full set of
data from the channel and convert it into the list of files for the
navigator.  From there, the performance of React isn't excellent, but we
can fix that in the future using something like `react-windowed` or
PatternFly's `react-virtualized-extension`.

This is a very rough first pass which tried to avoid touching too much
unrelated code, but despite that, there were quite some changes (since
the use of the old data model reached deep into various corners of the
codes, and they needed to be updated).

Closes: cockpit-project#137
  • Loading branch information
allisonkarlitskaya committed Jan 30, 2024
1 parent 67d580b commit 78ec505
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 237 deletions.
25 changes: 6 additions & 19 deletions src/apis/spawnHelpers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const spawnRenameItem = ({ selected, name, path, Dialogs, setErrorMessage

export const spawnCreateDirectory = ({ name, currentPath, selected, Dialogs, setErrorMessage }) => {
let path;
if (selected.icons_cnt || selected.type === "directory") {
if (selected.icons_cnt || selected.type === "dir") {
path = currentPath + selected.name + "/" + name;
} else {
path = currentPath + name;
Expand All @@ -112,31 +112,18 @@ export const spawnCreateLink = ({ type, currentPath, originalName, newName, Dial
.then(Dialogs.close, (err) => { setErrorMessage(err.message) });
};

// eslint-disable-next-line max-len
export const spawnEditPermissions = ({ ownerAccess, groupAccess, otherAccess, path, selected, owner, group, Dialogs, setErrorMessage }) => {
const command = [
"chmod",
ownerAccess + groupAccess + otherAccess,
path.join("/") + "/" + selected.name
];
const permissionChanged = (
ownerAccess !== selected.permissions[0] ||
groupAccess !== selected.permissions[1] ||
otherAccess !== selected.permissions[2]
);
const ownerChanged = owner !== selected.owner || group !== selected.group;
export const spawnEditPermissions = ({ mode, path, selected, owner, group, Dialogs, setErrorMessage }) => {
const permissionChanged = mode !== selected.mode;
const ownerChanged = owner !== selected.user || group !== selected.group;

Promise.resolve()
.then(() => {
if (permissionChanged)
return cockpit.spawn(command, options);
return cockpit.spawn(["chmod", mode.toString(8), path.join("/") + "/" + selected.name], options);
})
.then(() => {
if (ownerChanged) {
return cockpit.spawn(
["chown", owner + ":" + group, path.join("/") + "/" + selected.name],
options
);
return cockpit.spawn(["chown", owner + ":" + group, path.join("/") + "/" + selected.name], options);
}
})
.then(Dialogs.close, err => setErrorMessage(err.message));
Expand Down
136 changes: 38 additions & 98 deletions src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { ContextMenu } from "./navigatorContextMenu.jsx";
import { NavigatorBreadcrumbs } from "./navigatorBreadcrumbs.jsx";
import {
copyItem, createDirectory, createLink, deleteItem, editPermissions, pasteItem, renameItem, updateFile
copyItem, createDirectory, createLink, deleteItem, editPermissions, pasteItem, renameItem
} from "./fileActions.jsx";
import { SidebarPanelDetails } from "./sidebar.jsx";
import { NavigatorCardHeader } from "./header.jsx";
import { usePageLocation } from "hooks.js";
import { fsinfo } from "./fsinfo";

const _ = cockpit.gettext;

Expand All @@ -56,10 +57,9 @@ export const Application = () => {
const [errorMessage, setErrorMessage] = useState();
const [currentFilter, setCurrentFilter] = useState("");
const [files, setFiles] = useState([]);
const [rootInfo, setRootInfo] = useState();
const [isGrid, setIsGrid] = useState(true);
const [sortBy, setSortBy] = useState(localStorage.getItem("cockpit-navigator.sort") || "az");
const channel = useRef(null);
const channelList = useRef(null);
const [selected, setSelected] = useState([]);
const [selectedContext, setSelectedContext] = useState(null);
const [showHidden, setShowHidden] = useState(false);
Expand Down Expand Up @@ -89,98 +89,38 @@ export const Application = () => {
});
}, [options]);

const getFsList = useCallback(() => {
const _files = [];
setLoadingFiles(true);

if (channelList.current !== null)
channelList.current.close();

channelList.current = cockpit.channel({
payload: "fslist1",
path: `/${currentDir}`,
superuser: "try",
watch: false,
});

channelList.current.addEventListener("message", (ev, data) => {
const file = JSON.parse(data);

_files.push({ ...file, name: file.path, isHidden: file.path.startsWith(".") });
});

channelList.current.addEventListener("close", (ev, data) => {
setLoading(false);
if (data?.problem && data?.message) {
setErrorMessage(data.message);
setLoadingFiles(false);
} else {
setErrorMessage(null);
Promise.all(_files.map(file => updateFile(file, currentDir)))
.then(() => {
setFiles(_files);
setLoadingFiles(false);
});
useEffect(
() => {
if (sel === undefined) {
return;
}
});
}, [currentDir]);

const watchFiles = useCallback(() => {
if (channel.current !== null)
channel.current.close();

channel.current = cockpit.channel({
payload: "fswatch1",
path: `/${currentDir}`,
superuser: "try",
});

channel.current.addEventListener("message", (ev, data) => {
const item = JSON.parse(data);

item.name = item.path.slice(item.path.lastIndexOf("/") + 1);
item.isHidden = item.name.startsWith(".");

// When files are created with some file editor we get also 'attribute-changed' and
// 'done-hint' events which are handled below. We should not add the same file twice.
if (item.event === "created" && item.type === "directory") {
updateFile(item, currentDir).then(file => {
setFiles(_f => [..._f, file]);
});
} else {
if (item.event === "deleted") {
setFiles(f => f.filter(res => res.name !== item.name));
} else {
// For events other than 'present' we don't receive file stat information
// so we rerun the fslist command to get the updated information
// https://github.com/allisonkarlitskaya/systemd_ctypes/issues/56
if (item.name[0] !== ".") {
getFsList();
}
}
}
});
}, [currentDir, getFsList]);

useEffect(() => {
// Wait for the path initial value to be set before fetching the files
if (sel === undefined) {
return;
}

setSelected([]);
setFiles([]);
setLoading(true);

watchFiles();
getFsList();
}, [sel, getFsList, watchFiles]);
const info = fsinfo(
`/${currentDir}`,
["type", "mode", "size", "mtime", "user", "group", "target", "entries", "targets"]
);
return info.effect(state => {
setLoading(false);
setLoadingFiles(!(state.info || state.error));
setRootInfo(state.info);
setErrorMessage(state.error?.message ?? "");
const entries = Object.entries(state?.info?.entries || {});
const files = entries.map(([name, attrs]) => ({
...attrs,
name,
to: info.target(name)?.type ?? null
}));
setFiles(files);
});
},
[currentDir, sel]
);

if (loading)
return <EmptyStatePanel loading />;

const visibleFiles = !showHidden
? files.filter(file => !file.isHidden)
? files.filter(file => !file.name.startsWith("."))
: files;

const _createDirectory = () => createDirectory(Dialogs, currentDir, selectedContext || selected);
Expand All @@ -192,7 +132,7 @@ export const Application = () => {
};
const _pasteItem = (targetPath, asSymlink) => pasteItem(clipboard, targetPath.join("/") + "/", asSymlink, addAlert);
const _renameItem = () => renameItem(Dialogs, { selected: selectedContext, path, setHistory, setHistoryIndex });
const _editProperties = () => editPermissions(Dialogs, { selected: selectedContext, path });
const _editProperties = () => editPermissions(Dialogs, { selected: selectedContext || rootInfo, path });
const _deleteItem = () => {
deleteItem(
Dialogs,
Expand Down Expand Up @@ -233,7 +173,7 @@ export const Application = () => {
? [{ title: _("Copy"), onClick: _copyItem }, { title: _("Delete"), onClick: _deleteItem, className: "pf-m-danger" }]
: [
{ title: _("Copy"), onClick: _copyItem },
...(selectedContext.type === "directory")
...(selectedContext.type === "dir")
? [
{
title: _("Paste into directory"),
Expand Down Expand Up @@ -348,8 +288,8 @@ export const Application = () => {

const compare = (sortBy) => {
const compareFileType = (a, b) => {
const aIsDir = (a.type === "directory" || a?.to === "directory");
const bIsDir = (b.type === "directory" || b?.to === "directory");
const aIsDir = (a.type === "dir" || a?.to === "dir");
const bIsDir = (b.type === "dir" || b?.to === "dir");

if (aIsDir && !bIsDir)
return -1;
Expand All @@ -373,13 +313,13 @@ const compare = (sortBy) => {
: compareFileType(a, b);
case "last_modified":
return (a, b) => compareFileType(a, b) === 0
? (a.modified > b.modified
? (a.mtime > b.mtime
? -1
: 1)
: compareFileType(a, b);
case "first_modified":
return (a, b) => compareFileType(a, b) === 0
? (a.modified < b.modified
? (a.mtime < b.mtime
? -1
: 1)
: compareFileType(a, b);
Expand Down Expand Up @@ -428,7 +368,7 @@ const NavigatorCardBody = ({

const onDoubleClickNavigate = useCallback((file) => {
const newPath = [...path, file.name].join("/");
if (file.type === "directory" || file.to === "directory") {
if (file.type === "dir" || file.to === "dir") {
setHistory(h => [...h.slice(0, historyIndex + 1), [...path, file.name]]);
setHistoryIndex(h => h + 1);

Expand Down Expand Up @@ -537,9 +477,9 @@ const NavigatorCardBody = ({

const Item = ({ file }) => {
const getFileType = (file) => {
if (file.type === "directory") {
if (file.type === "dir") {
return "directory-item";
} else if (file.type === "link" && file?.to === "directory") {
} else if (file.type === "lnk" && file?.to === "dir") {
return "directory-item";
} else {
return "file-item";
Expand Down Expand Up @@ -575,7 +515,7 @@ const NavigatorCardBody = ({
? "xl"
: "lg"} isInline
>
{file.type === "directory" || file.to === "directory"
{file.type === "dir" || file.to === "dir"
? <FolderIcon />
: <FileIcon />}
</Icon>
Expand Down
37 changes: 25 additions & 12 deletions src/common.js → src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,32 @@ import cockpit from "cockpit";
const _ = cockpit.gettext;

export const permissions = [
{ label: _("None"), value: "0" },
{ label: _("Read-only"), value: "4" },
{ label: _("Write-only"), value: "2" },
{ label: _("Execute-only"), value: "1" },
{ label: _("Read and write"), value: "6" },
{ label: _("Read and execute"), value: "5" },
{ label: _("Read, write and execute"), value: "7" },
{ label: _("Write and execute"), value: "3" },
/* 0 */ _("None"),
/* 1 */ _("Execute-only"),
/* 2 */ _("Write-only"),
/* 3 */ _("Write and execute"),
/* 4 */ _("Read-only"),
/* 5 */ _("Read and execute"),
/* 6 */ _("Read and write"),
/* 7 */ _("Read, write and execute"),
];

export const inode_types = {
directory: _("Directory"),
file: _("Regular file"),
link: _("Symbolic link"),
special: _("Special file"),
blk: _("Block device"),
chr: _("Character device"),
dir: _("Directory"),
fifo: _("Named pipe"),
lnk: _("Symbolic link"),
reg: _("Regular file"),
sock: _("Socket"),
};

export function get_permissions(n: number) {
return permissions[n & 0o7];
}

export function * map_permissions<T>(func: (value: number, label: string) => T) {
for (const [value, label] of permissions.entries()) {
yield func(value, label);
}
}
Loading

0 comments on commit 78ec505

Please sign in to comment.