Add Phase 3 remote read-only file operations
Introduce dedicated remote file facade for /Clients paths, add agent read/download endpoints, enable remote view/properties/download/image preview in the web UI, and keep remote write operations disabled.
This commit is contained in:
+63
-18
@@ -459,6 +459,23 @@ function isOpenableSelection(item) {
|
||||
return isImageSelection(item) || isVideoSelection(item);
|
||||
}
|
||||
|
||||
function isTextPreviewSelection(item) {
|
||||
if (!item || item.kind !== "file") {
|
||||
return false;
|
||||
}
|
||||
const lower = (item.name || "").toLowerCase();
|
||||
if (lower === "dockerfile" || lower === "containerfile") {
|
||||
return true;
|
||||
}
|
||||
return [".txt", ".log", ".ini", ".cfg", ".conf", ".md", ".yml", ".yaml", ".json", ".js", ".py", ".css", ".html"].some((suffix) =>
|
||||
lower.endsWith(suffix)
|
||||
);
|
||||
}
|
||||
|
||||
function isRemoteViewableSelection(item) {
|
||||
return isImageSelection(item) || isTextPreviewSelection(item);
|
||||
}
|
||||
|
||||
function isZipDownloadSelection(items) {
|
||||
return items.length > 1 || (items.length === 1 && items[0].kind === "directory");
|
||||
}
|
||||
@@ -778,16 +795,17 @@ function openContextMenu(pane, entry, event) {
|
||||
contextMenuState.anchorPath = entry.path;
|
||||
|
||||
const isMulti = items.length > 1;
|
||||
const openableSingle = items.length === 1 && (!remoteSelection || items[0].kind === "directory") && isOpenableSelection(items[0]);
|
||||
const openableSingle =
|
||||
items.length === 1 && (remoteSelection ? items[0].kind === "directory" || isRemoteViewableSelection(items[0]) : isOpenableSelection(items[0]));
|
||||
const editableSingle = items.length === 1 && !remoteSelection && isEditableSelection(items[0]);
|
||||
const downloadableSelection = items.length > 0 && !remoteSelection;
|
||||
const downloadableSelection = items.length === 1 && items[0].kind === "file";
|
||||
elements.scope.textContent = isMulti ? "Multi-selection" : "Single item";
|
||||
elements.target.textContent = isMulti ? `${items.length} selected items` : entry.name;
|
||||
elements.openButton.classList.toggle("hidden", isMulti);
|
||||
elements.openButton.disabled = !openableSingle;
|
||||
elements.editButton.classList.toggle("hidden", isMulti || items.length !== 1 || items[0].kind !== "file" || remoteSelection);
|
||||
elements.editButton.disabled = !editableSingle;
|
||||
elements.downloadButton.classList.toggle("hidden", remoteSelection);
|
||||
elements.downloadButton.classList.remove("hidden");
|
||||
elements.downloadButton.disabled = !downloadableSelection;
|
||||
elements.renameButton.classList.toggle("hidden", isMulti || remoteSelection);
|
||||
elements.duplicateButton.classList.remove("hidden");
|
||||
@@ -798,8 +816,8 @@ function openContextMenu(pane, entry, event) {
|
||||
elements.moveButton.disabled = remoteSelection || items.length === 0;
|
||||
elements.deleteButton.classList.remove("hidden");
|
||||
elements.deleteButton.disabled = remoteSelection || items.length === 0;
|
||||
elements.propertiesButton.classList.toggle("hidden", remoteSelection);
|
||||
elements.propertiesButton.disabled = remoteSelection || items.length === 0;
|
||||
elements.propertiesButton.classList.remove("hidden");
|
||||
elements.propertiesButton.disabled = items.length === 0;
|
||||
|
||||
const menuWidth = 220;
|
||||
const menuHeight = 120;
|
||||
@@ -960,17 +978,23 @@ async function startDownloadSelected() {
|
||||
setStatus(`Download started: ${task.destination}`);
|
||||
return;
|
||||
}
|
||||
const { blob, fileName } = await downloadFileRequest(selectedPaths);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = fileName || selected.name;
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
markSingleFileDownloadRequested(anchor.download, selected.path);
|
||||
setStatus(`Download requested: ${anchor.download}`);
|
||||
let fileName = selected.name;
|
||||
if (isRemoteBrowsePath(selected.path)) {
|
||||
fileName = startDirectSingleFileDownload(selected.path, selected.name).fileName || selected.name;
|
||||
} else {
|
||||
const response = await downloadFileRequest(selectedPaths);
|
||||
const url = URL.createObjectURL(response.blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = response.fileName || selected.name;
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
fileName = anchor.download || selected.name;
|
||||
}
|
||||
markSingleFileDownloadRequested(fileName, selected.path);
|
||||
setStatus(`Download requested: ${fileName}`);
|
||||
} catch (err) {
|
||||
if (zipDownload) {
|
||||
if (err.code === "download_cancelled") {
|
||||
@@ -1279,6 +1303,18 @@ async function downloadFileRequest(paths) {
|
||||
};
|
||||
}
|
||||
|
||||
function startDirectSingleFileDownload(path, fallbackName) {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = `/api/files/download?${new URLSearchParams({ path }).toString()}`;
|
||||
anchor.download = fallbackName || "";
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
return {
|
||||
fileName: anchor.download || fallbackName || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function createArchiveDownloadTask(paths) {
|
||||
return apiRequest("POST", "/api/files/download/archive-prepare", { paths });
|
||||
}
|
||||
@@ -2108,7 +2144,8 @@ function updateActionButtons() {
|
||||
const exactlyOne = count === 1;
|
||||
const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file");
|
||||
const remoteBrowse = isRemoteBrowsePath(activePaneState().currentPath);
|
||||
document.getElementById("view-btn").disabled = remoteBrowse || !exactlyOne || !allFiles;
|
||||
const remoteViewable = exactlyOne && isRemoteViewableSelection(selectedItems[0] || null);
|
||||
document.getElementById("view-btn").disabled = remoteBrowse ? !remoteViewable : !exactlyOne || !allFiles;
|
||||
document.getElementById("edit-btn").disabled = remoteBrowse || !exactlyOne || !allFiles || !isEditableSelection(selectedItems[0] || null);
|
||||
document.getElementById("rename-btn").disabled = remoteBrowse || !exactlyOne;
|
||||
document.getElementById("delete-btn").disabled = remoteBrowse || !hasSelection;
|
||||
@@ -4691,6 +4728,14 @@ function openViewer() {
|
||||
return;
|
||||
}
|
||||
const selected = selectedItems[0];
|
||||
if (isRemoteBrowsePath(selected.path)) {
|
||||
if (isImageSelection(selected)) {
|
||||
openImageViewer();
|
||||
return;
|
||||
}
|
||||
openTextViewer();
|
||||
return;
|
||||
}
|
||||
if (isImageSelection(selected)) {
|
||||
openImageViewer();
|
||||
return;
|
||||
@@ -4792,7 +4837,7 @@ function openCurrentDirectory() {
|
||||
openImageViewer();
|
||||
return;
|
||||
}
|
||||
if (isVideoSelection(item)) {
|
||||
if (!isRemoteBrowsePath(item.path) && isVideoSelection(item)) {
|
||||
openVideoViewer();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user