wsh getva, setvar, and file commands (#1317)

This commit is contained in:
Mike Sawka 2024-11-19 17:20:47 -08:00 committed by GitHub
parent 0acad2fbe2
commit 271d8e2e9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1761 additions and 111 deletions

View File

@ -79,7 +79,7 @@ func aiRun(cmd *cobra.Command, args []string) (rtnErr error) {
}
// Default to "waveai" block
isDefaultBlock := blockArg == "" || blockArg == "this"
isDefaultBlock := blockArg == ""
if isDefaultBlock {
blockArg = "view@waveai"
}

View File

@ -14,18 +14,80 @@ import (
)
var connCmd = &cobra.Command{
Use: "conn [status|reinstall|disconnect|connect|ensure] [connection-name]",
Short: "implements connection commands",
Args: cobra.RangeArgs(1, 2),
RunE: connRun,
Use: "conn",
Short: "manage Wave Terminal connections",
Long: "Commands to manage Wave Terminal SSH and WSL connections",
}
var connStatusCmd = &cobra.Command{
Use: "status",
Short: "show status of all connections",
Args: cobra.NoArgs,
RunE: connStatusRun,
PreRunE: preRunSetupRpcClient,
}
var connReinstallCmd = &cobra.Command{
Use: "reinstall CONNECTION",
Short: "reinstall wsh on a connection",
Args: cobra.ExactArgs(1),
RunE: connReinstallRun,
PreRunE: preRunSetupRpcClient,
}
var connDisconnectCmd = &cobra.Command{
Use: "disconnect CONNECTION",
Short: "disconnect a connection",
Args: cobra.ExactArgs(1),
RunE: connDisconnectRun,
PreRunE: preRunSetupRpcClient,
}
var connDisconnectAllCmd = &cobra.Command{
Use: "disconnectall",
Short: "disconnect all connections",
Args: cobra.NoArgs,
RunE: connDisconnectAllRun,
PreRunE: preRunSetupRpcClient,
}
var connConnectCmd = &cobra.Command{
Use: "connect CONNECTION",
Short: "connect to a connection",
Args: cobra.ExactArgs(1),
RunE: connConnectRun,
PreRunE: preRunSetupRpcClient,
}
var connEnsureCmd = &cobra.Command{
Use: "ensure CONNECTION",
Short: "ensure wsh is installed on a connection",
Args: cobra.ExactArgs(1),
RunE: connEnsureRun,
PreRunE: preRunSetupRpcClient,
}
func init() {
rootCmd.AddCommand(connCmd)
connCmd.AddCommand(connStatusCmd)
connCmd.AddCommand(connReinstallCmd)
connCmd.AddCommand(connDisconnectCmd)
connCmd.AddCommand(connDisconnectAllCmd)
connCmd.AddCommand(connConnectCmd)
connCmd.AddCommand(connEnsureCmd)
}
func connStatus() error {
func validateConnectionName(name string) error {
if !strings.HasPrefix(name, "wsl://") {
_, err := remote.ParseOpts(name)
if err != nil {
return fmt.Errorf("cannot parse connection name: %w", err)
}
}
return nil
}
func connStatusRun(cmd *cobra.Command, args []string) error {
var allResp []wshrpc.ConnStatus
sshResp, err := wshclient.ConnStatusCommand(RpcClient, nil)
if err != nil {
@ -48,13 +110,38 @@ func connStatus() error {
if conn.Error != "" {
str += fmt.Sprintf(" (%s)", conn.Error)
}
str += "\n"
WriteStdout("%s\n", str)
}
return nil
}
func connDisconnectAll() error {
func connReinstallRun(cmd *cobra.Command, args []string) error {
connName := args[0]
if err := validateConnectionName(connName); err != nil {
return err
}
err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("reinstalling connection: %w", err)
}
WriteStdout("wsh reinstalled on connection %q\n", connName)
return nil
}
func connDisconnectRun(cmd *cobra.Command, args []string) error {
connName := args[0]
if err := validateConnectionName(connName); err != nil {
return err
}
err := wshclient.ConnDisconnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("disconnecting %q error: %w", connName, err)
}
WriteStdout("disconnected %q\n", connName)
return nil
}
func connDisconnectAllRun(cmd *cobra.Command, args []string) error {
resp, err := wshclient.ConnStatusCommand(RpcClient, nil)
if err != nil {
return fmt.Errorf("getting connection status: %w", err)
@ -64,43 +151,22 @@ func connDisconnectAll() error {
}
for _, conn := range resp {
if conn.Status == "connected" {
err := connDisconnect(conn.Connection)
err := wshclient.ConnDisconnectCommand(RpcClient, conn.Connection, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
WriteStdout("error disconnecting %q: %v\n", conn.Connection, err)
} else {
WriteStdout("disconnected %q\n", conn.Connection)
}
}
}
return nil
}
func connEnsure(connName string) error {
err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("ensuring connection: %w", err)
func connConnectRun(cmd *cobra.Command, args []string) error {
connName := args[0]
if err := validateConnectionName(connName); err != nil {
return err
}
WriteStdout("wsh ensured on connection %q\n", connName)
return nil
}
func connReinstall(connName string) error {
err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("reinstalling connection: %w", err)
}
WriteStdout("wsh reinstalled on connection %q\n", connName)
return nil
}
func connDisconnect(connName string) error {
err := wshclient.ConnDisconnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("disconnecting %q error: %w", connName, err)
}
WriteStdout("disconnected %q\n", connName)
return nil
}
func connConnect(connName string) error {
err := wshclient.ConnConnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("connecting connection: %w", err)
@ -109,32 +175,15 @@ func connConnect(connName string) error {
return nil
}
func connRun(cmd *cobra.Command, args []string) error {
connCmd := args[0]
var connName string
if connCmd != "status" && connCmd != "disconnectall" {
if len(args) < 2 {
return fmt.Errorf("connection name is required %q", connCmd)
}
connName = args[1]
_, err := remote.ParseOpts(connName)
if err != nil && !strings.HasPrefix(connName, "wsl://") {
return fmt.Errorf("cannot parse connection name: %w", err)
}
func connEnsureRun(cmd *cobra.Command, args []string) error {
connName := args[0]
if err := validateConnectionName(connName); err != nil {
return err
}
if connCmd == "status" {
return connStatus()
} else if connCmd == "ensure" {
return connEnsure(connName)
} else if connCmd == "reinstall" {
return connReinstall(connName)
} else if connCmd == "disconnect" {
return connDisconnect(connName)
} else if connCmd == "disconnectall" {
return connDisconnectAll()
} else if connCmd == "connect" {
return connConnect(connName)
} else {
return fmt.Errorf("unknown command %q", connCmd)
err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("ensuring connection: %w", err)
}
WriteStdout("wsh ensured on connection %q\n", connName)
return nil
}

View File

@ -25,8 +25,7 @@ func deleteBlockRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("deleteblock", rtnErr == nil)
}()
oref := blockArg
fullORef, err := resolveSimpleId(oref)
fullORef, err := resolveBlockArg()
if err != nil {
return err
}

View File

@ -0,0 +1,212 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"encoding/base64"
"fmt"
"io"
"io/fs"
"strings"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
func convertNotFoundErr(err error) error {
if err == nil {
return nil
}
if strings.HasPrefix(err.Error(), "NOTFOUND:") {
return fs.ErrNotExist
}
return err
}
func ensureWaveFile(origName string, fileData wshrpc.CommandFileData) (*wshrpc.WaveFileInfo, error) {
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
createData := wshrpc.CommandFileCreateData{
ZoneId: fileData.ZoneId,
FileName: fileData.FileName,
}
err = wshclient.FileCreateCommand(RpcClient, createData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
if err != nil {
return nil, fmt.Errorf("creating file: %w", err)
}
info, err = wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
if err != nil {
return nil, fmt.Errorf("getting file info: %w", err)
}
return info, err
}
if err != nil {
return nil, fmt.Errorf("getting file info: %w", err)
}
return info, nil
}
func streamWriteToWaveFile(fileData wshrpc.CommandFileData, reader io.Reader) error {
// First truncate the file with an empty write
emptyWrite := fileData
emptyWrite.Data64 = ""
err := wshclient.FileWriteCommand(RpcClient, emptyWrite, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
if err != nil {
return fmt.Errorf("initializing file with empty write: %w", err)
}
const chunkSize = 32 * 1024 // 32KB chunks
buf := make([]byte, chunkSize)
totalWritten := int64(0)
for {
n, err := reader.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading input: %w", err)
}
// Check total size
totalWritten += int64(n)
if totalWritten > MaxFileSize {
return fmt.Errorf("input exceeds maximum file size of %d bytes", MaxFileSize)
}
// Prepare and send chunk
chunk := buf[:n]
appendData := fileData
appendData.Data64 = base64.StdEncoding.EncodeToString(chunk)
err = wshclient.FileAppendCommand(RpcClient, appendData, &wshrpc.RpcOpts{Timeout: fileTimeout})
if err != nil {
return fmt.Errorf("appending chunk to file: %w", err)
}
}
return nil
}
func streamReadFromWaveFile(fileData wshrpc.CommandFileData, size int64, writer io.Writer) error {
const chunkSize = 32 * 1024 // 32KB chunks
for offset := int64(0); offset < size; offset += chunkSize {
// Calculate the length of this chunk
length := chunkSize
if offset+int64(length) > size {
length = int(size - offset)
}
// Set up the ReadAt request
fileData.At = &wshrpc.CommandFileDataAt{
Offset: offset,
Size: int64(length),
}
// Read the chunk
content64, err := wshclient.FileReadCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
if err != nil {
return fmt.Errorf("reading chunk at offset %d: %w", offset, err)
}
// Decode and write the chunk
chunk, err := base64.StdEncoding.DecodeString(content64)
if err != nil {
return fmt.Errorf("decoding chunk at offset %d: %w", offset, err)
}
_, err = writer.Write(chunk)
if err != nil {
return fmt.Errorf("writing chunk at offset %d: %w", offset, err)
}
}
return nil
}
type fileListResult struct {
info *wshrpc.WaveFileInfo
err error
}
func streamFileList(zoneId string, path string, recursive bool, filesOnly bool) (<-chan fileListResult, error) {
resultChan := make(chan fileListResult)
// If path doesn't end in /, do a single file lookup
if path != "" && !strings.HasSuffix(path, "/") {
go func() {
defer close(resultChan)
fileData := wshrpc.CommandFileData{
ZoneId: zoneId,
FileName: path,
}
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
resultChan <- fileListResult{err: fmt.Errorf("%s: No such file or directory", path)}
return
}
if err != nil {
resultChan <- fileListResult{err: err}
return
}
resultChan <- fileListResult{info: info}
}()
return resultChan, nil
}
// Directory listing case
go func() {
defer close(resultChan)
prefix := path
prefixLen := len(prefix)
offset := 0
foundAny := false
for {
listData := wshrpc.CommandFileListData{
ZoneId: zoneId,
Prefix: prefix,
All: recursive,
Offset: offset,
Limit: 100,
}
files, err := wshclient.FileListCommand(RpcClient, listData, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
resultChan <- fileListResult{err: err}
return
}
if len(files) == 0 {
if !foundAny {
resultChan <- fileListResult{err: fmt.Errorf("%s: No such file or directory", path)}
}
return
}
for _, f := range files {
if filesOnly && f.IsDir {
continue
}
foundAny = true
if prefixLen > 0 {
f.Name = f.Name[prefixLen:]
}
resultChan <- fileListResult{info: f}
}
if len(files) < 100 {
return
}
offset += len(files)
}
}()
return resultChan, nil
}

632
cmd/wsh/cmd/wshcmd-file.go Normal file
View File

@ -0,0 +1,632 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/util/colprint"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
"golang.org/x/term"
)
const (
MaxFileSize = 10 * 1024 * 1024 // 10MB
WaveFileScheme = "wavefile"
WaveFilePrefix = "wavefile://"
DefaultFileTimeout = 5000
)
var fileCmd = &cobra.Command{
Use: "file",
Short: "manage Wave Terminal files",
Long: "Commands to manage Wave Terminal files stored in blocks",
}
var fileTimeout int
func init() {
rootCmd.AddCommand(fileCmd)
fileCmd.PersistentFlags().IntVarP(&fileTimeout, "timeout", "t", 15000, "timeout in milliseconds for long operations")
fileListCmd.Flags().BoolP("recursive", "r", false, "list subdirectories recursively")
fileListCmd.Flags().BoolP("long", "l", false, "use long listing format")
fileListCmd.Flags().BoolP("one", "1", false, "list one file per line")
fileListCmd.Flags().BoolP("files", "f", false, "list files only")
fileCmd.AddCommand(fileListCmd)
fileCmd.AddCommand(fileCatCmd)
fileCmd.AddCommand(fileWriteCmd)
fileCmd.AddCommand(fileRmCmd)
fileCmd.AddCommand(fileInfoCmd)
fileCmd.AddCommand(fileAppendCmd)
fileCmd.AddCommand(fileCpCmd)
}
type waveFileRef struct {
zoneId string
fileName string
}
func parseWaveFileURL(fileURL string) (*waveFileRef, error) {
if !strings.HasPrefix(fileURL, WaveFilePrefix) {
return nil, fmt.Errorf("invalid file reference %q: must use wavefile:// URL format", fileURL)
}
u, err := url.Parse(fileURL)
if err != nil {
return nil, fmt.Errorf("invalid wavefile URL: %w", err)
}
if u.Scheme != WaveFileScheme {
return nil, fmt.Errorf("invalid URL scheme %q: must be wavefile://", u.Scheme)
}
// Path must start with /
if !strings.HasPrefix(u.Path, "/") {
return nil, fmt.Errorf("invalid wavefile URL: path must start with /")
}
// Must have a host (zone)
if u.Host == "" {
return nil, fmt.Errorf("invalid wavefile URL: must specify zone (e.g., wavefile://block/file.txt)")
}
return &waveFileRef{
zoneId: u.Host,
fileName: strings.TrimPrefix(u.Path, "/"),
}, nil
}
func resolveWaveFile(ref *waveFileRef) (*waveobj.ORef, error) {
return resolveSimpleId(ref.zoneId)
}
var fileListCmd = &cobra.Command{
Use: "ls [wavefile://zone[/path]]",
Short: "list wave files",
Example: " wsh file ls wavefile://block/\n wsh file ls wavefile://client/configs/",
RunE: fileListRun,
PreRunE: preRunSetupRpcClient,
}
var fileCatCmd = &cobra.Command{
Use: "cat wavefile://zone/file",
Short: "display contents of a wave file",
Example: " wsh file cat wavefile://block/config.txt\n wsh file cat wavefile://client/settings.json",
Args: cobra.ExactArgs(1),
RunE: fileCatRun,
PreRunE: preRunSetupRpcClient,
}
var fileInfoCmd = &cobra.Command{
Use: "info wavefile://zone/file",
Short: "show wave file information",
Example: " wsh file info wavefile://block/config.txt",
Args: cobra.ExactArgs(1),
RunE: fileInfoRun,
PreRunE: preRunSetupRpcClient,
}
var fileRmCmd = &cobra.Command{
Use: "rm wavefile://zone/file",
Short: "remove a wave file",
Example: " wsh file rm wavefile://block/config.txt",
Args: cobra.ExactArgs(1),
RunE: fileRmRun,
PreRunE: preRunSetupRpcClient,
}
var fileWriteCmd = &cobra.Command{
Use: "write wavefile://zone/file",
Short: "write stdin into a wave file (up to 10MB)",
Example: " echo 'hello' | wsh file write wavefile://block/greeting.txt",
Args: cobra.ExactArgs(1),
RunE: fileWriteRun,
PreRunE: preRunSetupRpcClient,
}
var fileAppendCmd = &cobra.Command{
Use: "append wavefile://zone/file",
Short: "append stdin to a wave file",
Long: "append stdin to a wave file, buffering input and respecting 10MB total file size limit",
Example: " tail -f log.txt | wsh file append wavefile://block/app.log",
Args: cobra.ExactArgs(1),
RunE: fileAppendRun,
PreRunE: preRunSetupRpcClient,
}
var fileCpCmd = &cobra.Command{
Use: "cp source destination",
Short: "copy between wave files and local files",
Long: `Copy files between wave storage and local filesystem.
Exactly one of source or destination must be a wavefile:// URL.`,
Example: " wsh file cp wavefile://block/config.txt ./local-config.txt\n wsh file cp ./local-config.txt wavefile://block/config.txt",
Args: cobra.ExactArgs(2),
RunE: fileCpRun,
PreRunE: preRunSetupRpcClient,
}
func fileCatRun(cmd *cobra.Command, args []string) error {
ref, err := parseWaveFileURL(args[0])
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
// Get file info first to check existence and get size
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
return fmt.Errorf("%s: no such file", args[0])
}
if err != nil {
return fmt.Errorf("getting file info: %w", err)
}
err = streamReadFromWaveFile(fileData, info.Size, os.Stdout)
if err != nil {
return fmt.Errorf("reading file: %w", err)
}
return nil
}
func fileInfoRun(cmd *cobra.Command, args []string) error {
ref, err := parseWaveFileURL(args[0])
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
return fmt.Errorf("%s: no such file", args[0])
}
if err != nil {
return fmt.Errorf("getting file info: %w", err)
}
WriteStdout("filename: %s\n", info.Name)
WriteStdout("size: %d\n", info.Size)
WriteStdout("ctime: %s\n", time.Unix(info.CreatedTs/1000, 0).Format(time.DateTime))
WriteStdout("mtime: %s\n", time.Unix(info.ModTs/1000, 0).Format(time.DateTime))
if len(info.Meta) > 0 {
WriteStdout("metadata:\n")
for k, v := range info.Meta {
WriteStdout(" %s: %v\n", k, v)
}
}
return nil
}
func fileRmRun(cmd *cobra.Command, args []string) error {
ref, err := parseWaveFileURL(args[0])
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
_, err = wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
return fmt.Errorf("%s: no such file", args[0])
}
if err != nil {
return fmt.Errorf("getting file info: %w", err)
}
err = wshclient.FileDeleteCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
if err != nil {
return fmt.Errorf("removing file: %w", err)
}
return nil
}
func fileWriteRun(cmd *cobra.Command, args []string) error {
ref, err := parseWaveFileURL(args[0])
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
_, err = ensureWaveFile(args[0], fileData)
if err != nil {
return err
}
err = streamWriteToWaveFile(fileData, WrappedStdin)
if err != nil {
return fmt.Errorf("writing file: %w", err)
}
return nil
}
func fileAppendRun(cmd *cobra.Command, args []string) error {
ref, err := parseWaveFileURL(args[0])
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
info, err := ensureWaveFile(args[0], fileData)
if err != nil {
return err
}
if info.Size >= MaxFileSize {
return fmt.Errorf("file already at maximum size (%d bytes)", MaxFileSize)
}
reader := bufio.NewReader(WrappedStdin)
var buf bytes.Buffer
remainingSpace := MaxFileSize - info.Size
for {
chunk := make([]byte, 8192)
n, err := reader.Read(chunk)
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading input: %w", err)
}
if int64(buf.Len()+n) > remainingSpace {
return fmt.Errorf("append would exceed maximum file size of %d bytes", MaxFileSize)
}
buf.Write(chunk[:n])
if buf.Len() >= 8192 { // 8KB batch size
fileData.Data64 = base64.StdEncoding.EncodeToString(buf.Bytes())
err = wshclient.FileAppendCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
if err != nil {
return fmt.Errorf("appending to file: %w", err)
}
remainingSpace -= int64(buf.Len())
buf.Reset()
}
}
if buf.Len() > 0 {
fileData.Data64 = base64.StdEncoding.EncodeToString(buf.Bytes())
err = wshclient.FileAppendCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
if err != nil {
return fmt.Errorf("appending to file: %w", err)
}
}
return nil
}
func getTargetPath(src, dst string) (string, error) {
var srcBase string
if strings.HasPrefix(src, WaveFilePrefix) {
srcBase = path.Base(src)
} else {
srcBase = filepath.Base(src)
}
if strings.HasPrefix(dst, WaveFilePrefix) {
// For wavefile URLs
if strings.HasSuffix(dst, "/") {
return dst + srcBase, nil
}
return dst, nil
}
// For local paths
dstInfo, err := os.Stat(dst)
if err == nil && dstInfo.IsDir() {
// If it's an existing directory, use the source filename
return filepath.Join(dst, srcBase), nil
}
if err != nil && !os.IsNotExist(err) {
// Return error if it's something other than not exists
return "", fmt.Errorf("checking destination path: %w", err)
}
return dst, nil
}
func fileCpRun(cmd *cobra.Command, args []string) error {
src, origDst := args[0], args[1]
dst, err := getTargetPath(src, origDst)
if err != nil {
return err
}
srcIsWave := strings.HasPrefix(src, WaveFilePrefix)
dstIsWave := strings.HasPrefix(dst, WaveFilePrefix)
if srcIsWave == dstIsWave {
return fmt.Errorf("exactly one file must be a wavefile:// URL")
}
if srcIsWave {
return copyFromWaveToLocal(src, dst)
} else {
return copyFromLocalToWave(src, dst)
}
}
func copyFromWaveToLocal(src, dst string) error {
ref, err := parseWaveFileURL(src)
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
// Get file info first to check existence and get size
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
return fmt.Errorf("%s: no such file", src)
}
if err != nil {
return fmt.Errorf("getting file info: %w", err)
}
// Create the destination file
f, err := os.Create(dst)
if err != nil {
return fmt.Errorf("creating local file: %w", err)
}
defer f.Close()
err = streamReadFromWaveFile(fileData, info.Size, f)
if err != nil {
return fmt.Errorf("reading wave file: %w", err)
}
return nil
}
func copyFromLocalToWave(src, dst string) error {
ref, err := parseWaveFileURL(dst)
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
// stat local file
stat, err := os.Stat(src)
if err == fs.ErrNotExist {
return fmt.Errorf("%s: no such file", src)
}
if err != nil {
return fmt.Errorf("stat local file: %w", err)
}
if stat.IsDir() {
return fmt.Errorf("%s: is a directory", src)
}
fileData := wshrpc.CommandFileData{
ZoneId: fullORef.OID,
FileName: ref.fileName,
}
_, err = ensureWaveFile(dst, fileData)
if err != nil {
return err
}
file, err := os.Open(src)
if err != nil {
return fmt.Errorf("opening local file: %w", err)
}
defer file.Close()
err = streamWriteToWaveFile(fileData, file)
if err != nil {
return fmt.Errorf("writing wave file: %w", err)
}
return nil
}
func filePrintColumns(filesChan <-chan fileListResult) error {
width := 80 // default if we can't get terminal
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {
width = w
}
numCols := width / 10
if numCols < 1 {
numCols = 1
}
return colprint.PrintColumns(
filesChan,
numCols,
100, // sample size
func(f fileListResult) (string, error) {
if f.err != nil {
return "", f.err
}
return f.info.Name, nil
},
os.Stdout,
)
}
func filePrintLong(filesChan <-chan fileListResult) error {
// Sample first 100 files to determine name width
maxNameLen := 0
var samples []*wshrpc.WaveFileInfo
for f := range filesChan {
if f.err != nil {
return f.err
}
samples = append(samples, f.info)
if len(f.info.Name) > maxNameLen {
maxNameLen = len(f.info.Name)
}
if len(samples) >= 100 {
break
}
}
// Use sampled width, but cap it at 60 chars to prevent excessive width
nameWidth := maxNameLen + 2
if nameWidth > 60 {
nameWidth = 60
}
// Print samples
for _, f := range samples {
name := f.Name
t := time.Unix(f.ModTs/1000, 0)
timestamp := utilfn.FormatLsTime(t)
if f.Size == 0 && strings.HasSuffix(name, "/") {
fmt.Fprintf(os.Stdout, "%-*s %8s %s\n", nameWidth, name, "-", timestamp)
} else {
fmt.Fprintf(os.Stdout, "%-*s %8d %s\n", nameWidth, name, f.Size, timestamp)
}
}
// Continue with remaining files
for f := range filesChan {
if f.err != nil {
return f.err
}
name := f.info.Name
timestamp := time.Unix(f.info.ModTs/1000, 0).Format("Jan 02 15:04")
if f.info.Size == 0 && strings.HasSuffix(name, "/") {
fmt.Fprintf(os.Stdout, "%-*s %8s %s\n", nameWidth, name, "-", timestamp)
} else {
fmt.Fprintf(os.Stdout, "%-*s %8d %s\n", nameWidth, name, f.info.Size, timestamp)
}
}
return nil
}
func fileListRun(cmd *cobra.Command, args []string) error {
recursive, _ := cmd.Flags().GetBool("recursive")
longForm, _ := cmd.Flags().GetBool("long")
onePerLine, _ := cmd.Flags().GetBool("one")
filesOnly, _ := cmd.Flags().GetBool("files")
// Check if we're in a pipe
stat, _ := os.Stdout.Stat()
isPipe := (stat.Mode() & os.ModeCharDevice) == 0
if isPipe {
onePerLine = true
}
// Default to listing everything if no path specified
if len(args) == 0 {
args = append(args, "wavefile://client/")
}
ref, err := parseWaveFileURL(args[0])
if err != nil {
return err
}
fullORef, err := resolveWaveFile(ref)
if err != nil {
return err
}
filesChan, err := streamFileList(fullORef.OID, ref.fileName, recursive, filesOnly)
if err != nil {
return err
}
if longForm {
return filePrintLong(filesChan)
}
if onePerLine {
for f := range filesChan {
if f.err != nil {
return f.err
}
fmt.Fprintln(os.Stdout, f.info.Name)
}
return nil
}
return filePrintColumns(filesChan)
}

View File

@ -76,12 +76,7 @@ func getMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("getmeta", rtnErr == nil)
}()
oref := blockArg
if oref == "" {
return fmt.Errorf("blockid is required")
}
fullORef, err := resolveSimpleId(oref)
fullORef, err := resolveBlockArg()
if err != nil {
return err
}

View File

@ -0,0 +1,132 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"encoding/base64"
"fmt"
"io/fs"
"sort"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/util/envutil"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
var getVarCmd = &cobra.Command{
Use: "getvar [flags] [key]",
Short: "get variable(s) from a block",
Long: `Get variable(s) from a block. Without --all, requires a key argument.
With --all, prints all variables. Use -0 for null-terminated output.`,
Example: " wsh getvar FOO\n wsh getvar --all\n wsh getvar --all -0",
RunE: getVarRun,
PreRunE: preRunSetupRpcClient,
}
var (
getVarFileName string
getVarAllVars bool
getVarNullTerminate bool
getVarLocal bool
)
func init() {
rootCmd.AddCommand(getVarCmd)
getVarCmd.Flags().StringVar(&getVarFileName, "varfile", DefaultVarFileName, "var file name")
getVarCmd.Flags().BoolVar(&getVarAllVars, "all", false, "get all variables")
getVarCmd.Flags().BoolVarP(&getVarNullTerminate, "null", "0", false, "use null terminators in output")
getVarCmd.Flags().BoolVarP(&getVarLocal, "local", "l", false, "get variables local to block")
}
func getVarRun(cmd *cobra.Command, args []string) error {
defer func() {
sendActivity("getvar", WshExitCode == 0)
}()
// Resolve block to get zoneId
if blockArg == "" {
if getVarLocal {
blockArg = "this"
} else {
blockArg = "client"
}
}
fullORef, err := resolveBlockArg()
if err != nil {
return err
}
if getVarAllVars {
if len(args) > 0 {
return fmt.Errorf("cannot specify key with --all")
}
return getAllVariables(fullORef.OID)
}
// Single variable case - existing logic
if len(args) != 1 {
return fmt.Errorf("requires a key argument")
}
key := args[0]
commandData := wshrpc.CommandVarData{
Key: key,
ZoneId: fullORef.OID,
FileName: getVarFileName,
}
resp, err := wshclient.GetVarCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("getting variable: %w", err)
}
if !resp.Exists {
WshExitCode = 1
return nil
}
WriteStdout("%s\n", resp.Val)
return nil
}
func getAllVariables(zoneId string) error {
fileData := wshrpc.CommandFileData{
ZoneId: zoneId,
FileName: getVarFileName,
}
envStr64, err := wshclient.FileReadCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
err = convertNotFoundErr(err)
if err == fs.ErrNotExist {
return nil
}
if err != nil {
return fmt.Errorf("reading variables: %w", err)
}
envBytes, err := base64.StdEncoding.DecodeString(envStr64)
if err != nil {
return fmt.Errorf("decoding variables: %w", err)
}
envMap := envutil.EnvToMap(string(envBytes))
terminator := "\n"
if getVarNullTerminate {
terminator = "\x00"
}
// Sort keys for consistent output
keys := make([]string, 0, len(envMap))
for k := range envMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
WriteStdout("%s=%s%s", k, envMap[k], terminator)
}
return nil
}

View File

@ -12,7 +12,7 @@ import (
)
var readFileCmd = &cobra.Command{
Use: "readfile",
Use: "readfile [filename]",
Short: "read a blockfile",
Args: cobra.ExactArgs(1),
Run: runReadFile,
@ -24,17 +24,12 @@ func init() {
}
func runReadFile(cmd *cobra.Command, args []string) {
oref := args[0]
if oref == "" {
WriteStderr("[error] oref is required\n")
return
}
fullORef, err := resolveSimpleId(oref)
fullORef, err := resolveBlockArg()
if err != nil {
WriteStderr("[error] %v\n", err)
return
}
resp64, err := wshclient.FileReadCommand(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[1]}, &wshrpc.RpcOpts{Timeout: 5000})
resp64, err := wshclient.FileReadCommand(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[0]}, &wshrpc.RpcOpts{Timeout: 5000})
if err != nil {
WriteStderr("[error] reading file: %v\n", err)
return

View File

@ -31,6 +31,7 @@ var RpcClient *wshutil.WshRpc
var RpcContext wshrpc.RpcContext
var UsingTermWshMode bool
var blockArg string
var WshExitCode int
func WriteStderr(fmtStr string, args ...interface{}) {
output := fmt.Sprintf(fmtStr, args...)
@ -59,7 +60,7 @@ func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
func resolveBlockArg() (*waveobj.ORef, error) {
oref := blockArg
if oref == "" {
return nil, fmt.Errorf("blockid is required")
oref = "this"
}
fullORef, err := resolveSimpleId(oref)
if err != nil {
@ -145,10 +146,10 @@ func Execute() {
debug.PrintStack()
wshutil.DoShutdown("", 1, true)
} else {
wshutil.DoShutdown("", 0, false)
wshutil.DoShutdown("", WshExitCode, false)
}
}()
rootCmd.PersistentFlags().StringVarP(&blockArg, "block", "b", "this", "for commands which require a block id")
rootCmd.PersistentFlags().StringVarP(&blockArg, "block", "b", "", "for commands which require a block id")
err := rootCmd.Execute()
if err != nil {
wshutil.DoShutdown("", 1, true)

View File

@ -111,10 +111,6 @@ func setMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("setmeta", rtnErr == nil)
}()
if blockArg == "" {
return fmt.Errorf("block (oref) is required")
}
var jsonMeta map[string]interface{}
if setMetaJsonFilePath != "" {
var err error
@ -139,7 +135,7 @@ func setMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
if len(fullMeta) == 0 {
return fmt.Errorf("no metadata keys specified")
}
fullORef, err := resolveSimpleId(blockArg)
fullORef, err := resolveBlockArg()
if err != nil {
return err
}

View File

@ -0,0 +1,101 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
const DefaultVarFileName = "var"
var setVarCmd = &cobra.Command{
Use: "setvar [flags] KEY=VALUE...",
Short: "set variable(s) for a block",
Long: `Set one or more variables for a block.
Use --remove/-r to remove variables instead of setting them.
When setting, each argument must be in KEY=VALUE format.
When removing, each argument is treated as a key to remove.`,
Example: " wsh setvar FOO=bar BAZ=123\n wsh setvar -r FOO BAZ",
Args: cobra.MinimumNArgs(1),
RunE: setVarRun,
PreRunE: preRunSetupRpcClient,
}
var (
setVarFileName string
setVarRemoveVar bool
setVarLocal bool
)
func init() {
rootCmd.AddCommand(setVarCmd)
setVarCmd.Flags().StringVar(&setVarFileName, "varfile", DefaultVarFileName, "var file name")
setVarCmd.Flags().BoolVarP(&setVarLocal, "local", "l", false, "set variables local to block")
setVarCmd.Flags().BoolVarP(&setVarRemoveVar, "remove", "r", false, "remove the variable(s) instead of setting")
}
func parseKeyValue(arg string) (key, value string, err error) {
if setVarRemoveVar {
return arg, "", nil
}
parts := strings.SplitN(arg, "=", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid KEY=VALUE format %q (= sign required)", arg)
}
key = parts[0]
if key == "" {
return "", "", fmt.Errorf("empty key not allowed")
}
return key, parts[1], nil
}
func setVarRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("setvar", rtnErr == nil)
}()
// Resolve block to get zoneId
if blockArg == "" {
if getVarLocal {
blockArg = "this"
} else {
blockArg = "client"
}
}
fullORef, err := resolveBlockArg()
if err != nil {
return err
}
// Process all variables
for _, arg := range args {
key, value, err := parseKeyValue(arg)
if err != nil {
return err
}
commandData := wshrpc.CommandVarData{
Key: key,
ZoneId: fullORef.OID,
FileName: setVarFileName,
Remove: setVarRemoveVar,
}
if !setVarRemoveVar {
commandData.Val = value
}
err = wshclient.SetVarCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("setting variable %s: %w", key, err)
}
}
return nil
}

View File

@ -51,11 +51,7 @@ func init() {
}
func webGetRun(cmd *cobra.Command, args []string) error {
oref := blockArg
if oref == "" {
return fmt.Errorf("blockid not specified")
}
fullORef, err := resolveSimpleId(oref)
fullORef, err := resolveBlockArg()
if err != nil {
return fmt.Errorf("resolving blockid: %w", err)
}

View File

@ -137,6 +137,26 @@ class RpcApiType {
return client.wshRpcCall("fileappendijson", data, opts);
}
// command "filecreate" [call]
FileCreateCommand(client: WshClient, data: CommandFileCreateData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("filecreate", data, opts);
}
// command "filedelete" [call]
FileDeleteCommand(client: WshClient, data: CommandFileData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("filedelete", data, opts);
}
// command "fileinfo" [call]
FileInfoCommand(client: WshClient, data: CommandFileData, opts?: RpcOpts): Promise<WaveFileInfo> {
return client.wshRpcCall("fileinfo", data, opts);
}
// command "filelist" [call]
FileListCommand(client: WshClient, data: CommandFileListData, opts?: RpcOpts): Promise<WaveFileInfo[]> {
return client.wshRpcCall("filelist", data, opts);
}
// command "fileread" [call]
FileReadCommand(client: WshClient, data: CommandFileData, opts?: RpcOpts): Promise<string> {
return client.wshRpcCall("fileread", data, opts);
@ -157,6 +177,11 @@ class RpcApiType {
return client.wshRpcCall("getupdatechannel", null, opts);
}
// command "getvar" [call]
GetVarCommand(client: WshClient, data: CommandVarData, opts?: RpcOpts): Promise<CommandVarResponseData> {
return client.wshRpcCall("getvar", data, opts);
}
// command "message" [call]
MessageCommand(client: WshClient, data: CommandMessageData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("message", data, opts);
@ -222,6 +247,11 @@ class RpcApiType {
return client.wshRpcCall("setmeta", data, opts);
}
// command "setvar" [call]
SetVarCommand(client: WshClient, data: CommandVarData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("setvar", data, opts);
}
// command "setview" [call]
SetViewCommand(client: WshClient, data: CommandBlockSetViewData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("setview", data, opts);

View File

@ -148,11 +148,35 @@ declare global {
maxitems: number;
};
// wshrpc.CommandFileCreateData
type CommandFileCreateData = {
zoneid: string;
filename: string;
meta?: {[key: string]: any};
opts?: FileOptsType;
};
// wshrpc.CommandFileData
type CommandFileData = {
zoneid: string;
filename: string;
data64?: string;
at?: CommandFileDataAt;
};
// wshrpc.CommandFileDataAt
type CommandFileDataAt = {
offset: number;
size?: number;
};
// wshrpc.CommandFileListData
type CommandFileListData = {
zoneid: string;
prefix?: string;
all?: boolean;
offset?: number;
limit?: number;
};
// wshrpc.CommandGetMetaData
@ -202,6 +226,22 @@ declare global {
meta: MetaType;
};
// wshrpc.CommandVarData
type CommandVarData = {
key: string;
val?: string;
remove?: boolean;
zoneid: string;
filename: string;
};
// wshrpc.CommandVarResponseData
type CommandVarResponseData = {
key: string;
val: string;
exists: boolean;
};
// wshrpc.CommandWaitForRouteData
type CommandWaitForRouteData = {
routeid: string;
@ -904,6 +944,18 @@ declare global {
meta: {[key: string]: any};
};
// wshrpc.WaveFileInfo
type WaveFileInfo = {
zoneid: string;
name: string;
opts?: FileOptsType;
size?: number;
createdts?: number;
modts?: number;
meta?: {[key: string]: any};
isdir?: boolean;
};
// wshrpc.WaveInfoData
type WaveInfoData = {
version: string;

View File

@ -0,0 +1,88 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package colprint
import (
"fmt"
"io"
)
// formatFn is a function that converts a value of type T to its string representation
type formatFn[T any] func(T) (string, error)
// PrintColumns prints values in columns, adapting to long values by letting them span multiple columns
func PrintColumns[T any](values <-chan T, numCols int, sampleSize int, format formatFn[T], w io.Writer) error {
// Get first batch and determine column width
maxLen := 0
var samples []T
for v := range values {
samples = append(samples, v)
str, err := format(v)
if err != nil {
return err
}
if len(str) > maxLen {
maxLen = len(str)
}
if len(samples) >= sampleSize {
break
}
}
colWidth := maxLen + 2 // Add minimum padding
if colWidth < 1 {
colWidth = 1
}
// Print in columns using our determined width
col := 0
for _, v := range samples {
str, err := format(v)
if err != nil {
return err
}
if err := printColHelper(str, colWidth, &col, numCols, w); err != nil {
return err
}
}
// Continue with any remaining values
for v := range values {
str, err := format(v)
if err != nil {
return err
}
if err := printColHelper(str, colWidth, &col, numCols, w); err != nil {
return err
}
}
if col > 0 {
if _, err := fmt.Fprint(w, "\n"); err != nil {
return err
}
}
return nil
}
func printColHelper(str string, colWidth int, col *int, numCols int, w io.Writer) error {
nameColSpan := (len(str) + 1) / colWidth
if (len(str)+1)%colWidth != 0 {
nameColSpan++
}
if *col+nameColSpan > numCols {
if _, err := fmt.Fprint(w, "\n"); err != nil {
return err
}
*col = 0
}
if _, err := fmt.Fprintf(w, "%-*s", nameColSpan*colWidth, str); err != nil {
return err
}
*col += nameColSpan
return nil
}

View File

@ -0,0 +1,69 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package envutil
import (
"fmt"
"strings"
)
const MaxEnvSize = 1024 * 1024
// env format:
// KEY=VALUE\0
// keys cannot have '=' or '\0' in them
// values can have '=' but not '\0'
func EnvToMap(envStr string) map[string]string {
rtn := make(map[string]string)
envLines := strings.Split(envStr, "\x00")
for _, line := range envLines {
if len(line) == 0 {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
rtn[parts[0]] = parts[1]
}
}
return rtn
}
func MapToEnv(envMap map[string]string) string {
var sb strings.Builder
for key, val := range envMap {
sb.WriteString(key)
sb.WriteByte('=')
sb.WriteString(val)
sb.WriteByte('\x00')
}
return sb.String()
}
func GetEnv(envStr string, key string) string {
envMap := EnvToMap(envStr)
return envMap[key]
}
func SetEnv(envStr string, key string, val string) (string, error) {
if strings.ContainsAny(key, "=\x00") {
return "", fmt.Errorf("key cannot contain '=' or '\\x00'")
}
if strings.Contains(val, "\x00") {
return "", fmt.Errorf("value cannot contain '\\x00'")
}
if len(key)+len(val)+2+len(envStr) > MaxEnvSize {
return "", fmt.Errorf("env string too large (max %d bytes)", MaxEnvSize)
}
envMap := EnvToMap(envStr)
envMap[key] = val
rtnStr := MapToEnv(envMap)
return rtnStr, nil
}
func RmEnv(envStr string, key string) string {
envMap := EnvToMap(envStr)
delete(envMap, key)
return MapToEnv(envMap)
}

View File

@ -28,6 +28,7 @@ import (
"strings"
"syscall"
"text/template"
"time"
"unicode/utf8"
)
@ -923,3 +924,16 @@ func GetLineColFromOffset(barr []byte, offset int) (int, int) {
}
return line, col
}
func FormatLsTime(t time.Time) string {
now := time.Now()
sixMonthsAgo := now.AddDate(0, -6, 0)
if t.After(sixMonthsAgo) {
// Recent files: "Nov 18 18:40"
return t.Format("Jan _2 15:04")
} else {
// Older files: "Apr 12 2024"
return t.Format("Jan _2 2006")
}
}

View File

@ -33,6 +33,8 @@ type SubscriptionRequest struct {
}
const (
FileOp_Create = "create"
FileOp_Delete = "delete"
FileOp_Append = "append"
FileOp_Truncate = "truncate"
FileOp_Invalidate = "invalidate"

View File

@ -171,6 +171,30 @@ func FileAppendIJsonCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendIJsonDat
return err
}
// command "filecreate", wshserver.FileCreateCommand
func FileCreateCommand(w *wshutil.WshRpc, data wshrpc.CommandFileCreateData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "filecreate", data, opts)
return err
}
// command "filedelete", wshserver.FileDeleteCommand
func FileDeleteCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "filedelete", data, opts)
return err
}
// command "fileinfo", wshserver.FileInfoCommand
func FileInfoCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.RpcOpts) (*wshrpc.WaveFileInfo, error) {
resp, err := sendRpcRequestCallHelper[*wshrpc.WaveFileInfo](w, "fileinfo", data, opts)
return resp, err
}
// command "filelist", wshserver.FileListCommand
func FileListCommand(w *wshutil.WshRpc, data wshrpc.CommandFileListData, opts *wshrpc.RpcOpts) ([]*wshrpc.WaveFileInfo, error) {
resp, err := sendRpcRequestCallHelper[[]*wshrpc.WaveFileInfo](w, "filelist", data, opts)
return resp, err
}
// command "fileread", wshserver.FileReadCommand
func FileReadCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.RpcOpts) (string, error) {
resp, err := sendRpcRequestCallHelper[string](w, "fileread", data, opts)
@ -195,6 +219,12 @@ func GetUpdateChannelCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (string, e
return resp, err
}
// command "getvar", wshserver.GetVarCommand
func GetVarCommand(w *wshutil.WshRpc, data wshrpc.CommandVarData, opts *wshrpc.RpcOpts) (*wshrpc.CommandVarResponseData, error) {
resp, err := sendRpcRequestCallHelper[*wshrpc.CommandVarResponseData](w, "getvar", data, opts)
return resp, err
}
// command "message", wshserver.MessageCommand
func MessageCommand(w *wshutil.WshRpc, data wshrpc.CommandMessageData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "message", data, opts)
@ -271,6 +301,12 @@ func SetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandSetMetaData, opts *wsh
return err
}
// command "setvar", wshserver.SetVarCommand
func SetVarCommand(w *wshutil.WshRpc, data wshrpc.CommandVarData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "setvar", data, opts)
return err
}
// command "setview", wshserver.SetViewCommand
func SetViewCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockSetViewData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "setview", data, opts)

View File

@ -10,6 +10,7 @@ import (
"os"
"reflect"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/ijson"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/vdom"
@ -66,6 +67,8 @@ const (
Command_WaveInfo = "waveinfo"
Command_WshActivity = "wshactivity"
Command_Activity = "activity"
Command_GetVar = "getvar"
Command_SetVar = "setvar"
Command_ConnStatus = "connstatus"
Command_WslStatus = "wslstatus"
@ -107,16 +110,20 @@ type WshRpcInterface interface {
ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error
ControllerStopCommand(ctx context.Context, blockId string) error
ControllerResyncCommand(ctx context.Context, data CommandControllerResyncData) error
FileAppendCommand(ctx context.Context, data CommandFileData) error
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error)
CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error)
CreateSubBlockCommand(ctx context.Context, data CommandCreateSubBlockData) (waveobj.ORef, error)
DeleteBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
DeleteSubBlockCommand(ctx context.Context, data CommandDeleteBlockData) error
WaitForRouteCommand(ctx context.Context, data CommandWaitForRouteData) (bool, error)
FileCreateCommand(ctx context.Context, data CommandFileCreateData) error
FileDeleteCommand(ctx context.Context, data CommandFileData) error
FileAppendCommand(ctx context.Context, data CommandFileData) error
FileAppendIJsonCommand(ctx context.Context, data CommandAppendIJsonData) error
FileWriteCommand(ctx context.Context, data CommandFileData) error
FileReadCommand(ctx context.Context, data CommandFileData) (string, error)
FileInfoCommand(ctx context.Context, data CommandFileData) (*WaveFileInfo, error)
FileListCommand(ctx context.Context, data CommandFileListData) ([]*WaveFileInfo, error)
EventPublishCommand(ctx context.Context, data wps.WaveEvent) error
EventSubCommand(ctx context.Context, data wps.SubscriptionRequest) error
EventUnsubCommand(ctx context.Context, data string) error
@ -131,6 +138,8 @@ type WshRpcInterface interface {
WaveInfoCommand(ctx context.Context) (*WaveInfoData, error)
WshActivityCommand(ct context.Context, data map[string]int) error
ActivityCommand(ctx context.Context, data telemetry.ActivityUpdate) error
GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error)
SetVarCommand(ctx context.Context, data CommandVarData) error
// connection functions
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)
@ -291,10 +300,42 @@ type CommandBlockInputData struct {
TermSize *waveobj.TermSize `json:"termsize,omitempty"`
}
type CommandFileDataAt struct {
Offset int64 `json:"offset"`
Size int64 `json:"size,omitempty"`
}
type CommandFileData struct {
ZoneId string `json:"zoneid" wshcontext:"BlockId"`
FileName string `json:"filename"`
Data64 string `json:"data64,omitempty"`
ZoneId string `json:"zoneid" wshcontext:"BlockId"`
FileName string `json:"filename"`
Data64 string `json:"data64,omitempty"`
At *CommandFileDataAt `json:"at,omitempty"` // if set, this turns read/write ops to ReadAt/WriteAt ops (len is only used for ReadAt)
}
type WaveFileInfo struct {
ZoneId string `json:"zoneid"`
Name string `json:"name"`
Opts filestore.FileOptsType `json:"opts,omitempty"`
Size int64 `json:"size,omitempty"`
CreatedTs int64 `json:"createdts,omitempty"`
ModTs int64 `json:"modts,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
IsDir bool `json:"isdir,omitempty"`
}
type CommandFileListData struct {
ZoneId string `json:"zoneid"`
Prefix string `json:"prefix,omitempty"`
All bool `json:"all,omitempty"`
Offset int `json:"offset,omitempty"`
Limit int `json:"limit,omitempty"`
}
type CommandFileCreateData struct {
ZoneId string `json:"zoneid"`
FileName string `json:"filename"`
Meta map[string]any `json:"meta,omitempty"`
Opts *filestore.FileOptsType `json:"opts,omitempty"`
}
type CommandAppendIJsonData struct {
@ -467,3 +508,17 @@ type WaveInfoData struct {
type AiMessageData struct {
Message string `json:"message,omitempty"`
}
type CommandVarData struct {
Key string `json:"key"`
Val string `json:"val,omitempty"`
Remove bool `json:"remove,omitempty"`
ZoneId string `json:"zoneid"`
FileName string `json:"filename"`
}
type CommandVarResponseData struct {
Key string `json:"key"`
Val string `json:"val"`
Exists bool `json:"exists"`
}

View File

@ -16,10 +16,15 @@ import (
"github.com/wavetermdev/waveterm/pkg/wstore"
)
const SimpleId_This = "this"
const SimpleId_Tab = "tab"
const SimpleId_Ws = "ws"
const SimpleId_Client = "client"
const (
SimpleId_This = "this"
SimpleId_Block = "block"
SimpleId_Tab = "tab"
SimpleId_Ws = "ws"
SimpleId_Workspace = "workspace"
SimpleId_Client = "client"
SimpleId_Global = "global"
)
var (
simpleTabNumRe = regexp.MustCompile(`^tab:(\d{1,3})$`)
@ -35,7 +40,8 @@ func parseSimpleId(simpleId string) (discriminator string, value string, err err
}
// Handle special keywords
if simpleId == SimpleId_This || simpleId == SimpleId_Tab || simpleId == SimpleId_Ws || simpleId == SimpleId_Client {
if simpleId == SimpleId_This || simpleId == SimpleId_Block || simpleId == SimpleId_Tab ||
simpleId == SimpleId_Ws || simpleId == SimpleId_Workspace || simpleId == SimpleId_Client || simpleId == SimpleId_Global {
return "this", simpleId, nil
}
@ -76,7 +82,7 @@ func resolveThis(ctx context.Context, data wshrpc.CommandResolveIdsData, value s
return nil, fmt.Errorf("no blockid in request")
}
if value == SimpleId_This {
if value == SimpleId_This || value == SimpleId_Block {
return &waveobj.ORef{OType: waveobj.OType_Block, OID: data.BlockId}, nil
}
if value == SimpleId_Tab {
@ -86,7 +92,7 @@ func resolveThis(ctx context.Context, data wshrpc.CommandResolveIdsData, value s
}
return &waveobj.ORef{OType: waveobj.OType_Tab, OID: tabId}, nil
}
if value == SimpleId_Ws {
if value == SimpleId_Ws || value == SimpleId_Workspace {
tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId)
if err != nil {
return nil, fmt.Errorf("error finding tab: %v", err)
@ -97,7 +103,7 @@ func resolveThis(ctx context.Context, data wshrpc.CommandResolveIdsData, value s
}
return &waveobj.ORef{OType: waveobj.OType_Workspace, OID: wsId}, nil
}
if value == SimpleId_Client {
if value == SimpleId_Client || value == SimpleId_Global {
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
return nil, fmt.Errorf("error getting client: %v", err)

View File

@ -21,6 +21,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/remote"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/envutil"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveai"
"github.com/wavetermdev/waveterm/pkg/wavebase"
@ -265,14 +266,152 @@ func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.Com
return bc.SendInput(inputUnion)
}
func (ws *WshServer) FileCreateCommand(ctx context.Context, data wshrpc.CommandFileCreateData) error {
var fileOpts filestore.FileOptsType
if data.Opts != nil {
fileOpts = *data.Opts
}
err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, data.Meta, fileOpts)
if err != nil {
return fmt.Errorf("error creating blockfile: %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Create,
},
})
return nil
}
func (ws *WshServer) FileDeleteCommand(ctx context.Context, data wshrpc.CommandFileData) error {
err := filestore.WFS.DeleteFile(ctx, data.ZoneId, data.FileName)
if err != nil {
return fmt.Errorf("error deleting blockfile: %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Delete,
},
})
return nil
}
func waveFileToWaveFileInfo(wf *filestore.WaveFile) *wshrpc.WaveFileInfo {
return &wshrpc.WaveFileInfo{
ZoneId: wf.ZoneId,
Name: wf.Name,
Opts: wf.Opts,
Size: wf.Size,
CreatedTs: wf.CreatedTs,
ModTs: wf.ModTs,
Meta: wf.Meta,
}
}
func (ws *WshServer) FileInfoCommand(ctx context.Context, data wshrpc.CommandFileData) (*wshrpc.WaveFileInfo, error) {
fileInfo, err := filestore.WFS.Stat(ctx, data.ZoneId, data.FileName)
if err != nil {
if err == fs.ErrNotExist {
return nil, fmt.Errorf("NOTFOUND: %w", err)
}
return nil, fmt.Errorf("error getting file info: %w", err)
}
return waveFileToWaveFileInfo(fileInfo), nil
}
func (ws *WshServer) FileListCommand(ctx context.Context, data wshrpc.CommandFileListData) ([]*wshrpc.WaveFileInfo, error) {
fileListOrig, err := filestore.WFS.ListFiles(ctx, data.ZoneId)
if err != nil {
return nil, fmt.Errorf("error listing blockfiles: %w", err)
}
var fileList []*wshrpc.WaveFileInfo
for _, wf := range fileListOrig {
fileList = append(fileList, waveFileToWaveFileInfo(wf))
}
if data.Prefix != "" {
var filteredList []*wshrpc.WaveFileInfo
for _, file := range fileList {
if strings.HasPrefix(file.Name, data.Prefix) {
filteredList = append(filteredList, file)
}
}
fileList = filteredList
}
if !data.All {
var filteredList []*wshrpc.WaveFileInfo
dirMap := make(map[string]int64) // the value is max modtime
for _, file := range fileList {
// if there is an extra "/" after the prefix, don't include it
// first strip the prefix
relPath := strings.TrimPrefix(file.Name, data.Prefix)
// then check if there is a "/" after the prefix
if strings.Contains(relPath, "/") {
dirPath := strings.Split(relPath, "/")[0]
modTime := dirMap[dirPath]
if file.ModTs > modTime {
dirMap[dirPath] = file.ModTs
}
continue
}
filteredList = append(filteredList, file)
}
for dir := range dirMap {
filteredList = append(filteredList, &wshrpc.WaveFileInfo{
ZoneId: data.ZoneId,
Name: data.Prefix + dir + "/",
Size: 0,
Meta: nil,
ModTs: dirMap[dir],
CreatedTs: dirMap[dir],
IsDir: true,
})
}
fileList = filteredList
}
if data.Offset > 0 {
if data.Offset >= len(fileList) {
fileList = nil
} else {
fileList = fileList[data.Offset:]
}
}
if data.Limit > 0 {
if data.Limit < len(fileList) {
fileList = fileList[:data.Limit]
}
}
return fileList, nil
}
func (ws *WshServer) FileWriteCommand(ctx context.Context, data wshrpc.CommandFileData) error {
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
if err != nil {
return fmt.Errorf("error decoding data64: %w", err)
}
err = filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, dataBuf)
if err != nil {
return fmt.Errorf("error writing to blockfile: %w", err)
if data.At != nil {
err = filestore.WFS.WriteAt(ctx, data.ZoneId, data.FileName, data.At.Offset, dataBuf)
if err == fs.ErrNotExist {
return fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return fmt.Errorf("error writing to blockfile: %w", err)
}
} else {
err = filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, dataBuf)
if err == fs.ErrNotExist {
return fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return fmt.Errorf("error writing to blockfile: %w", err)
}
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
@ -287,11 +426,25 @@ func (ws *WshServer) FileWriteCommand(ctx context.Context, data wshrpc.CommandFi
}
func (ws *WshServer) FileReadCommand(ctx context.Context, data wshrpc.CommandFileData) (string, error) {
_, dataBuf, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err != nil {
return "", fmt.Errorf("error reading blockfile: %w", err)
if data.At != nil {
_, dataBuf, err := filestore.WFS.ReadAt(ctx, data.ZoneId, data.FileName, data.At.Offset, data.At.Size)
if err == fs.ErrNotExist {
return "", fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return "", fmt.Errorf("error reading blockfile: %w", err)
}
return base64.StdEncoding.EncodeToString(dataBuf), nil
} else {
_, dataBuf, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err == fs.ErrNotExist {
return "", fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return "", fmt.Errorf("error reading blockfile: %w", err)
}
return base64.StdEncoding.EncodeToString(dataBuf), nil
}
return base64.StdEncoding.EncodeToString(dataBuf), nil
}
func (ws *WshServer) FileAppendCommand(ctx context.Context, data wshrpc.CommandFileData) error {
@ -300,6 +453,9 @@ func (ws *WshServer) FileAppendCommand(ctx context.Context, data wshrpc.CommandF
return fmt.Errorf("error decoding data64: %w", err)
}
err = filestore.WFS.AppendData(ctx, data.ZoneId, data.FileName, dataBuf)
if err == fs.ErrNotExist {
return fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return fmt.Errorf("error appending to blockfile: %w", err)
}
@ -610,3 +766,37 @@ func (ws *WshServer) ActivityCommand(ctx context.Context, activity telemetry.Act
telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity")
return nil
}
func (ws *WshServer) GetVarCommand(ctx context.Context, data wshrpc.CommandVarData) (*wshrpc.CommandVarResponseData, error) {
_, fileData, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err == fs.ErrNotExist {
return &wshrpc.CommandVarResponseData{Key: data.Key, Exists: false}, nil
}
if err != nil {
return nil, fmt.Errorf("error reading blockfile: %w", err)
}
envMap := envutil.EnvToMap(string(fileData))
value, ok := envMap[data.Key]
return &wshrpc.CommandVarResponseData{Key: data.Key, Exists: ok, Val: value}, nil
}
func (ws *WshServer) SetVarCommand(ctx context.Context, data wshrpc.CommandVarData) error {
_, fileData, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err == fs.ErrNotExist {
fileData = []byte{}
err = filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{})
if err != nil {
return fmt.Errorf("error creating blockfile: %w", err)
}
} else if err != nil {
return fmt.Errorf("error reading blockfile: %w", err)
}
envMap := envutil.EnvToMap(string(fileData))
if data.Remove {
delete(envMap, data.Key)
} else {
envMap[data.Key] = data.Val
}
envStr := envutil.MapToEnv(envMap)
return filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, []byte(envStr))
}