mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
wsh getva, setvar, and file commands (#1317)
This commit is contained in:
parent
0acad2fbe2
commit
271d8e2e9c
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
func connEnsureRun(cmd *cobra.Command, args []string) error {
|
||||
connName := args[0]
|
||||
if err := validateConnectionName(connName); err != nil {
|
||||
return err
|
||||
}
|
||||
connName = args[1]
|
||||
_, err := remote.ParseOpts(connName)
|
||||
if err != nil && !strings.HasPrefix(connName, "wsl://") {
|
||||
return fmt.Errorf("cannot parse connection name: %w", 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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
212
cmd/wsh/cmd/wshcmd-file-util.go
Normal file
212
cmd/wsh/cmd/wshcmd-file-util.go
Normal 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
632
cmd/wsh/cmd/wshcmd-file.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
132
cmd/wsh/cmd/wshcmd-getvar.go
Normal file
132
cmd/wsh/cmd/wshcmd-getvar.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
101
cmd/wsh/cmd/wshcmd-setvar.go
Normal file
101
cmd/wsh/cmd/wshcmd-setvar.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
|
52
frontend/types/gotypes.d.ts
vendored
52
frontend/types/gotypes.d.ts
vendored
@ -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;
|
||||
|
88
pkg/util/colprint/colprint.go
Normal file
88
pkg/util/colprint/colprint.go
Normal 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
|
||||
}
|
69
pkg/util/envutil/envutil.go
Normal file
69
pkg/util/envutil/envutil.go
Normal 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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ type SubscriptionRequest struct {
|
||||
}
|
||||
|
||||
const (
|
||||
FileOp_Create = "create"
|
||||
FileOp_Delete = "delete"
|
||||
FileOp_Append = "append"
|
||||
FileOp_Truncate = "truncate"
|
||||
FileOp_Invalidate = "invalidate"
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
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"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,15 +266,153 @@ 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 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,
|
||||
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
|
||||
@ -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 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
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user