Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions cmd/wsh/cmd/wshcmd-file.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ func init() {
fileCmd.AddCommand(fileInfoCmd)
fileCmd.AddCommand(fileAppendCmd)
fileCpCmd.Flags().BoolP("merge", "m", false, "merge directories")
fileCpCmd.Flags().BoolP("recursive", "r", false, "copy directories recursively")
fileCpCmd.Flags().BoolP("force", "f", false, "force overwrite of existing files")
fileCmd.AddCommand(fileCpCmd)
fileMvCmd.Flags().BoolP("recursive", "r", false, "move directories recursively")
Expand Down Expand Up @@ -174,7 +173,7 @@ var fileAppendCmd = &cobra.Command{
var fileCpCmd = &cobra.Command{
Use: "cp [source-uri] [destination-uri]" + UriHelpText,
Aliases: []string{"copy"},
Short: "copy files between storage systems",
Short: "copy files between storage systems, recursively if needed",
Long: "Copy files between different storage systems." + UriHelpText,
Example: " wsh file cp wavefile://block/config.txt ./local-config.txt\n wsh file cp ./local-config.txt wavefile://block/config.txt\n wsh file cp wsh://user@ec2/home/user/config.txt wavefile://client/config.txt",
Args: cobra.ExactArgs(2),
Expand Down Expand Up @@ -398,10 +397,6 @@ func getTargetPath(src, dst string) (string, error) {

func fileCpRun(cmd *cobra.Command, args []string) error {
src, dst := args[0], args[1]
recursive, err := cmd.Flags().GetBool("recursive")
if err != nil {
return err
}
merge, err := cmd.Flags().GetBool("merge")
if err != nil {
return err
Expand All @@ -419,9 +414,9 @@ func fileCpRun(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("unable to parse dest path: %w", err)
}
log.Printf("Copying %s to %s; recursive: %v, merge: %v, force: %v", srcPath, destPath, recursive, merge, force)
log.Printf("Copying %s to %s; merge: %v, force: %v", srcPath, destPath, merge, force)
rpcOpts := &wshrpc.RpcOpts{Timeout: TimeoutYear}
err = wshclient.FileCopyCommand(RpcClient, wshrpc.CommandFileCopyData{SrcUri: srcPath, DestUri: destPath, Opts: &wshrpc.FileCopyOpts{Recursive: recursive, Merge: merge, Overwrite: force, Timeout: TimeoutYear}}, rpcOpts)
err = wshclient.FileCopyCommand(RpcClient, wshrpc.CommandFileCopyData{SrcUri: srcPath, DestUri: destPath, Opts: &wshrpc.FileCopyOpts{Merge: merge, Overwrite: force, Timeout: TimeoutYear}}, rpcOpts)
if err != nil {
return fmt.Errorf("copying file: %w", err)
}
Expand Down Expand Up @@ -449,7 +444,7 @@ func fileMvRun(cmd *cobra.Command, args []string) error {
}
log.Printf("Moving %s to %s; recursive: %v, force: %v", srcPath, destPath, recursive, force)
rpcOpts := &wshrpc.RpcOpts{Timeout: TimeoutYear}
err = wshclient.FileMoveCommand(RpcClient, wshrpc.CommandFileCopyData{SrcUri: srcPath, DestUri: destPath, Opts: &wshrpc.FileCopyOpts{Recursive: recursive, Overwrite: force, Timeout: TimeoutYear}}, rpcOpts)
err = wshclient.FileMoveCommand(RpcClient, wshrpc.CommandFileCopyData{SrcUri: srcPath, DestUri: destPath, Opts: &wshrpc.FileCopyOpts{Overwrite: force, Timeout: TimeoutYear, Recursive: recursive}}, rpcOpts)
if err != nil {
return fmt.Errorf("moving file: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/wsh/cmd/wshcmd-view.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var viewMagnified bool

var viewCmd = &cobra.Command{
Use: "view {file|directory|URL}",
Aliases: []string{"preview", "open"},
Short: "preview/edit a file or directory",
RunE: viewRun,
PreRunE: preRunSetupRpcClient,
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ class RpcApiType {
}

// command "remotefilecopy" [call]
RemoteFileCopyCommand(client: WshClient, data: CommandRemoteFileCopyData, opts?: RpcOpts): Promise<void> {
RemoteFileCopyCommand(client: WshClient, data: CommandFileCopyData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefilecopy", data, opts);
}

Expand All @@ -283,7 +283,7 @@ class RpcApiType {
}

// command "remotefilemove" [call]
RemoteFileMoveCommand(client: WshClient, data: CommandRemoteFileCopyData, opts?: RpcOpts): Promise<void> {
RemoteFileMoveCommand(client: WshClient, data: CommandFileCopyData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("remotefilemove", data, opts);
}

Expand Down
7 changes: 0 additions & 7 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,6 @@ declare global {
message: string;
};

// wshrpc.CommandRemoteFileCopyData
type CommandRemoteFileCopyData = {
srcuri: string;
desturi: string;
opts?: FileCopyOpts;
};

// wshrpc.CommandRemoteListEntriesData
type CommandRemoteListEntriesData = {
path: string;
Expand Down
12 changes: 10 additions & 2 deletions pkg/remote/fileshare/fileshare.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,19 @@ func Move(ctx context.Context, data wshrpc.CommandFileCopyData) error {
return fmt.Errorf("error creating fileshare client, could not parse destination connection %s", data.DestUri)
}
if srcConn.Host != destConn.Host {
err := destClient.CopyRemote(ctx, srcConn, destConn, srcClient, data.Opts)
finfo, err := srcClient.Stat(ctx, srcConn)
if err != nil {
return fmt.Errorf("cannot stat %q: %w", data.SrcUri, err)
}
recursive := data.Opts != nil && data.Opts.Recursive
if finfo.IsDir && data.Opts != nil && !recursive {
return fmt.Errorf("cannot move directory %q to %q without recursive flag", data.SrcUri, data.DestUri)
}
err = destClient.CopyRemote(ctx, srcConn, destConn, srcClient, data.Opts)
if err != nil {
return fmt.Errorf("cannot copy %q to %q: %w", data.SrcUri, data.DestUri, err)
}
return srcClient.Delete(ctx, srcConn, data.Opts.Recursive)
return srcClient.Delete(ctx, srcConn, recursive)
} else {
return srcClient.MoveInternal(ctx, srcConn, destConn, data.Opts)
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/remote/fileshare/fstype/fstype.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ package fstype

import (
"context"
"time"

"github.com/wavetermdev/waveterm/pkg/remote/connparse"
"github.com/wavetermdev/waveterm/pkg/util/iochan/iochantypes"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)

const (
DefaultTimeout = 30 * time.Second
)

type FileShareClient interface {
// Stat returns the file info at the given parsed connection path
Stat(ctx context.Context, conn *connparse.Connection) (*wshrpc.FileInfo, error)
Expand Down
42 changes: 24 additions & 18 deletions pkg/remote/fileshare/wavefs/wavefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshutil"
)

const (
DefaultTimeout = 30 * time.Second
)

type WaveClient struct{}

var _ fstype.FileShareClient = WaveClient{}
Expand All @@ -54,7 +50,7 @@ func (c WaveClient) ReadStream(ctx context.Context, conn *connparse.Connection,
if !rtnData.Info.IsDir {
for i := 0; i < dataLen; i += wshrpc.FileChunkSize {
if ctx.Err() != nil {
ch <- wshutil.RespErr[wshrpc.FileData](ctx.Err())
ch <- wshutil.RespErr[wshrpc.FileData](context.Cause(ctx))
return
}
dataEnd := min(i+wshrpc.FileChunkSize, dataLen)
Expand All @@ -63,7 +59,7 @@ func (c WaveClient) ReadStream(ctx context.Context, conn *connparse.Connection,
} else {
for i := 0; i < len(rtnData.Entries); i += wshrpc.DirChunkSize {
if ctx.Err() != nil {
ch <- wshutil.RespErr[wshrpc.FileData](ctx.Err())
ch <- wshutil.RespErr[wshrpc.FileData](context.Cause(ctx))
return
}
ch <- wshrpc.RespOrErrorUnion[wshrpc.FileData]{Response: wshrpc.FileData{Entries: rtnData.Entries[i:min(i+wshrpc.DirChunkSize, len(rtnData.Entries))], Info: rtnData.Info}}
Expand Down Expand Up @@ -116,7 +112,7 @@ func (c WaveClient) ReadTarStream(ctx context.Context, conn *connparse.Connectio
pathPrefix := getPathPrefix(conn)
schemeAndHost := conn.GetSchemeAndHost() + "/"

timeout := DefaultTimeout
timeout := fstype.DefaultTimeout
if opts.Timeout > 0 {
timeout = time.Duration(opts.Timeout) * time.Millisecond
}
Expand All @@ -130,12 +126,12 @@ func (c WaveClient) ReadTarStream(ctx context.Context, conn *connparse.Connectio
}()
for _, file := range list {
if readerCtx.Err() != nil {
rtn <- wshutil.RespErr[iochantypes.Packet](readerCtx.Err())
rtn <- wshutil.RespErr[iochantypes.Packet](context.Cause(readerCtx))
return
}
file.Mode = 0644

if err = writeHeader(fileutil.ToFsFileInfo(file), file.Path); err != nil {
if err = writeHeader(fileutil.ToFsFileInfo(file), file.Path, file.Path == conn.Path); err != nil {
rtn <- wshutil.RespErr[iochantypes.Packet](fmt.Errorf("error writing tar header: %w", err))
return
}
Expand Down Expand Up @@ -447,27 +443,37 @@ func (c WaveClient) CopyRemote(ctx context.Context, srcConn, destConn *connparse
if zoneId == "" {
return fmt.Errorf("zoneid not found in connection")
}
overwrite := opts != nil && opts.Overwrite
merge := opts != nil && opts.Merge
destHasSlash := strings.HasSuffix(destConn.Path, "/")
destPrefix := getPathPrefix(destConn)
destPrefix = strings.TrimPrefix(destPrefix, destConn.GetSchemeAndHost()+"/")
log.Printf("CopyRemote: srcConn: %v, destConn: %v, destPrefix: %s\n", srcConn, destConn, destPrefix)
entries, err := c.ListEntries(ctx, srcConn, nil)
if err != nil {
return fmt.Errorf("error listing blockfiles: %w", err)
}
if len(entries) > 1 && !merge {
return fmt.Errorf("more than one entry at destination prefix, use merge flag to copy")
}
readCtx, cancel := context.WithCancelCause(ctx)
ioch := srcClient.ReadTarStream(readCtx, srcConn, opts)
err := tarcopy.TarCopyDest(readCtx, cancel, ioch, func(next *tar.Header, reader *tar.Reader) error {
err = tarcopy.TarCopyDest(readCtx, cancel, ioch, func(next *tar.Header, reader *tar.Reader, singleFile bool) error {
if next.Typeflag == tar.TypeDir {
return nil
}
fileName, err := cleanPath(path.Join(destPrefix, next.Name))
if singleFile && !destHasSlash {
fileName, err = cleanPath(destConn.Path)
}
if err != nil {
return fmt.Errorf("error cleaning path: %w", err)
}
_, err = filestore.WFS.Stat(ctx, zoneId, fileName)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("error getting blockfile info: %w", err)
}
err := filestore.WFS.MakeFile(ctx, zoneId, fileName, nil, wshrpc.FileOpts{})
if err != nil {
return fmt.Errorf("error making blockfile: %w", err)
if !overwrite {
for _, entry := range entries {
if entry.Name == fileName {
return fmt.Errorf("destination already exists: %v", fileName)
}
}
}
log.Printf("CopyRemote: writing file: %s; size: %d\n", fileName, next.Size)
Expand Down
4 changes: 2 additions & 2 deletions pkg/remote/fileshare/wshfs/wshfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func (c WshClient) MoveInternal(ctx context.Context, srcConn, destConn *connpars
if timeout == 0 {
timeout = ThirtySeconds
}
return wshclient.RemoteFileMoveCommand(RpcClient, wshrpc.CommandRemoteFileCopyData{SrcUri: srcConn.GetFullURI(), DestUri: destConn.GetFullURI(), Opts: opts}, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(destConn.Host), Timeout: timeout})
return wshclient.RemoteFileMoveCommand(RpcClient, wshrpc.CommandFileCopyData{SrcUri: srcConn.GetFullURI(), DestUri: destConn.GetFullURI(), Opts: opts}, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(destConn.Host), Timeout: timeout})
}

func (c WshClient) CopyRemote(ctx context.Context, srcConn, destConn *connparse.Connection, _ fstype.FileShareClient, opts *wshrpc.FileCopyOpts) error {
Expand All @@ -172,7 +172,7 @@ func (c WshClient) CopyInternal(ctx context.Context, srcConn, destConn *connpars
if timeout == 0 {
timeout = ThirtySeconds
}
return wshclient.RemoteFileCopyCommand(RpcClient, wshrpc.CommandRemoteFileCopyData{SrcUri: srcConn.GetFullURI(), DestUri: destConn.GetFullURI(), Opts: opts}, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(destConn.Host), Timeout: timeout})
return wshclient.RemoteFileCopyCommand(RpcClient, wshrpc.CommandFileCopyData{SrcUri: srcConn.GetFullURI(), DestUri: destConn.GetFullURI(), Opts: opts}, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(destConn.Host), Timeout: timeout})
}

func (c WshClient) Delete(ctx context.Context, conn *connparse.Connection, recursive bool) error {
Expand Down
4 changes: 4 additions & 0 deletions pkg/util/fileutil/fileutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
)

func FixPath(path string) (string, error) {
origPath := path
var err error
if strings.HasPrefix(path, "~") {
path = filepath.Join(wavebase.GetHomeDir(), path[1:])
Expand All @@ -28,6 +29,9 @@ func FixPath(path string) (string, error) {
return "", err
}
}
if strings.HasSuffix(origPath, "/") && !strings.HasSuffix(path, "/") {
path += "/"
}
return path, nil
}

Expand Down
55 changes: 40 additions & 15 deletions pkg/util/tarcopy/tarcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,53 @@ const (
pipeReaderName = "pipe reader"
pipeWriterName = "pipe writer"
tarWriterName = "tar writer"

// custom flag to indicate that the source is a single file
SingleFile = "singlefile"
)

// TarCopySrc creates a tar stream writer and returns a channel to send the tar stream to.
// writeHeader is a function that writes the tar header for the file.
// writeHeader is a function that writes the tar header for the file. If only a single file is being written, the singleFile flag should be set to true.
// writer is the tar writer to write the file data to.
// close is a function that closes the tar writer and internal pipe writer.
func TarCopySrc(ctx context.Context, pathPrefix string) (outputChan chan wshrpc.RespOrErrorUnion[iochantypes.Packet], writeHeader func(fi fs.FileInfo, file string) error, writer io.Writer, close func()) {
func TarCopySrc(ctx context.Context, pathPrefix string) (outputChan chan wshrpc.RespOrErrorUnion[iochantypes.Packet], writeHeader func(fi fs.FileInfo, file string, singleFile bool) error, writer io.Writer, close func()) {
pipeReader, pipeWriter := io.Pipe()
tarWriter := tar.NewWriter(pipeWriter)
rtnChan := iochan.ReaderChan(ctx, pipeReader, wshrpc.FileChunkSize, func() {
gracefulClose(pipeReader, tarCopySrcName, pipeReaderName)
})

return rtnChan, func(fi fs.FileInfo, file string) error {
singleFileFlagSet := false

return rtnChan, func(fi fs.FileInfo, path string, singleFile bool) error {
// generate tar header
header, err := tar.FileInfoHeader(fi, file)
header, err := tar.FileInfoHeader(fi, path)
if err != nil {
return err
}

header.Name = filepath.Clean(strings.TrimPrefix(file, pathPrefix))
if err := validatePath(header.Name); err != nil {
if singleFile {
if singleFileFlagSet {
return errors.New("attempting to write multiple files to a single file tar stream")
}

header.PAXRecords = map[string]string{SingleFile: "true"}
singleFileFlagSet = true
}

path, err = fixPath(path, pathPrefix)
if err != nil {
return err
}

// skip if path is empty, which means the file is the root directory
if path == "" {
return nil
}
header.Name = path

log.Printf("TarCopySrc: header name: %v\n", header.Name)

// write header
if err := tarWriter.WriteHeader(header); err != nil {
return err
Expand All @@ -65,20 +87,18 @@ func TarCopySrc(ctx context.Context, pathPrefix string) (outputChan chan wshrpc.
}
}

func validatePath(path string) error {
func fixPath(path string, prefix string) (string, error) {
path = strings.TrimPrefix(strings.TrimPrefix(filepath.Clean(strings.TrimPrefix(path, prefix)), "/"), "\\")
if strings.Contains(path, "..") {
return fmt.Errorf("invalid tar path containing directory traversal: %s", path)
return "", fmt.Errorf("invalid tar path containing directory traversal: %s", path)
}
if strings.HasPrefix(path, "/") {
return fmt.Errorf("invalid tar path starting with /: %s", path)
}
return nil
return path, nil
}

// TarCopyDest reads a tar stream from a channel and writes the files to the destination.
// readNext is a function that is called for each file in the tar stream to read the file data. It should return an error if the file cannot be read.
// readNext is a function that is called for each file in the tar stream to read the file data. If only a single file is being written from the tar src, the singleFile flag will be set in this callback. It should return an error if the file cannot be read.
// The function returns an error if the tar stream cannot be read.
func TarCopyDest(ctx context.Context, cancel context.CancelCauseFunc, ch <-chan wshrpc.RespOrErrorUnion[iochantypes.Packet], readNext func(next *tar.Header, reader *tar.Reader) error) error {
func TarCopyDest(ctx context.Context, cancel context.CancelCauseFunc, ch <-chan wshrpc.RespOrErrorUnion[iochantypes.Packet], readNext func(next *tar.Header, reader *tar.Reader, singleFile bool) error) error {
pipeReader, pipeWriter := io.Pipe()
iochan.WriterChan(ctx, pipeWriter, ch, func() {
gracefulClose(pipeWriter, tarCopyDestName, pipeWriterName)
Expand Down Expand Up @@ -110,7 +130,12 @@ func TarCopyDest(ctx context.Context, cancel context.CancelCauseFunc, ch <-chan
return err
}
}
err = readNext(next, tarReader)

// Check for directory traversal
if strings.Contains(next.Name, "..") {
return nil
}
err = readNext(next, tarReader, next.PAXRecords != nil && next.PAXRecords[SingleFile] == "true")
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/wshrpc/wshclient/wshclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func RecordTEventCommand(w *wshutil.WshRpc, data telemetrydata.TEvent, opts *wsh
}

// command "remotefilecopy", wshserver.RemoteFileCopyCommand
func RemoteFileCopyCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteFileCopyData, opts *wshrpc.RpcOpts) error {
func RemoteFileCopyCommand(w *wshutil.WshRpc, data wshrpc.CommandFileCopyData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "remotefilecopy", data, opts)
return err
}
Expand All @@ -345,7 +345,7 @@ func RemoteFileJoinCommand(w *wshutil.WshRpc, data []string, opts *wshrpc.RpcOpt
}

// command "remotefilemove", wshserver.RemoteFileMoveCommand
func RemoteFileMoveCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteFileCopyData, opts *wshrpc.RpcOpts) error {
func RemoteFileMoveCommand(w *wshutil.WshRpc, data wshrpc.CommandFileCopyData, opts *wshrpc.RpcOpts) error {
_, err := sendRpcRequestCallHelper[any](w, "remotefilemove", data, opts)
return err
}
Expand Down
Loading