diff --git a/diff.txt b/diff.txt new file mode 100644 index 000000000..b747b2832 --- /dev/null +++ b/diff.txt @@ -0,0 +1,318 @@ +diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx +index c5461a90..0d43a66c 100644 +--- a/src/app/workspace/cmdinput/textareainput.tsx ++++ b/src/app/workspace/cmdinput/textareainput.tsx +@@ -131,165 +131,177 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () + this.checkHeight(false); + this.updateSP(); + let keybindManager = GlobalModel.keybindManager; +- keybindManager.registerKeybinding("pane", "cmdinput", "any", (waveEvent) => { +- return mobx.action(() => { +- let inputRef = this.mainInputRef.current; +- if (util.isModKeyPress(waveEvent)) { +- return false; +- } +- let model = GlobalModel; +- let inputModel = model.inputModel; +- let ctrlMod = waveEvent.control || waveEvent.cmd || waveEvent.shift; +- let curLine = inputModel.getCurLine(); +- +- let lastTab = this.lastTab; +- this.lastTab = keybindManager.checkKeyPressed(waveEvent, "cmdinput:autocomplete"); +- let lastHist = this.lastHistoryUpDown; +- this.lastHistoryUpDown = false; +- +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:autocomplete")) { +- if (lastTab) { +- GlobalModel.submitCommand( +- "_compgen", +- null, +- [curLine], +- { comppos: String(curLine.length), compshow: "1", nohist: "1" }, +- true +- ); +- } else { +- GlobalModel.submitCommand( +- "_compgen", +- null, +- [curLine], +- { comppos: String(curLine.length), nohist: "1" }, +- true +- ); ++ if (GlobalModel.activeMainView.get() == "session") { ++ console.log("session"); ++ keybindManager.registerKeybinding("pane", "cmdinput", "any", (waveEvent) => { ++ return mobx.action(() => { ++ let inputRef = this.mainInputRef.current; ++ if (util.isModKeyPress(waveEvent)) { ++ return false; + } +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "generic:confirm")) { +- if (!ctrlMod) { +- if (GlobalModel.inputModel.isEmpty()) { +- let activeWindow = GlobalModel.getScreenLinesForActiveScreen(); +- let activeScreen = GlobalModel.getActiveScreen(); +- if (activeScreen != null && activeWindow != null && activeWindow.lines.length > 0) { +- activeScreen.setSelectedLine(0); +- GlobalCommandRunner.screenSelectLine("E"); +- } ++ let model = GlobalModel; ++ let inputModel = model.inputModel; ++ let ctrlMod = waveEvent.control || waveEvent.cmd || waveEvent.shift; ++ let curLine = inputModel.getCurLine(); ++ ++ let lastTab = this.lastTab; ++ this.lastTab = keybindManager.checkKeyPressed(waveEvent, "cmdinput:autocomplete"); ++ let lastHist = this.lastHistoryUpDown; ++ this.lastHistoryUpDown = false; ++ ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:autocomplete")) { ++ if (lastTab) { ++ GlobalModel.submitCommand( ++ "_compgen", ++ null, ++ [curLine], ++ { comppos: String(curLine.length), compshow: "1", nohist: "1" }, ++ true ++ ); + } else { +- setTimeout(() => GlobalModel.inputModel.uiSubmitCommand(), 0); ++ GlobalModel.submitCommand( ++ "_compgen", ++ null, ++ [curLine], ++ { comppos: String(curLine.length), nohist: "1" }, ++ true ++ ); + } + return true; + } +- inputRef.setRangeText("\n", inputRef.selectionStart, inputRef.selectionEnd, "end"); +- GlobalModel.inputModel.setCurLine(inputRef.value); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "generic:cancel")) { +- let inputModel = GlobalModel.inputModel; +- inputModel.toggleInfoMsg(); +- console.log("hello?", inputModel.inputMode.get()); +- if (inputModel.inputMode.get() != null) { +- inputModel.resetInputMode(); +- console.log("hello? 2"); +- } +- console.log("hello 3?"); +- inputModel.closeAIAssistantChat(true); +- console.log("hello 4?"); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:expandInput")) { +- let inputModel = GlobalModel.inputModel; +- inputModel.toggleExpandInput(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:clearInput")) { +- inputModel.resetInput(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:cutLineLeftOfCursor")) { +- this.controlU(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:previousHistoryItem")) { +- this.controlP(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:nextHistoryItem")) { +- this.controlN(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:cutWordLeftOfCursor")) { +- this.controlW(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:paste")) { +- this.controlY(); +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:openHistory")) { +- inputModel.openHistory(); +- return true; +- } +- if (keybindManager.checkKeysPressed(waveEvent, ["generic:selectAbove", "generic:selectBelow"])) { +- if (!inputModel.isHistoryLoaded()) { +- if (keybindManager.checkKeyPressed(waveEvent, "generic:selectAbove")) { +- this.lastHistoryUpDown = true; +- inputModel.loadHistory(false, 1, "screen"); ++ if (keybindManager.checkKeyPressed(waveEvent, "generic:confirm")) { ++ if (!ctrlMod) { ++ if (GlobalModel.inputModel.isEmpty()) { ++ let activeWindow = GlobalModel.getScreenLinesForActiveScreen(); ++ let activeScreen = GlobalModel.getActiveScreen(); ++ if (activeScreen != null && activeWindow != null && activeWindow.lines.length > 0) { ++ activeScreen.setSelectedLine(0); ++ GlobalCommandRunner.screenSelectLine("E"); ++ } ++ } else { ++ setTimeout(() => GlobalModel.inputModel.uiSubmitCommand(), 0); ++ } ++ return true; + } ++ inputRef.setRangeText("\n", inputRef.selectionStart, inputRef.selectionEnd, "end"); ++ GlobalModel.inputModel.setCurLine(inputRef.value); + return true; + } +- // invisible history movement +- let linePos = this.getLinePos(inputRef); +- if (keybindManager.checkKeyPressed(waveEvent, "generic:selectAbove")) { +- if (!lastHist && linePos.linePos > 1) { +- // regular arrow +- return false; ++ if (keybindManager.checkKeyPressed(waveEvent, "generic:cancel")) { ++ let inputModel = GlobalModel.inputModel; ++ inputModel.toggleInfoMsg(); ++ if (inputModel.inputMode.get() != null) { ++ inputModel.resetInputMode(); + } +- inputModel.moveHistorySelection(1); +- this.lastHistoryUpDown = true; ++ inputModel.closeAIAssistantChat(true); + return true; + } +- if (keybindManager.checkKeyPressed(waveEvent, "generic:selectBelow")) { +- if (!lastHist && linePos.linePos < linePos.numLines) { +- // regular arrow +- return false; ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:expandInput")) { ++ let inputModel = GlobalModel.inputModel; ++ inputModel.toggleExpandInput(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:clearInput")) { ++ inputModel.resetInput(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:cutLineLeftOfCursor")) { ++ this.controlU(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:previousHistoryItem")) { ++ this.controlP(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:nextHistoryItem")) { ++ this.controlN(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:cutWordLeftOfCursor")) { ++ this.controlW(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:paste")) { ++ this.controlY(); ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:openHistory")) { ++ inputModel.openHistory(); ++ return true; ++ } ++ if (keybindManager.checkKeysPressed(waveEvent, ["generic:selectAbove", "generic:selectBelow"])) { ++ if (!inputModel.isHistoryLoaded()) { ++ if (keybindManager.checkKeyPressed(waveEvent, "generic:selectAbove")) { ++ this.lastHistoryUpDown = true; ++ inputModel.loadHistory(false, 1, "screen"); ++ } ++ return true; ++ } ++ // invisible history movement ++ let linePos = this.getLinePos(inputRef); ++ if (keybindManager.checkKeyPressed(waveEvent, "generic:selectAbove")) { ++ if (!lastHist && linePos.linePos > 1) { ++ // regular arrow ++ return false; ++ } ++ inputModel.moveHistorySelection(1); ++ this.lastHistoryUpDown = true; ++ return true; ++ } ++ if (keybindManager.checkKeyPressed(waveEvent, "generic:selectBelow")) { ++ if (!lastHist && linePos.linePos < linePos.numLines) { ++ // regular arrow ++ return false; ++ } ++ inputModel.moveHistorySelection(-1); ++ this.lastHistoryUpDown = true; ++ return true; ++ } ++ } ++ if ( ++ keybindManager.checkKeysPressed(waveEvent, [ ++ "generic:selectPageAbove", ++ "generic:selectPageBelow", ++ ]) ++ ) { ++ let infoScroll = inputModel.hasScrollingInfoMsg(); ++ if (infoScroll) { ++ let div = document.querySelector(".cmd-input-info"); ++ let amt = pageSize(div); ++ scrollDiv( ++ div, ++ keybindManager.checkKeyPressed(waveEvent, "generic:selectPageAbove") ? -amt : amt ++ ); + } +- inputModel.moveHistorySelection(-1); +- this.lastHistoryUpDown = true; + return true; + } +- } +- if ( +- keybindManager.checkKeysPressed(waveEvent, ["generic:selectPageAbove", "generic:selectPageBelow"]) +- ) { +- let infoScroll = inputModel.hasScrollingInfoMsg(); +- if (infoScroll) { +- let div = document.querySelector(".cmd-input-info"); +- let amt = pageSize(div); +- scrollDiv( +- div, +- keybindManager.checkKeyPressed(waveEvent, "generic:selectPageAbove") ? -amt : amt +- ); ++ if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:openAIChat")) { ++ inputModel.openAIAssistantChat(); ++ return true; + } +- return true; +- } +- if (keybindManager.checkKeyPressed(waveEvent, "cmdinput:openAIChat")) { +- inputModel.openAIAssistantChat(); +- return true; +- } +- // console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e); +- return false; +- })(); +- }); ++ // console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e); ++ return false; ++ })(); ++ }); ++ } else { ++ console.log("not session"); ++ keybindManager.unregisterDomain("cmdinput"); ++ } + } + + componentWillUnmount(): void { ++ console.log("component unmounted??"); + let keybindManager = GlobalModel.keybindManager; + keybindManager.unregisterDomain("cmdinput"); + } + + componentDidUpdate() { ++ console.log("component update"); ++ if (GlobalModel.activeMainView.get() != "session") { ++ console.log("unregistering"); ++ let keybindManager = GlobalModel.keybindManager; ++ keybindManager.unregisterDomain("cmdinput"); ++ } + let activeScreen = GlobalModel.getActiveScreen(); + if (activeScreen != null) { + let focusType = activeScreen.focusType.get(); diff --git a/prettify.sh b/prettify.sh new file mode 100755 index 000000000..073d40b94 --- /dev/null +++ b/prettify.sh @@ -0,0 +1,4 @@ +dirs=$(git diff main --name-only --diff-filter d | grep -e '\.[tj]sx\?$' | xargs) +echo dirs: $dirs +node_modules/prettier/bin-prettier.js --write $dirs + diff --git a/prettifyAndPush.sh b/prettifyAndPush.sh new file mode 100755 index 000000000..56763ede0 --- /dev/null +++ b/prettifyAndPush.sh @@ -0,0 +1,5 @@ +dirs=$(git diff main --name-only --diff-filter d | grep -e '\.[tj]sx\?$' | xargs) +node_modules/prettier/bin-prettier.js --write $dirs +git add $dirs +git commit --amend +git push diff --git a/waveshell/pkg/packet/packet.go b/waveshell/pkg/packet/packet.go index 553e16e6f..a520ba70b 100644 --- a/waveshell/pkg/packet/packet.go +++ b/waveshell/pkg/packet/packet.go @@ -64,6 +64,8 @@ const ( FileStatPacketStr = "filestat" LogPacketStr = "log" // logging packet (sent from waveshell back to server) ShellStatePacketStr = "shellstate" + ListDirPacketStr = "listdir" + SearchDirPacketStr = "searchdir" RpcInputPacketStr = "rpcinput" // rpc-followup SudoRequestPacketStr = "sudorequest" SudoResponsePacketStr = "sudoresponse" @@ -120,6 +122,8 @@ func init() { TypeStrToFactory[WriteFileDonePacketStr] = reflect.TypeOf(WriteFileDonePacketType{}) TypeStrToFactory[LogPacketStr] = reflect.TypeOf(LogPacketType{}) TypeStrToFactory[ShellStatePacketStr] = reflect.TypeOf(ShellStatePacketType{}) + TypeStrToFactory[ListDirPacketStr] = reflect.TypeOf(ListDirPacketType{}) + TypeStrToFactory[SearchDirPacketStr] = reflect.TypeOf(SearchDirPacketType{}) TypeStrToFactory[FileStatPacketStr] = reflect.TypeOf(FileStatPacketType{}) TypeStrToFactory[RpcInputPacketStr] = reflect.TypeOf(RpcInputPacketType{}) TypeStrToFactory[SudoRequestPacketStr] = reflect.TypeOf(SudoRequestPacketType{}) @@ -133,6 +137,8 @@ func init() { var _ RpcPacketType = (*ReInitPacketType)(nil) var _ RpcPacketType = (*StreamFilePacketType)(nil) var _ RpcPacketType = (*WriteFilePacketType)(nil) + var _ RpcPacketType = (*ListDirPacketType)(nil) + var _ RpcPacketType = (*SearchDirPacketType)(nil) var _ RpcResponsePacketType = (*CmdStartPacketType)(nil) var _ RpcResponsePacketType = (*ResponsePacketType)(nil) @@ -449,8 +455,12 @@ func MakeFileStatPacketType() *FileStatPacketType { return &FileStatPacketType{Type: FileStatPacketStr} } -func MakeFileStatPacketFromFileInfo(finfo fs.FileInfo, err string, done bool) *FileStatPacketType { +func MakeFileStatPacketFromFileInfo(listDirPk *ListDirPacketType, finfo fs.FileInfo, err string, done bool) *FileStatPacketType { resp := MakeFileStatPacketType() + if listDirPk != nil { + resp.RespId = listDirPk.ReqId + resp.Path = listDirPk.Path + } resp.Error = err resp.Done = done @@ -464,6 +474,50 @@ func MakeFileStatPacketFromFileInfo(finfo fs.FileInfo, err string, done bool) *F return resp } +type ListDirPacketType struct { + Type string `json:"type"` + ReqId string `json:"reqid"` + Path string `json:"path"` +} + +func (*ListDirPacketType) GetType() string { + return ListDirPacketStr +} + +func (p *ListDirPacketType) GetReqId() string { + return p.ReqId +} + +func MakeListDirPacket() *ListDirPacketType { + return &ListDirPacketType{Type: ListDirPacketStr} +} + +type SearchDirPacketType struct { + Type string `json:"type"` + ReqId string `json:"reqid"` + Path string `json:"path"` + SearchQuery string `json:"searchquery"` +} + +func (*SearchDirPacketType) GetType() string { + return SearchDirPacketStr +} + +func (p *SearchDirPacketType) GetReqId() string { + return p.ReqId +} + +func (p *SearchDirPacketType) ConvertToListDir() *ListDirPacketType { + rtn := MakeListDirPacket() + rtn.ReqId = p.ReqId + rtn.Path = p.Path + return rtn +} + +func MakeSearchDirPacket() *SearchDirPacketType { + return &SearchDirPacketType{Type: SearchDirPacketStr} +} + type StreamFilePacketType struct { Type string `json:"type"` ReqId string `json:"reqid"` diff --git a/waveshell/pkg/server/server.go b/waveshell/pkg/server/server.go index 1de50a0e9..dc4d4ada7 100644 --- a/waveshell/pkg/server/server.go +++ b/waveshell/pkg/server/server.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "sort" "strings" "sync" @@ -660,6 +661,97 @@ func (m *MServer) streamFile(pk *packet.StreamFilePacketType) { return } +func (m *MServer) writeListDirErrPacket(err error, reqId string) { + resp := packet.MakeFileStatPacketType() + resp.RespId = reqId + resp.Error = fmt.Sprintf("Error in list dir: %v", err) + resp.Done = true + m.Sender.SendPacket(resp) +} + +func (m *MServer) ListDir(listDirPk *packet.ListDirPacketType) { + dirEntries, err := os.ReadDir(listDirPk.Path) + var readDirError string = "" + if err != nil { + readDirError = fmt.Sprintf("error in list dir: %v", err) + } + curDirStat, err := os.Stat(listDirPk.Path) + if err != nil { + m.writeListDirErrPacket(err, listDirPk.ReqId) + } + resp := packet.MakeFileStatPacketFromFileInfo(listDirPk, curDirStat, readDirError, false) + resp.Name = "." + m.Sender.SendPacket(resp) + curDirStat, err = os.Stat(filepath.Join(listDirPk.Path, "..")) + if err != nil { + m.writeListDirErrPacket(err, listDirPk.ReqId) + return + } + resp = packet.MakeFileStatPacketFromFileInfo(listDirPk, curDirStat, readDirError, len(dirEntries) == 0) + resp.Name = ".." + m.Sender.SendPacket(resp) + + for index := 0; index < len(dirEntries); index++ { + dirEntry := dirEntries[index] + dirEntryFileInfo, err := dirEntry.Info() + if err != nil { + m.writeListDirErrPacket(err, listDirPk.ReqId) + return + } + done := index == len(dirEntries)-1 + resp = packet.MakeFileStatPacketFromFileInfo(listDirPk, dirEntryFileInfo, readDirError, done) + m.Sender.SendPacket(resp) + } +} + +func (m *MServer) SearchDir(searchDirPk *packet.SearchDirPacketType) { + searchEmpty := true + foundRoot := false + err := filepath.WalkDir(searchDirPk.Path, func(path string, dirEntry fs.DirEntry, err error) error { + if err != nil { + errString := fmt.Sprintf("%v", err) + if strings.Contains(errString, "operation not permitted") { + return filepath.SkipDir + } + } + fileName := filepath.Base(path) + match, err := regexp.MatchString(searchDirPk.SearchQuery, fileName) + if err != nil { + return err + } + if match { + base.Logf("matched file: %v %v", path, searchDirPk.SearchQuery) + // special case where walkdir includes the current path, which messes up the stat pk + rootName := filepath.Base(searchDirPk.Path) + if !foundRoot && fileName == rootName { + foundRoot = true + return nil + } + dirEntryFileInfo, err := dirEntry.Info() + if err != nil { + return err + } + searchEmpty = false + resp := packet.MakeFileStatPacketFromFileInfo(searchDirPk.ConvertToListDir(), dirEntryFileInfo, "", false) + m.Sender.SendPacket(resp) + } + return nil + }) + if err != nil { + m.writeListDirErrPacket(err, searchDirPk.ReqId) + } else { + searchError := "" + if searchEmpty { + searchError = "none" + } + resp := packet.MakeFileStatPacketType() + resp.Error = searchError + resp.Done = true + m.Sender.SendPacket(resp) + } + +} + func int64Min(v1 int64, v2 int64) int64 { if v1 < v2 { return v1 @@ -703,6 +795,14 @@ func (m *MServer) ProcessRpcPacket(pk packet.RpcPacketType) { go m.writeFile(writePk, wfc) return } + if listDirPk, ok := pk.(*packet.ListDirPacketType); ok { + go m.ListDir(listDirPk) + return + } + if searchDirPk, ok := pk.(*packet.SearchDirPacketType); ok { + go m.SearchDir(searchDirPk) + return + } m.Sender.SendErrorResponse(reqId, fmt.Errorf("invalid rpc type '%s'", pk.GetType())) } diff --git a/wavesrv/cmd/main-server.go b/wavesrv/cmd/main-server.go index be98f4cb0..3508045de 100644 --- a/wavesrv/cmd/main-server.go +++ b/wavesrv/cmd/main-server.go @@ -1033,7 +1033,7 @@ func configDirHandler(w http.ResponseWriter, r *http.Request) { var files []*packet.FileStatPacketType for index := 0; index < len(entries); index++ { curEntry := entries[index] - curFile := packet.MakeFileStatPacketFromFileInfo(curEntry, "", false) + curFile := packet.MakeFileStatPacketFromFileInfo(nil, curEntry, "", false) files = append(files, curFile) } dirListJson, err := json.Marshal(files) diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 47b01fcb9..76eb5d966 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -279,6 +279,7 @@ func init() { registerCmdFn("set", SetCommand) registerCmdFn("view:stat", ViewStatCommand) + registerCmdFn("view:dir", ViewDirCommand) registerCmdFn("view:test", ViewTestCommand) registerCmdFn("edit:test", EditTestCommand) @@ -287,6 +288,8 @@ func init() { registerCmdFn("codeedit", CodeEditCommand) registerCmdFn("codeview", CodeEditCommand) + registerCmdFn("searchdir", SearchDirCommand) + registerCmdFn("imageview", ImageViewCommand) registerCmdFn("mdview", MarkdownViewCommand) registerCmdFn("markdownview", MarkdownViewCommand) @@ -1323,6 +1326,15 @@ func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remoteWsh return } defer localFile.Close() + fileStat, err := localFile.Stat() + if err != nil { + writeStringToPty(ctx, cmd, fmt.Sprintf("error: could not get file stat: %v", err), &outputPos) + return + } + if fileStat.IsDir() { + writeStringToPty(ctx, cmd, "Cant copy a directory, try zipping it up first", &outputPos) + return + } writePk := packet.MakeWriteFilePacket() writePk.ReqId = uuid.New().String() writePk.Path = destPath @@ -1337,11 +1349,6 @@ func doCopyLocalFileToRemote(ctx context.Context, cmd *sstore.CmdType, remoteWsh writeStringToPty(ctx, cmd, fmt.Sprintf("Write ready packet error: %v\r\n", err), &outputPos) return } - fileStat, err := localFile.Stat() - if err != nil { - writeStringToPty(ctx, cmd, fmt.Sprintf("error: could not get file stat: %v", err), &outputPos) - return - } fileSizeBytes := fileStat.Size() bytesWritten := int64(0) lastFileTransferPercentage := float64(0) @@ -1434,6 +1441,10 @@ func doCopyRemoteFileToRemote(ctx context.Context, cmd *sstore.CmdType, sourceWs writeStringToPty(ctx, cmd, fmt.Sprintf("Response packet has error: %v\r\n", err), &outputPos) return } + if resp.Info.IsDir { + writeStringToPty(ctx, cmd, "Cant copy a directory, try zipping it up first", &outputPos) + return + } fileSizeBytes := resp.Info.Size if fileSizeBytes == 0 { writeStringToPty(ctx, cmd, "Source file does not exist or is empty - exiting\r\n", &outputPos) @@ -1525,6 +1536,10 @@ func doCopyLocalFileToLocal(ctx context.Context, cmd *sstore.CmdType, sourcePath writeStringToPty(ctx, cmd, fmt.Sprintf("error getting filestat %v", err), &outputPos) return } + if sourceFileStat.IsDir() { + writeStringToPty(ctx, cmd, "Cant copy a directory, try zipping it up first", &outputPos) + return + } fileSizeBytes := sourceFileStat.Size() writeStringToPty(ctx, cmd, fmt.Sprintf("Source File Size: %v\r\n", prettyPrintByteSize(fileSizeBytes)), &outputPos) destFile, err := os.Create(destPath) @@ -1571,6 +1586,10 @@ func doCopyRemoteFileToLocal(ctx context.Context, cmd *sstore.CmdType, remoteWsh writeStringToPty(ctx, cmd, fmt.Sprintf("Response packet has error: %v\r\n", err), &outputPos) return } + if resp.Info.IsDir { + writeStringToPty(ctx, cmd, "Cant copy a directory, try zipping it up first", &outputPos) + return + } fileSizeBytes := resp.Info.Size if fileSizeBytes == 0 { writeStringToPty(ctx, cmd, "Source file doesn't exist or file is empty - exiting\r\n", &outputPos) @@ -1610,6 +1629,7 @@ func doCopyRemoteFileToLocal(ctx context.Context, cmd *sstore.CmdType, remoteWsh fileTransferPercentage = float64(bytesWritten) / float64(fileSizeBytes) if fileTransferPercentage-lastFileTransferPercentage > float64(0.05) { + log.Printf("updating percentage\n") writeStringToPty(ctx, cmd, "-", &outputPos) lastFileTransferPercentage = fileTransferPercentage } @@ -1647,6 +1667,15 @@ func parseCopyFileParam(info string) (remote string, path string, err error) { } } +func fileIsDir(ctx context.Context, ids resolvedIds, remote *ResolvedRemote, filePath string) bool { + fileStat, err := getSingleFileStat(ctx, ids, remote, filePath) + log.Printf("file is dir: %v", err) + if err != nil { + return false + } + return fileStat.IsDir +} + func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("usage: /copyfile [file to copy] local=[path to copy to on local]") @@ -1716,9 +1745,10 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb sourceFullPath = filepath.Join(sourceCwd, sourcePathWithHome) } } - if destPath[len(destPath)-1:] == "/" { + if destPath[len(destPath)-1:] == "/" || fileIsDir(ctx, ids, destRemoteId, destPath) { sourceFileName := filepath.Base(sourceFullPath) destPath = filepath.Join(destPath, sourceFileName) + log.Printf("destPath: %v", destPath) } destWsh := destRemoteId.Waveshell if destWsh == nil { @@ -1746,15 +1776,15 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb cmd, err := makeDynCmd(ctx, "copy file", ids, pk.GetRawStr(), *pkTermOpts, nil) writeStringToPty(ctx, cmd, outputStr, &outputPos) if err != nil { - // TODO tricky error since the command was a success, but we can't show the output - return nil, err + return nil, fmt.Errorf("cannot make termopts: %w", err) } - update, err := addLineForCmd(ctx, "/copy file", false, ids, cmd, "", nil) + pkTermOpts := convertTermOpts(termOpts) + cmd, err := makeDynCmd(ctx, "copy file", ids, pk.GetRawStr(), *pkTermOpts, nil) + writeStringToPty(ctx, cmd, outputStr, &outputPos) if err != nil { // TODO tricky error since the command was a success, but we can't show the output return nil, err } - update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive)) if destRemote != ConnectedRemote && destRemoteId != nil && !destRemoteId.RState.IsConnected() { writeStringToPty(ctx, cmd, fmt.Sprintf("Attempting to autoconnect to remote %v\r\n", destRemote), &outputPos) err = destRemoteId.Waveshell.TryAutoConnect() @@ -1773,8 +1803,7 @@ func CopyFileCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb writeStringToPty(ctx, cmd, "Auto connect successful\r\n", &outputPos) } } - scbus.MainUpdateBus.DoScreenUpdate(cmd.ScreenId, update) - update = scbus.MakeUpdatePacket() + update := scbus.MakeUpdatePacket() if destRemote == LocalRemote && sourceRemote == LocalRemote { go doCopyLocalFileToLocal(context.Background(), cmd, sourceFullPath, destFullPath, outputPos) } else if destRemote == LocalRemote && sourceRemote != LocalRemote { @@ -5039,6 +5068,10 @@ func SetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.Up func makeStreamFilePk(ids resolvedIds, pk *scpacket.FeCommandPacketType) (*packet.StreamFilePacketType, error) { cwd := ids.Remote.FeState["cwd"] fileArg := pk.Args[0] + return makeStreamFilePkFromParams(cwd, fileArg) +} + +func makeStreamFilePkFromParams(cwd string, fileArg string) (*packet.StreamFilePacketType, error) { if fileArg == "" { return nil, fmt.Errorf("/view:stat file argument must be set (cannot be empty)") } @@ -5050,6 +5083,7 @@ func makeStreamFilePk(ids resolvedIds, pk *scpacket.FeCommandPacketType) (*packe streamPk.Path = filepath.Join(cwd, fileArg) } return streamPk, nil + } func ViewStatCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { @@ -5060,42 +5094,18 @@ func ViewStatCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb if err != nil { return nil, err } - streamPk, err := makeStreamFilePk(ids, pk) + fileArg := pk.Args[0] + statPk, streamPk, err := getFileStat(ctx, ids, fileArg) if err != nil { return nil, err } - streamPk.StatOnly = true - wsh := ids.Remote.Waveshell - iter, err := wsh.StreamFile(ctx, streamPk) - if err != nil { - return nil, fmt.Errorf("/view:stat error: %v", err) - } - defer iter.Close() - respIf, err := iter.Next(ctx) - if err != nil { - return nil, fmt.Errorf("/view:stat error getting response: %v", err) - } - resp, ok := respIf.(*packet.StreamFileResponseType) - if !ok { - return nil, fmt.Errorf("/view:stat error, bad response packet type: %T", respIf) - } - if resp.Error != "" { - return nil, fmt.Errorf("/view:stat error: %s", resp.Error) - } - if resp.Info == nil { - return nil, fmt.Errorf("/view:stat error, no file info") - } var buf bytes.Buffer - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "path", resp.Info.Name)) - buf.WriteString(fmt.Sprintf(" %-15s %d\n", "size", resp.Info.Size)) - modTs := time.UnixMilli(resp.Info.ModTs) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "path", statPk.Name)) + buf.WriteString(fmt.Sprintf(" %-15s %d\n", "size", statPk.Size)) + modTs := statPk.ModTs buf.WriteString(fmt.Sprintf(" %-15s %s\n", "modts", modTs.Format(TsFormatStr))) - buf.WriteString(fmt.Sprintf(" %-15s %v\n", "isdir", resp.Info.IsDir)) - modeStr := fs.FileMode(resp.Info.Perm).String() - if len(modeStr) > 9 { - modeStr = modeStr[len(modeStr)-9:] - } - buf.WriteString(fmt.Sprintf(" %-15s %s\n", "perms", modeStr)) + buf.WriteString(fmt.Sprintf(" %-15s %v\n", "isdir", statPk.IsDir)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "perms", statPk.ModeStr)) update := scbus.MakeUpdatePacket() update.AddUpdate(sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("view stat %q", streamPk.Path), @@ -5104,6 +5114,62 @@ func ViewStatCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb return update, nil } +func getSingleFileStat(ctx context.Context, ids resolvedIds, remote *ResolvedRemote, fileArg string) (*packet.FileStatPacketType, error) { + if remote.RemoteCopy.IsLocal() { + fileStat, err := os.Stat(fileArg) + if err != nil { + return nil, err + } + statPk := packet.MakeFileStatPacketFromFileInfo(nil, fileStat, "", true) + return statPk, nil + } else { + statPk, _, err := getFileStat(ctx, ids, fileArg) + if err != nil { + return nil, err + } + return statPk, nil + } +} + +func getFileStat(ctx context.Context, ids resolvedIds, fileArg string) (*packet.FileStatPacketType, *packet.StreamFilePacketType, error) { + streamPk, err := makeStreamFilePkFromParams(ids.Remote.FeState["cwd"], fileArg) + if err != nil { + return nil, nil, err + } + streamPk.StatOnly = true + wsh := ids.Remote.Waveshell + iter, err := wsh.StreamFile(ctx, streamPk) + if err != nil { + return nil, nil, fmt.Errorf("/view:stat error: %v", err) + } + defer iter.Close() + respIf, err := iter.Next(ctx) + if err != nil { + return nil, nil, fmt.Errorf("/view:stat error getting response: %v", err) + } + resp, ok := respIf.(*packet.StreamFileResponseType) + if !ok { + return nil, nil, fmt.Errorf("/view:stat error, bad response packet type: %T", respIf) + } + if resp.Error != "" { + return nil, nil, fmt.Errorf("/view:stat error: %s", resp.Error) + } + if resp.Info == nil { + return nil, nil, fmt.Errorf("/view:stat error, no file info") + } + statPk := packet.MakeFileStatPacketType() + statPk.Name = resp.Info.Name + statPk.Size = resp.Info.Size + statPk.ModTs = time.UnixMilli(resp.Info.ModTs) + statPk.IsDir = resp.Info.IsDir + statPk.Perm = resp.Info.Perm + statPk.ModeStr = fs.FileMode(statPk.Perm).String() + if len(statPk.ModeStr) > 9 { + statPk.ModeStr = statPk.ModeStr[len(statPk.ModeStr)-9:] + } + return statPk, streamPk, nil +} + func ViewTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("/view:test requires an argument (file name)") @@ -5388,6 +5454,209 @@ func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) return update, nil } +func StatDir(ctx context.Context, ids resolvedIds, path string, fileCallback func(pk *packet.FileStatPacketType, done bool, err error)) { + statPk, _, err := getFileStat(ctx, ids, path) + log.Printf("statPk: %\n", statPk) + if err != nil { + fileCallback(nil, true, err) + return + } + if !statPk.IsDir { + fileCallback(statPk, true, nil) + return + } else { + cwd := ids.Remote.FeState["cwd"] + listDirPk := packet.MakeListDirPacket() + listDirPk.ReqId = uuid.New().String() + if filepath.IsAbs(path) { + listDirPk.Path = path + } else { + listDirPk.Path = filepath.Join(cwd, path) + } + msh := ids.Remote.Waveshell + listDirIter, err := msh.ListDir(ctx, listDirPk) + if err != nil { + fileCallback(nil, true, err) + return + } + defer listDirIter.Close() + for { + respIf, err := listDirIter.Next(ctx) + if err != nil { + fileCallback(nil, true, err) + return + } + resp, ok := respIf.(*packet.FileStatPacketType) + if !ok || resp == nil { + fileCallback(nil, true, fmt.Errorf("error unmarshalling file stat response type %v %v", resp, respIf)) + return + } + if resp.Error != "" { + err = fmt.Errorf(resp.Error) + } else { + err = nil + } + fileCallback(resp, resp.Done, err) + if resp.Done { + return + } + } + } +} + +func SearchDir(ctx context.Context, ids resolvedIds, path string, searchQuery string, fileCallback func(pk *packet.FileStatPacketType, done bool, err error)) { + log.Printf("running searchdir %v %v \n", path, searchQuery) + statPk, _, err := getFileStat(ctx, ids, path) + if err != nil { + fileCallback(nil, true, err) + return + } + if !statPk.IsDir { + match, err := regexp.MatchString(searchQuery, statPk.Name) + if err != nil { + fileCallback(nil, true, err) + } + if match { + fileCallback(statPk, true, nil) + } else { + fileCallback(nil, true, nil) + } + return + } + + cwd := ids.Remote.FeState["cwd"] + searchDirPk := packet.MakeSearchDirPacket() + searchDirPk.ReqId = uuid.New().String() + if filepath.IsAbs(path) { + searchDirPk.Path = path + } else { + searchDirPk.Path = filepath.Join(cwd, path) + } + searchDirPk.SearchQuery = searchQuery + msh := ids.Remote.Waveshell + searchDirIter, err := msh.SearchDir(ctx, searchDirPk) + if err != nil { + fileCallback(nil, true, err) + return + } + defer searchDirIter.Close() + for { + respIf, err := searchDirIter.Next(ctx) + log.Printf("got next\n") + if err != nil { + fileCallback(nil, true, err) + return + } + resp, ok := respIf.(*packet.FileStatPacketType) + if !ok || resp == nil { + fileCallback(nil, true, fmt.Errorf("error unmarshalling file stat response type %v %v", resp, respIf)) + return + } + log.Printf("resp: %v\n", resp) + if resp.Error == "none" { + fileCallback(nil, true, nil) + return + } else if resp.Error != "" { + err = fmt.Errorf(resp.Error) + } else { + err = nil + } + if resp.Done { + return + } + fileCallback(resp, resp.Done, err) + } +} + +func SearchDirCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + path := ids.Remote.FeState["cwd"] + if len(pk.Args) > 0 { + path = pk.Args[0] + } + var searchQuery string + if len(pk.Args) > 1 { + searchQuery = pk.Args[1] + } else { + return nil, fmt.Errorf("no search query specified - usage /searchdir [path] [query]") + } + lineId := pk.Kwargs["lineid"] + screenId := pk.Kwargs["screenid"] + cmd, err := sstore.GetCmdByScreenId(ctx, screenId, lineId) + if err != nil { + return nil, err + } + outputPty := resolveBool(pk.Kwargs["outputpty"], false) + if outputPty { + ids.Remote.Waveshell.ResetDataPos(base.MakeCommandKey(screenId, lineId)) + err = sstore.ClearCmdPtyFile(ctx, screenId, lineId) + if err != nil { + return nil, fmt.Errorf("error clearing existing pty file: %v", err) + } + } + go func() { + var outputPos int64 + searchCtx := context.Background() + SearchDir(searchCtx, ids, path, searchQuery, func(statPk *packet.FileStatPacketType, done bool, err error) { + log.Printf("got callback\n") + if err != nil { + log.Printf("got error: %v\n", err) + } else { + err = writePacketToPty(searchCtx, cmd, statPk, &outputPos) + if err != nil { + log.Printf("err writing packet to pty: %v\n", err) + } + } + }) + }() + update := scbus.MakeUpdatePacket() + return update, nil +} + +func ViewDirCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { + ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected) + if err != nil { + return nil, err + } + path := ids.Remote.FeState["cwd"] + if len(pk.Args) > 0 { + path = pk.Args[0] + } + lineId := pk.Kwargs["lineid"] + screenId := pk.Kwargs["screenid"] + cmd, err := sstore.GetCmdByScreenId(ctx, screenId, lineId) + if err != nil { + return nil, err + } + outputPty := resolveBool(pk.Kwargs["outputpty"], false) + if outputPty { + ids.Remote.Waveshell.ResetDataPos(base.MakeCommandKey(screenId, lineId)) + err = sstore.ClearCmdPtyFile(ctx, screenId, lineId) + if err != nil { + return nil, fmt.Errorf("error clearing existing pty file: %v", err) + } + } + go func() { + var outputPos int64 + statCtx := context.Background() + StatDir(statCtx, ids, path, func(statPk *packet.FileStatPacketType, done bool, err error) { + if err != nil { + log.Printf("got error: %v\n", err) + } else { + err = writePacketToPty(statCtx, cmd, statPk, &outputPos) + if err != nil { + log.Printf("err writing packet to pty: %v\n", err) + } + } + }) + }() + update := scbus.MakeUpdatePacket() + return update, nil +} + func EditTestCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { if len(pk.Args) == 0 { return nil, fmt.Errorf("/edit:test requires an argument (file name)") @@ -5982,6 +6251,52 @@ func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc if err != nil { return nil, fmt.Errorf("error updating client ai base url: %v", err) } + feOpts := clientData.FeOpts + feOpts.SudoPwStore = strings.ToLower(sudoPwStoreStr) + err = sstore.UpdateClientFeOpts(ctx, feOpts) + if err != nil { + return nil, fmt.Errorf("error updating client feopts: %v", err) + } + // clear all sudo pw if turning off + if feOpts.SudoPwStore == "off" { + for _, proc := range remote.GetRemoteMap() { + proc.ClearCachedSudoPw() + } + } + varsUpdated = append(varsUpdated, "sudopwstore") + } + if sudoPwTimeoutStr, found := pk.Kwargs["sudopwtimeout"]; found { + oldPwTimeout := clientData.FeOpts.SudoPwTimeoutMs / 1000 / 60 // ms to minutes + if oldPwTimeout == 0 { + oldPwTimeout = sstore.DefaultSudoTimeout + } + newSudoPwTimeout, err := resolveNonNegInt(sudoPwTimeoutStr, sstore.DefaultSudoTimeout) + if err != nil { + return nil, fmt.Errorf("invalid sudo pw timeout, must be a number greater than 0: %v", err) + } + if newSudoPwTimeout == 0 { + return nil, fmt.Errorf("invalid sudo pw timeout, must be a number greater than 0") + } + feOpts := clientData.FeOpts + feOpts.SudoPwTimeoutMs = newSudoPwTimeout * 60 * 1000 // minutes to ms + err = sstore.UpdateClientFeOpts(ctx, feOpts) + if err != nil { + return nil, fmt.Errorf("error updating client feopts: %v", err) + } + for _, proc := range remote.GetRemoteMap() { + proc.ChangeSudoTimeout(int64(newSudoPwTimeout - oldPwTimeout)) + } + varsUpdated = append(varsUpdated, "sudopwtimeout") + } + if sudoPwClearOnSleepStr, found := pk.Kwargs["sudopwclearonsleep"]; found { + newSudoPwClearOnSleep := resolveBool(sudoPwClearOnSleepStr, true) + feOpts := clientData.FeOpts + feOpts.NoSudoPwClearOnSleep = !newSudoPwClearOnSleep + err = sstore.UpdateClientFeOpts(ctx, feOpts) + if err != nil { + return nil, fmt.Errorf("error updating client feopts: %v", err) + } + varsUpdated = append(varsUpdated, "sudopwclearonsleep") } if aiTimeoutStr, found := CheckOptionAlias(pk.Kwargs, "openaitimeout", "aitimeout"); found { aiTimeout, err := strconv.ParseFloat(aiTimeoutStr, 64) diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go index f738723af..560ce7d50 100644 --- a/wavesrv/pkg/remote/remote.go +++ b/wavesrv/pkg/remote/remote.go @@ -1522,6 +1522,18 @@ func (wsh *WaveshellProc) StreamFile(ctx context.Context, streamPk *packet.Strea return wsh.PacketRpcIter(ctx, streamPk) } +func (msh *WaveshellProc) ListDir(ctx context.Context, listDirPk *packet.ListDirPacketType) (*packet.RpcResponseIter, error) { + return msh.PacketRpcIter(ctx, listDirPk) +} + +func (msh *WaveshellProc) SearchDir(ctx context.Context, searchDirPk *packet.SearchDirPacketType) (*packet.RpcResponseIter, error) { + return msh.PacketRpcIter(ctx, searchDirPk) +} + +func (wsh *WaveshellProc) StreamFile(ctx context.Context, streamPk *packet.StreamFilePacketType) (*packet.RpcResponseIter, error) { + return wsh.PacketRpcIter(ctx, streamPk) +} + func addScVarsToState(state *packet.ShellState) *packet.ShellState { if state == nil { return nil @@ -2210,6 +2222,7 @@ func (wsh *WaveshellProc) PacketRpcIter(ctx context.Context, pk packet.RpcPacket if pk == nil { return nil, fmt.Errorf("PacketRpc passed nil packet") } + log.Printf("sending packet: %v", pk) reqId := pk.GetReqId() wsh.ServerProc.Output.RegisterRpcSz(reqId, RpcIterChannelSize) err := wsh.ServerProc.Input.SendPacketCtx(ctx, pk)