Not found paths in prefix fs always treated as dir (#2002)

Gracefully handle prefix paths that don't exist, representing them as
directories so they can be escaped from.

Also removes the ".." file info from the backend, instead only creating
it on the frontend
This commit is contained in:
Evan Simkowitz 2025-02-21 16:32:14 -08:00 committed by GitHub
parent 9ef213fc42
commit d51ff87c26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 79 additions and 132 deletions

View File

@ -757,7 +757,6 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [focusIndex, setFocusIndex] = useState(0); const [focusIndex, setFocusIndex] = useState(0);
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]); const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
const showHiddenFiles = useAtomValue(model.showHiddenFiles); const showHiddenFiles = useAtomValue(model.showHiddenFiles);
const [selectedPath, setSelectedPath] = useState(""); const [selectedPath, setSelectedPath] = useState("");
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion); const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
@ -776,8 +775,9 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
}; };
}, [setRefreshVersion]); }, [setRefreshVersion]);
useEffect(() => { useEffect(
const getContent = async () => { () =>
fireAndForget(async () => {
let entries: FileInfo[]; let entries: FileInfo[];
try { try {
const file = await RpcApi.FileReadCommand( const file = await RpcApi.FileReadCommand(
@ -790,6 +790,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
null null
); );
entries = file.entries ?? []; entries = file.entries ?? [];
entries.unshift({
name: "..",
path: file?.info?.dir,
isdir: true,
modtime: new Date().getTime(),
mimetype: "directory",
});
} catch (e) { } catch (e) {
setErrorMsg({ setErrorMsg({
status: "Cannot Read Directory", status: "Cannot Read Directory",
@ -797,12 +804,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
}); });
} }
setUnfilteredData(entries); setUnfilteredData(entries);
}; }),
getContent(); [conn, dirPath, refreshVersion]
}, [conn, dirPath, refreshVersion]); );
useEffect(() => { const filteredData = useMemo(
const filtered = unfilteredData?.filter((fileInfo) => { () =>
unfilteredData?.filter((fileInfo) => {
if (fileInfo.name == null) { if (fileInfo.name == null) {
console.log("fileInfo.name is null", fileInfo); console.log("fileInfo.name is null", fileInfo);
return false; return false;
@ -811,9 +819,9 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
return false; return false;
} }
return fileInfo.name.toLowerCase().includes(searchText); return fileInfo.name.toLowerCase().includes(searchText);
}); }) ?? [],
setFilteredData(filtered ?? []); [unfilteredData, showHiddenFiles, searchText]
}, [unfilteredData, showHiddenFiles, searchText]); );
useEffect(() => { useEffect(() => {
model.directoryKeyDownHandler = (waveEvent: WaveKeyboardEvent): boolean => { model.directoryKeyDownHandler = (waveEvent: WaveKeyboardEvent): boolean => {

View File

@ -5,7 +5,6 @@ import { BlockNodeModel } from "@/app/block/blocktypes";
import { Button } from "@/app/element/button"; import { Button } from "@/app/element/button";
import { CopyButton } from "@/app/element/copybutton"; import { CopyButton } from "@/app/element/copybutton";
import { CenteredDiv } from "@/app/element/quickelems"; import { CenteredDiv } from "@/app/element/quickelems";
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { tryReinjectKey } from "@/app/store/keymodel"; import { tryReinjectKey } from "@/app/store/keymodel";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
@ -18,7 +17,7 @@ import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { getWebServerEndpoint } from "@/util/endpoints"; import { getWebServerEndpoint } from "@/util/endpoints";
import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil"; import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { addOpenMenuItems } from "@/util/previewutil"; import { addOpenMenuItems } from "@/util/previewutil";
import { base64ToString, fireAndForget, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util"; import { base64ToString, fireAndForget, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util";
import { formatRemoteUri } from "@/util/waveutil"; import { formatRemoteUri } from "@/util/waveutil";
@ -28,7 +27,7 @@ import { Atom, atom, Getter, PrimitiveAtom, useAtom, useAtomValue, useSetAtom, W
import { loadable } from "jotai/utils"; import { loadable } from "jotai/utils";
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api"; import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { createRef, memo, useCallback, useEffect, useMemo, useState } from "react"; import { createRef, memo, useCallback, useEffect, useMemo } from "react";
import { TransformComponent, TransformWrapper, useControls } from "react-zoom-pan-pinch"; import { TransformComponent, TransformWrapper, useControls } from "react-zoom-pan-pinch";
import { CSVView } from "./csvview"; import { CSVView } from "./csvview";
import { DirectoryPreview } from "./directorypreview"; import { DirectoryPreview } from "./directorypreview";
@ -1073,17 +1072,17 @@ function PreviewView({
model: PreviewModel; model: PreviewModel;
}) { }) {
const connStatus = useAtomValue(model.connStatus); const connStatus = useAtomValue(model.connStatus);
const filePath = useAtomValue(model.metaFilePath);
const [errorMsg, setErrorMsg] = useAtom(model.errorMsgAtom); const [errorMsg, setErrorMsg] = useAtom(model.errorMsgAtom);
const connection = useAtomValue(model.connectionImmediate); const connection = useAtomValue(model.connectionImmediate);
const fileInfo = useAtomValue(model.statFile); const fileInfo = useAtomValue(model.statFile);
useEffect(() => { useEffect(() => {
console.log("fileInfo or connection changed", fileInfo, connection);
if (!fileInfo) { if (!fileInfo) {
return; return;
} }
setErrorMsg(null); setErrorMsg(null);
}, [connection, filePath, fileInfo]); }, [connection, fileInfo]);
if (connStatus?.status != "connected") { if (connStatus?.status != "connected") {
return null; return null;
@ -1113,7 +1112,6 @@ function PreviewView({
return ( return (
<> <>
{/* <OpenFileModal blockId={blockId} model={model} blockRef={blockRef} /> */}
<div key="fullpreview" className="full-preview scrollbar-hide-until-hover"> <div key="fullpreview" className="full-preview scrollbar-hide-until-hover">
{errorMsg && <ErrorOverlay errorMsg={errorMsg} resetOverlay={() => setErrorMsg(null)} />} {errorMsg && <ErrorOverlay errorMsg={errorMsg} resetOverlay={() => setErrorMsg(null)} />}
<div ref={contentRef} className="full-preview-content"> <div ref={contentRef} className="full-preview-content">
@ -1133,72 +1131,6 @@ function PreviewView({
); );
} }
const OpenFileModal = memo(
({
model,
blockRef,
blockId,
}: {
model: PreviewModel;
blockRef: React.RefObject<HTMLDivElement>;
blockId: string;
}) => {
const openFileModal = useAtomValue(model.openFileModal);
const curFileName = useAtomValue(model.metaFilePath);
const [filePath, setFilePath] = useState("");
const isNodeFocused = useAtomValue(model.nodeModel.isFocused);
const handleKeyDown = useCallback(
keydownWrapper((waveEvent: WaveKeyboardEvent): boolean => {
if (checkKeyPressed(waveEvent, "Escape")) {
model.updateOpenFileModalAndError(false);
return true;
}
const handleCommandOperations = async () => {
if (checkKeyPressed(waveEvent, "Enter")) {
await model.handleOpenFile(filePath);
return true;
}
return false;
};
handleCommandOperations().catch((error) => {
console.error("Error handling key down:", error);
model.updateOpenFileModalAndError(true, "An error occurred during operation.");
return false;
});
return false;
}),
[model, blockId, filePath, curFileName]
);
const handleFileSuggestionSelect = (value) => {
globalStore.set(model.openFileModal, false);
};
const handleFileSuggestionChange = (value) => {
setFilePath(value);
};
const handleBackDropClick = () => {
globalStore.set(model.openFileModal, false);
};
if (!openFileModal) {
return null;
}
return (
<TypeAheadModal
label="Open path"
blockRef={blockRef}
anchorRef={model.previewTextRef}
onKeyDown={handleKeyDown}
onSelect={handleFileSuggestionSelect}
onChange={handleFileSuggestionChange}
onClickBackdrop={handleBackDropClick}
autoFocus={isNodeFocused}
giveFocusRef={model.openFileModalGiveFocusRef}
/>
);
}
);
const ErrorOverlay = memo(({ errorMsg, resetOverlay }: { errorMsg: ErrorMsg; resetOverlay: () => void }) => { const ErrorOverlay = memo(({ errorMsg, resetOverlay }: { errorMsg: ErrorMsg; resetOverlay: () => void }) => {
const showDismiss = errorMsg.showDismiss ?? true; const showDismiss = errorMsg.showDismiss ?? true;
const buttonClassName = "outlined grey font-size-11 vertical-padding-3 horizontal-padding-7"; const buttonClassName = "outlined grey font-size-11 vertical-padding-3 horizontal-padding-7";

View File

@ -150,6 +150,8 @@ func DetermineCopyDestPath(ctx context.Context, srcConn, destConn *connparse.Con
srcInfo, err = srcClient.Stat(ctx, srcConn) srcInfo, err = srcClient.Stat(ctx, srcConn)
if err != nil { if err != nil {
return "", "", nil, fmt.Errorf("error getting source file info: %w", err) return "", "", nil, fmt.Errorf("error getting source file info: %w", err)
} else if srcInfo.NotFound {
return "", "", nil, fmt.Errorf("source file not found: %w", err)
} }
destInfo, err := destClient.Stat(ctx, destConn) destInfo, err := destClient.Stat(ctx, destConn)
destExists := err == nil && !destInfo.NotFound destExists := err == nil && !destInfo.NotFound
@ -158,7 +160,7 @@ func DetermineCopyDestPath(ctx context.Context, srcConn, destConn *connparse.Con
} }
originalDestPath := destPath originalDestPath := destPath
if !srcHasSlash { if !srcHasSlash {
if destInfo.IsDir || (!destExists && !destHasSlash && srcInfo.IsDir) { if (destExists && destInfo.IsDir) || (!destExists && !destHasSlash && srcInfo.IsDir) {
destPath = fspath.Join(destPath, fspath.Base(srcConn.Path)) destPath = fspath.Join(destPath, fspath.Base(srcConn.Path))
} }
} }

View File

@ -62,6 +62,20 @@ func (c S3Client) ReadStream(ctx context.Context, conn *connparse.Connection, da
return return
} }
rtn <- wshrpc.RespOrErrorUnion[wshrpc.FileData]{Response: wshrpc.FileData{Info: finfo}} rtn <- wshrpc.RespOrErrorUnion[wshrpc.FileData]{Response: wshrpc.FileData{Info: finfo}}
if finfo.NotFound {
rtn <- wshrpc.RespOrErrorUnion[wshrpc.FileData]{Response: wshrpc.FileData{Entries: []*wshrpc.FileInfo{
{
Path: finfo.Dir,
Dir: fspath.Dir(finfo.Dir),
Name: "..",
IsDir: true,
Size: 0,
ModTime: time.Now().Unix(),
MimeType: "directory",
},
}}}
return
}
if finfo.IsDir { if finfo.IsDir {
listEntriesCh := c.ListEntriesStream(ctx, conn, nil) listEntriesCh := c.ListEntriesStream(ctx, conn, nil)
defer func() { defer func() {
@ -455,20 +469,6 @@ func (c S3Client) ListEntriesStream(ctx context.Context, conn *connparse.Connect
rtn <- wshutil.RespErr[wshrpc.CommandRemoteListEntriesRtnData](err) rtn <- wshutil.RespErr[wshrpc.CommandRemoteListEntriesRtnData](err)
return return
} }
parentPath := fsutil.GetParentPath(conn)
if parentPath != "" {
rtn <- wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteListEntriesRtnData]{Response: wshrpc.CommandRemoteListEntriesRtnData{FileInfo: []*wshrpc.FileInfo{
{
Path: parentPath,
Dir: fsutil.GetParentPathString(parentPath),
Name: "..",
IsDir: true,
Size: 0,
ModTime: time.Now().Unix(),
MimeType: "directory",
},
}}}
}
entries := make([]*wshrpc.FileInfo, 0, wshrpc.DirChunkSize) entries := make([]*wshrpc.FileInfo, 0, wshrpc.DirChunkSize)
for _, entry := range entryMap { for _, entry := range entryMap {
entries = append(entries, entry) entries = append(entries, entry)
@ -532,6 +532,7 @@ func (c S3Client) Stat(ctx context.Context, conn *connparse.Connection) (*wshrpc
Path: bucketName, Path: bucketName,
Dir: fspath.Separator, Dir: fspath.Separator,
NotFound: true, NotFound: true,
IsDir: true,
}, nil }, nil
} }
} }
@ -556,7 +557,7 @@ func (c S3Client) Stat(ctx context.Context, conn *connparse.Connection) (*wshrpc
MaxKeys: aws.Int32(1), MaxKeys: aws.Int32(1),
}) })
if err == nil { if err == nil {
if entries.Contents != nil && len(entries.Contents) > 0 { if entries.Contents != nil {
return &wshrpc.FileInfo{ return &wshrpc.FileInfo{
Name: objectKey, Name: objectKey,
Path: conn.GetPathWithHost(), Path: conn.GetPathWithHost(),
@ -575,6 +576,7 @@ func (c S3Client) Stat(ctx context.Context, conn *connparse.Connection) (*wshrpc
Name: objectKey, Name: objectKey,
Path: conn.GetPathWithHost(), Path: conn.GetPathWithHost(),
Dir: fsutil.GetParentPath(conn), Dir: fsutil.GetParentPath(conn),
IsDir: true,
NotFound: true, NotFound: true,
}, nil }, nil
} }

View File

@ -105,6 +105,16 @@ func (c WaveClient) Read(ctx context.Context, conn *connparse.Connection, data w
if err != nil { if err != nil {
return nil, fmt.Errorf("error listing blockfiles: %w", err) return nil, fmt.Errorf("error listing blockfiles: %w", err)
} }
if len(list) == 0 {
return &wshrpc.FileData{
Info: &wshrpc.FileInfo{
Name: fspath.Base(fileName),
Path: fileName,
Dir: fspath.Dir(fileName),
NotFound: true,
IsDir: true,
}}, nil
}
return &wshrpc.FileData{Info: data.Info, Entries: list}, nil return &wshrpc.FileData{Info: data.Info, Entries: list}, nil
} }

View File

@ -108,13 +108,6 @@ func (impl *ServerImpl) remoteStreamFileDir(ctx context.Context, path string, by
} }
} }
var fileInfoArr []*wshrpc.FileInfo var fileInfoArr []*wshrpc.FileInfo
parent := filepath.Dir(path)
parentFileInfo, err := impl.fileInfoInternal(parent, false)
if err == nil && parent != path {
parentFileInfo.Name = ".."
parentFileInfo.Size = -1
fileInfoArr = append(fileInfoArr, parentFileInfo)
}
for _, innerFileEntry := range innerFilesEntries { for _, innerFileEntry := range innerFilesEntries {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()