Create and rename files and dirs in dirpreview (#1156)

New context menu options are available in the directory preview to
create and rename files and directories

It's missing three pieces of functionality, none of which are a
regression:
- Editing or creating an entry does not update the focused index. Focus
index right now is pretty dumb, it doesn't factor in the column sorting
so if you change that, the selected item will change to whatever is now
at that index. We should update this so we use the actual file name to
determine which element to focus and let the table determine which index
to then highlight given the current sorting algo
- Open in native preview should not be an option on remote connections
with the exception of WSL, where it should resolve the file in the
Windows filesystem, rather than the WSL one
- We should catch CRUD errors in the dir preview and display a popup
This commit is contained in:
Evan Simkowitz 2024-12-03 01:23:44 -05:00 committed by GitHub
parent f606a74339
commit 04c4f0a203
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 461 additions and 91 deletions

View File

@ -359,9 +359,11 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => {
electron.ipcMain.on("open-native-path", (event, filePath: string) => {
console.log("open-native-path", filePath);
electron.shell.openPath(filePath).catch((err) => {
console.error(`Failed to open path ${filePath}:`, err);
});
fireAndForget(async () =>
electron.shell.openPath(filePath).then((excuse) => {
if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`);
})
);
});
async function createNewWaveWindow(): Promise<void> {

View File

@ -2,15 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
.popover-content {
min-width: 100px;
min-height: 150px;
position: absolute;
z-index: 1000; // TODO: put this in theme.scss
display: flex;
padding: 2px;
gap: 1px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: #212121;
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
min-width: 100px;
min-height: 150px;
position: absolute;
z-index: 1000; // TODO: put this in theme.scss
display: flex;
padding: 2px;
gap: 1px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: #212121;
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
}

View File

@ -5,6 +5,7 @@ import { Button } from "@/element/button";
import {
autoUpdate,
FloatingPortal,
Middleware,
offset as offsetMiddleware,
useClick,
useDismiss,
@ -34,6 +35,7 @@ interface PopoverProps {
placement?: Placement;
offset?: OffsetOptions;
onDismiss?: () => void;
middleware?: Middleware[];
}
const isPopoverButton = (
@ -48,54 +50,63 @@ const isPopoverContent = (
return element.type === PopoverContent;
};
const Popover = memo(({ children, className, placement = "bottom-start", offset = 3, onDismiss }: PopoverProps) => {
const [isOpen, setIsOpen] = useState(false);
const Popover = memo(
({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }: PopoverProps) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (!open && onDismiss) {
onDismiss();
}
};
const { refs, floatingStyles, context } = useFloating({
placement,
open: isOpen,
onOpenChange: handleOpenChange,
middleware: [offsetMiddleware(offset)],
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
const renderChildren = Children.map(children, (child) => {
if (isValidElement(child)) {
if (isPopoverButton(child)) {
return cloneElement(child as any, {
isActive: isOpen,
ref: refs.setReference,
getReferenceProps,
// Do not overwrite onClick
});
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (!open && onDismiss) {
onDismiss();
}
};
if (isPopoverContent(child)) {
return isOpen
? cloneElement(child as any, {
ref: refs.setFloating,
style: floatingStyles,
getFloatingProps,
})
: null;
}
if (offset === undefined) {
offset = 3;
}
return child;
});
return <div className={clsx("popover", className)}>{renderChildren}</div>;
});
middleware ??= [];
middleware.push(offsetMiddleware(offset));
const { refs, floatingStyles, context } = useFloating({
placement,
open: isOpen,
onOpenChange: handleOpenChange,
middleware: middleware,
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
const renderChildren = Children.map(children, (child) => {
if (isValidElement(child)) {
if (isPopoverButton(child)) {
return cloneElement(child as any, {
isActive: isOpen,
ref: refs.setReference,
getReferenceProps,
// Do not overwrite onClick
});
}
if (isPopoverContent(child)) {
return isOpen
? cloneElement(child as any, {
ref: refs.setFloating,
style: floatingStyles,
getFloatingProps,
})
: null;
}
}
return child;
});
return <div className={clsx("popover", className)}>{renderChildren}</div>;
}
);
interface PopoverButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isActive?: boolean;

View File

@ -59,11 +59,17 @@ class FileServiceType {
GetWaveFile(arg1: string, arg2: string): Promise<any> {
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
}
Mkdir(arg1: string, arg2: string): Promise<void> {
return WOS.callBackendService("file", "Mkdir", Array.from(arguments))
}
// read file
ReadFile(connection: string, path: string): Promise<FullFile> {
return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
}
Rename(arg1: string, arg2: string, arg3: string): Promise<void> {
return WOS.callBackendService("file", "Rename", Array.from(arguments))
}
// save file
SaveFile(connection: string, path: string, data64: string): Promise<void> {
@ -74,6 +80,9 @@ class FileServiceType {
StatFile(connection: string, path: string): Promise<FileInfo> {
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
}
TouchFile(arg1: string, arg2: string): Promise<void> {
return WOS.callBackendService("file", "TouchFile", Array.from(arguments))
}
}
export const FileService = new FileServiceType();

View File

@ -212,6 +212,21 @@ class RpcApiType {
return client.wshRpcCall("remotefilejoin", data, opts);
}
// command "remotefilerename" [call]
RemoteFileRenameCommand(client: WshClient, data: string[], opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefilerename", data, opts);
}
// command "remotefiletouch" [call]
RemoteFileTouchCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefiletouch", data, opts);
}
// command "remotemkdir" [call]
RemoteMkdirCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotemkdir", data, opts);
}
// command "remotestreamcpudata" [responsestream]
RemoteStreamCpuDataCommand(client: WshClient, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> {
return client.wshRpcStream("remotestreamcpudata", null, opts);

View File

@ -207,7 +207,11 @@ const WorkspaceSwitcher = () => {
};
return (
<Popover className="workspace-switcher-popover" onDismiss={() => setEditingWorkspace(null)}>
<Popover
className="workspace-switcher-popover"
placement="bottom-start"
onDismiss={() => setEditingWorkspace(null)}
>
<PopoverButton
className="workspace-switcher-button grey"
as="div"

View File

@ -230,3 +230,17 @@
background-color: var(--highlight-bg-color);
}
}
.entry-manager-overlay {
display: flex;
flex-direction: column;
max-width: 90%;
max-height: fit-content;
display: flex;
padding: 10px;
gap: 10px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: #212121;
box-shadow: 0px 8px 24px 0px rgba(0, 0, 0, 0.3);
}

View File

@ -1,15 +1,18 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Input } from "@/app/element/input";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { PLATFORM, atoms, createBlock, getApi } from "@/app/store/global";
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
import { FileService } from "@/app/store/services";
import type { PreviewModel } from "@/app/view/preview/preview";
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
import { base64ToString, isBlank } from "@/util/util";
import { base64ToString, fireAndForget, isBlank } from "@/util/util";
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import {
Column,
Row,
RowData,
Table,
createColumnHelper,
flexRender,
@ -19,13 +22,21 @@ import {
} from "@tanstack/react-table";
import clsx from "clsx";
import dayjs from "dayjs";
import { useAtom, useAtomValue } from "jotai";
import { PrimitiveAtom, atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { quote as shellQuote } from "shell-quote";
import { debounce } from "throttle-debounce";
import "./directorypreview.scss";
declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
updateName: (path: string) => void;
newFile: () => void;
newDirectory: () => void;
}
}
interface DirectoryTableProps {
model: PreviewModel;
data: FileInfo[];
@ -35,6 +46,9 @@ interface DirectoryTableProps {
setSearch: (_: string) => void;
setSelectedPath: (_: string) => void;
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
entryManagerOverlayPropsAtom: PrimitiveAtom<EntryManagerOverlayProps>;
newFile: () => void;
newDirectory: () => void;
}
const columnHelper = createColumnHelper<FileInfo>();
@ -121,6 +135,46 @@ function cleanMimetype(input: string): string {
return truncated.trim();
}
enum EntryManagerType {
NewFile = "New File",
NewDirectory = "New Folder",
EditName = "Rename",
}
type EntryManagerOverlayProps = {
forwardRef?: React.Ref<HTMLDivElement>;
entryManagerType: EntryManagerType;
startingValue?: string;
onSave: (newValue: string) => void;
style?: React.CSSProperties;
getReferenceProps?: () => any;
};
const EntryManagerOverlay = memo(
({ entryManagerType, startingValue, onSave, forwardRef, style, getReferenceProps }: EntryManagerOverlayProps) => {
const [value, setValue] = useState(startingValue);
return (
<div className="entry-manager-overlay" ref={forwardRef} style={style} {...getReferenceProps()}>
<div className="entry-manager-type">{entryManagerType}</div>
<div className="entry-manager-input">
<Input
value={value}
onChange={setValue}
autoFocus={true}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
onSave(value);
}
}}
></Input>
</div>
</div>
);
}
);
function DirectoryTable({
model,
data,
@ -130,6 +184,9 @@ function DirectoryTable({
setSearch,
setSelectedPath,
setRefreshVersion,
entryManagerOverlayPropsAtom,
newFile,
newDirectory,
}: DirectoryTableProps) {
const fullConfig = useAtomValue(atoms.fullConfigAtom);
const getIconFromMimeType = useCallback(
@ -205,6 +262,28 @@ function DirectoryTable({
[fullConfig]
);
const setEntryManagerProps = useSetAtom(entryManagerOverlayPropsAtom);
const updateName = useCallback((path: string) => {
const fileName = path.split("/").at(-1);
setEntryManagerProps({
entryManagerType: EntryManagerType.EditName,
startingValue: fileName,
onSave: (newName: string) => {
let newPath: string;
if (newName !== fileName) {
newPath = path.replace(fileName, newName);
console.log(`replacing ${fileName} with ${newName}: ${path}`);
fireAndForget(async () => {
await FileService.Rename(globalStore.get(model.connection), path, newPath);
model.refreshCallback();
});
}
setEntryManagerProps(undefined);
},
});
}, []);
const table = useReactTable({
data,
columns,
@ -229,6 +308,11 @@ function DirectoryTable({
},
enableMultiSort: false,
enableSortingRemoval: false,
meta: {
updateName,
newFile,
newDirectory,
},
});
useEffect(() => {
@ -418,6 +502,27 @@ function TableBody({
openNativeLabel = "Open File in Default Application";
}
const menu: ContextMenuItem[] = [
{
label: "New File",
click: () => {
table.options.meta.newFile();
},
},
{
label: "New Folder",
click: () => {
table.options.meta.newDirectory();
},
},
{
label: "Rename",
click: () => {
table.options.meta.updateName(finfo.path);
},
},
{
type: "separator",
},
{
label: "Copy File Name",
click: () => navigator.clipboard.writeText(fileName),
@ -446,6 +551,7 @@ function TableBody({
{
type: "separator",
},
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
{
label: openNativeLabel,
click: async () => {
@ -483,14 +589,18 @@ function TableBody({
},
});
}
menu.push({ type: "separator" });
menu.push({
label: "Delete File",
click: async () => {
await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e));
setRefreshVersion((current) => current + 1);
menu.push(
{
type: "separator",
},
});
{
label: "Delete",
click: async () => {
await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e));
setRefreshVersion((current) => current + 1);
},
}
);
ContextMenuModel.showContextMenu(menu, e);
},
[setRefreshVersion, conn]
@ -560,12 +670,12 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
const [focusIndex, setFocusIndex] = useState(0);
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
const fileName = useAtomValue(model.metaFilePath);
const showHiddenFiles = useAtomValue(model.showHiddenFiles);
const [selectedPath, setSelectedPath] = useState("");
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
const conn = useAtomValue(model.connection);
const blockData = useAtomValue(model.blockAtom);
const dirPath = useAtomValue(model.normFilePath);
useEffect(() => {
model.refreshCallback = () => {
@ -578,13 +688,13 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
useEffect(() => {
const getContent = async () => {
const file = await FileService.ReadFile(conn, fileName);
const file = await FileService.ReadFile(conn, dirPath);
const serializedContent = base64ToString(file?.data64);
const content: FileInfo[] = JSON.parse(serializedContent);
setUnfilteredData(content);
};
getContent();
}, [conn, fileName, refreshVersion]);
}, [conn, dirPath, refreshVersion]);
useEffect(() => {
const filtered = unfilteredData.filter((fileInfo) => {
@ -652,26 +762,138 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
}
}, [filteredData]);
const entryManagerPropsAtom = useState(
atom<EntryManagerOverlayProps>(null) as PrimitiveAtom<EntryManagerOverlayProps>
)[0];
const [entryManagerProps, setEntryManagerProps] = useAtom(entryManagerPropsAtom);
const { refs, floatingStyles, context } = useFloating({
open: !!entryManagerProps,
onOpenChange: () => setEntryManagerProps(undefined),
middleware: [offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)],
});
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
const newFile = useCallback(() => {
setEntryManagerProps({
entryManagerType: EntryManagerType.NewFile,
onSave: (newName: string) => {
console.log(`newFile: ${newName}`);
fireAndForget(async () => {
await FileService.TouchFile(globalStore.get(model.connection), `${dirPath}/${newName}`);
model.refreshCallback();
});
setEntryManagerProps(undefined);
},
});
}, [dirPath]);
const newDirectory = useCallback(() => {
setEntryManagerProps({
entryManagerType: EntryManagerType.NewDirectory,
onSave: (newName: string) => {
console.log(`newDirectory: ${newName}`);
fireAndForget(async () => {
await FileService.Mkdir(globalStore.get(model.connection), `${dirPath}/${newName}`);
model.refreshCallback();
});
setEntryManagerProps(undefined);
},
});
}, [dirPath]);
const handleFileContextMenu = useCallback(
(e: any) => {
e.preventDefault();
e.stopPropagation();
let openNativeLabel = "Open Directory in File Manager";
if (PLATFORM == "darwin") {
openNativeLabel = "Open Directory in Finder";
} else if (PLATFORM == "win32") {
openNativeLabel = "Open Directory in Explorer";
}
const menu: ContextMenuItem[] = [
{
label: "New File",
click: () => {
newFile();
},
},
{
label: "New Folder",
click: () => {
newDirectory();
},
},
{
type: "separator",
},
// TODO: Only show this option for local files, resolve correct host path if connection is WSL
{
label: openNativeLabel,
click: () => {
console.log(`opening ${dirPath}`);
getApi().openNativePath(dirPath);
},
},
];
menu.push({
label: "Open Terminal in New Block",
click: async () => {
const termBlockDef: BlockDef = {
meta: {
controller: "shell",
view: "term",
"cmd:cwd": dirPath,
},
};
await createBlock(termBlockDef);
},
});
ContextMenuModel.showContextMenu(menu, e);
},
[setRefreshVersion, conn, newFile, newDirectory, dirPath]
);
return (
<div
className="dir-table-container"
onChangeCapture={(e) => {
const event = e as React.ChangeEvent<HTMLInputElement>;
setSearchText(event.target.value.toLowerCase());
}}
// onFocusCapture={() => document.getSelection().collapseToEnd()}
>
<DirectoryTable
model={model}
data={filteredData}
search={searchText}
focusIndex={focusIndex}
setFocusIndex={setFocusIndex}
setSearch={setSearchText}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
/>
</div>
<Fragment>
<div
ref={refs.setReference}
className="dir-table-container"
onChangeCapture={(e) => {
const event = e as React.ChangeEvent<HTMLInputElement>;
if (!entryManagerProps) {
setSearchText(event.target.value.toLowerCase());
}
}}
{...getReferenceProps()}
onContextMenu={(e) => handleFileContextMenu(e)}
>
<DirectoryTable
model={model}
data={filteredData}
search={searchText}
focusIndex={focusIndex}
setFocusIndex={setFocusIndex}
setSearch={setSearchText}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
entryManagerOverlayPropsAtom={entryManagerPropsAtom}
newFile={newFile}
newDirectory={newDirectory}
/>
</div>
{entryManagerProps && (
<EntryManagerOverlay
{...entryManagerProps}
forwardRef={refs.setFloating}
style={floatingStyles}
getReferenceProps={getFloatingProps}
/>
)}
</Fragment>
);
}

View File

@ -61,6 +61,33 @@ func (fs *FileService) StatFile(connection string, path string) (*wshrpc.FileInf
return wshclient.RemoteFileInfoCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
}
func (fs *FileService) Mkdir(connection string, path string) error {
if connection == "" {
connection = wshrpc.LocalConnName
}
connRoute := wshutil.MakeConnectionRouteId(connection)
client := wshserver.GetMainRpcClient()
return wshclient.RemoteMkdirCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
}
func (fs *FileService) TouchFile(connection string, path string) error {
if connection == "" {
connection = wshrpc.LocalConnName
}
connRoute := wshutil.MakeConnectionRouteId(connection)
client := wshserver.GetMainRpcClient()
return wshclient.RemoteFileTouchCommand(client, path, &wshrpc.RpcOpts{Route: connRoute})
}
func (fs *FileService) Rename(connection string, path string, newPath string) error {
if connection == "" {
connection = wshrpc.LocalConnName
}
connRoute := wshutil.MakeConnectionRouteId(connection)
client := wshserver.GetMainRpcClient()
return wshclient.RemoteFileRenameCommand(client, [2]string{path, newPath}, &wshrpc.RpcOpts{Route: connRoute})
}
func (fs *FileService) ReadFile_Meta() tsgenmeta.MethodMeta {
return tsgenmeta.MethodMeta{
Desc: "read file",

View File

@ -259,6 +259,24 @@ func RemoteFileJoinCommand(w *wshutil.WshRpc, data []string, opts *wshrpc.RpcOpt
return resp, err
}
// command "remotefilerename", wshserver.RemoteFileRenameCommand
func RemoteFileRenameCommand(w *wshutil.WshRpc, data [2]string, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "remotefilerename", data, opts)
return err
}
// command "remotefiletouch", wshserver.RemoteFileTouchCommand
func RemoteFileTouchCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "remotefiletouch", data, opts)
return err
}
// command "remotemkdir", wshserver.RemoteMkdirCommand
func RemoteMkdirCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "remotemkdir", data, opts)
return err
}
// command "remotestreamcpudata", wshserver.RemoteStreamCpuDataCommand
func RemoteStreamCpuDataCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.TimeSeriesData] {
return sendRpcRequestResponseStreamHelper[wshrpc.TimeSeriesData](w, "remotestreamcpudata", nil, opts)

View File

@ -307,6 +307,49 @@ func (impl *ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string)
return impl.fileInfoInternal(path, true)
}
func (impl *ServerImpl) RemoteFileTouchCommand(ctx context.Context, path string) error {
cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path))
if _, err := os.Stat(cleanedPath); err == nil {
return fmt.Errorf("file %q already exists", path)
}
if err := os.MkdirAll(filepath.Dir(cleanedPath), 0755); err != nil {
return fmt.Errorf("cannot create directory %q: %w", filepath.Dir(cleanedPath), err)
}
if err := os.WriteFile(cleanedPath, []byte{}, 0644); err != nil {
return fmt.Errorf("cannot create file %q: %w", cleanedPath, err)
}
return nil
}
func (impl *ServerImpl) RemoteFileRenameCommand(ctx context.Context, pathTuple [2]string) error {
path := pathTuple[0]
newPath := pathTuple[1]
cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path))
cleanedNewPath := filepath.Clean(wavebase.ExpandHomeDirSafe(newPath))
if _, err := os.Stat(cleanedNewPath); err == nil {
return fmt.Errorf("destination file path %q already exists", path)
}
if err := os.Rename(cleanedPath, cleanedNewPath); err != nil {
return fmt.Errorf("cannot rename file %q to %q: %w", cleanedPath, cleanedNewPath, err)
}
return nil
}
func (impl *ServerImpl) RemoteMkdirCommand(ctx context.Context, path string) error {
cleanedPath := filepath.Clean(wavebase.ExpandHomeDirSafe(path))
if stat, err := os.Stat(cleanedPath); err == nil {
if stat.IsDir() {
return fmt.Errorf("directory %q already exists", path)
} else {
return fmt.Errorf("cannot create directory %q, file exists at path", path)
}
}
if err := os.MkdirAll(cleanedPath, 0755); err != nil {
return fmt.Errorf("cannot create directory %q: %w", cleanedPath, err)
}
return nil
}
func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
path, err := wavebase.ExpandHomeDir(data.Path)
if err != nil {

View File

@ -61,6 +61,7 @@ const (
Command_Test = "test"
Command_RemoteStreamFile = "remotestreamfile"
Command_RemoteFileInfo = "remotefileinfo"
Command_RemoteFileTouch = "remotefiletouch"
Command_RemoteWriteFile = "remotewritefile"
Command_RemoteFileDelete = "remotefiledelete"
Command_RemoteFileJoin = "remotefilejoin"
@ -69,6 +70,7 @@ const (
Command_Activity = "activity"
Command_GetVar = "getvar"
Command_SetVar = "setvar"
Command_RemoteMkdir = "remotemkdir"
Command_ConnStatus = "connstatus"
Command_WslStatus = "wslstatus"
@ -161,9 +163,12 @@ type WshRpcInterface interface {
// remotes
RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData]
RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error)
RemoteFileTouchCommand(ctx context.Context, path string) error
RemoteFileRenameCommand(ctx context.Context, pathTuple [2]string) error
RemoteFileDeleteCommand(ctx context.Context, path string) error
RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error
RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error)
RemoteMkdirCommand(ctx context.Context, path string) error
RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData]
// emain