waveterm/pkg/remote/fileshare/fileshare.go
Evan Simkowitz 902ff9baf1
enable wsh file cross-remote copy/move (#1725)
This adds the ability to stream `tar` archives over channels between
`wsh` instances. The main use cases for this are remote copy and move
operations.

It also completes the `wavefs` implementation of the FileShare interface
to allow copy/move interoperability between wavefiles and other storage
types.

The tar streaming functionality has been broken out into the new
`tarcopy` package for easy reuse.

New `fileshare` functions are added for `CopyInternal`, which allows
copying files internal to a filesystem to bypass the expensive interop
layer, and `MoveInternal`, which does the same for moving a file within
a filesystem. Copying between remotes is now handled by `CopyRemote`,
which accepts the source `FileShareClient` as a parameter. `wsh`
connections use the same implementation for `CopyInternal` and
`CopyRemote` as they need to request the channel on the remote
destination, since we don't offer a way to pass channels as a parameter
to a remote call.

This also adds a recursive `-r` flag to `wsh file rm` to allow for
deleting a directory and all its contents.

S3 support will be addressed in a future PR.

---------

Co-authored-by: sawka <mike@commandline.dev>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-31 10:42:39 -08:00

169 lines
6.2 KiB
Go

package fileshare
import (
"context"
"fmt"
"log"
"github.com/wavetermdev/waveterm/pkg/remote/connparse"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/fstype"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wavefs"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
"github.com/wavetermdev/waveterm/pkg/util/iochan/iochantypes"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshutil"
)
const (
ErrorParsingConnection = "error creating fileshare client, could not parse connection %s"
)
// CreateFileShareClient creates a fileshare client based on the connection string
// Returns the client and the parsed connection
func CreateFileShareClient(ctx context.Context, connection string) (fstype.FileShareClient, *connparse.Connection) {
conn, err := connparse.ParseURIAndReplaceCurrentHost(ctx, connection)
if err != nil {
log.Printf("error parsing connection: %v", err)
return nil, nil
}
conntype := conn.GetType()
if conntype == connparse.ConnectionTypeS3 {
// config, err := awsconn.GetConfig(ctx, connection)
// if err != nil {
// log.Printf("error getting aws config: %v", err)
// return nil, nil
// }
return nil, nil
} else if conntype == connparse.ConnectionTypeWave {
return wavefs.NewWaveClient(), conn
} else if conntype == connparse.ConnectionTypeWsh {
return wshfs.NewWshClient(), conn
} else {
log.Printf("unsupported connection type: %s", conntype)
return nil, nil
}
}
func Read(ctx context.Context, data wshrpc.FileData) (*wshrpc.FileData, error) {
client, conn := CreateFileShareClient(ctx, data.Info.Path)
if conn == nil || client == nil {
return nil, fmt.Errorf(ErrorParsingConnection, data.Info.Path)
}
return client.Read(ctx, conn, data)
}
func ReadStream(ctx context.Context, data wshrpc.FileData) <-chan wshrpc.RespOrErrorUnion[wshrpc.FileData] {
client, conn := CreateFileShareClient(ctx, data.Info.Path)
if conn == nil || client == nil {
return wshutil.SendErrCh[wshrpc.FileData](fmt.Errorf(ErrorParsingConnection, data.Info.Path))
}
return client.ReadStream(ctx, conn, data)
}
func ReadTarStream(ctx context.Context, data wshrpc.CommandRemoteStreamTarData) <-chan wshrpc.RespOrErrorUnion[iochantypes.Packet] {
client, conn := CreateFileShareClient(ctx, data.Path)
if conn == nil || client == nil {
return wshutil.SendErrCh[iochantypes.Packet](fmt.Errorf(ErrorParsingConnection, data.Path))
}
return client.ReadTarStream(ctx, conn, data.Opts)
}
func ListEntries(ctx context.Context, path string, opts *wshrpc.FileListOpts) ([]*wshrpc.FileInfo, error) {
client, conn := CreateFileShareClient(ctx, path)
if conn == nil || client == nil {
return nil, fmt.Errorf(ErrorParsingConnection, path)
}
return client.ListEntries(ctx, conn, opts)
}
func ListEntriesStream(ctx context.Context, path string, opts *wshrpc.FileListOpts) <-chan wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteListEntriesRtnData] {
client, conn := CreateFileShareClient(ctx, path)
if conn == nil || client == nil {
return wshutil.SendErrCh[wshrpc.CommandRemoteListEntriesRtnData](fmt.Errorf(ErrorParsingConnection, path))
}
return client.ListEntriesStream(ctx, conn, opts)
}
func Stat(ctx context.Context, path string) (*wshrpc.FileInfo, error) {
client, conn := CreateFileShareClient(ctx, path)
if conn == nil || client == nil {
return nil, fmt.Errorf(ErrorParsingConnection, path)
}
return client.Stat(ctx, conn)
}
func PutFile(ctx context.Context, data wshrpc.FileData) error {
client, conn := CreateFileShareClient(ctx, data.Info.Path)
if conn == nil || client == nil {
return fmt.Errorf(ErrorParsingConnection, data.Info.Path)
}
return client.PutFile(ctx, conn, data)
}
func Mkdir(ctx context.Context, path string) error {
client, conn := CreateFileShareClient(ctx, path)
if conn == nil || client == nil {
return fmt.Errorf(ErrorParsingConnection, path)
}
return client.Mkdir(ctx, conn)
}
func Move(ctx context.Context, data wshrpc.CommandFileCopyData) error {
srcClient, srcConn := CreateFileShareClient(ctx, data.SrcUri)
if srcConn == nil || srcClient == nil {
return fmt.Errorf("error creating fileshare client, could not parse source connection %s", data.SrcUri)
}
destClient, destConn := CreateFileShareClient(ctx, data.DestUri)
if destConn == nil || destClient == nil {
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)
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)
} else {
return srcClient.MoveInternal(ctx, srcConn, destConn, data.Opts)
}
}
func Copy(ctx context.Context, data wshrpc.CommandFileCopyData) error {
srcClient, srcConn := CreateFileShareClient(ctx, data.SrcUri)
if srcConn == nil || srcClient == nil {
return fmt.Errorf("error creating fileshare client, could not parse source connection %s", data.SrcUri)
}
destClient, destConn := CreateFileShareClient(ctx, data.DestUri)
if destConn == nil || destClient == nil {
return fmt.Errorf("error creating fileshare client, could not parse destination connection %s", data.DestUri)
}
if srcConn.Host != destConn.Host {
return destClient.CopyRemote(ctx, srcConn, destConn, srcClient, data.Opts)
} else {
return srcClient.CopyInternal(ctx, srcConn, destConn, data.Opts)
}
}
func Delete(ctx context.Context, data wshrpc.CommandDeleteFileData) error {
client, conn := CreateFileShareClient(ctx, data.Path)
if conn == nil || client == nil {
return fmt.Errorf(ErrorParsingConnection, data.Path)
}
return client.Delete(ctx, conn, data.Recursive)
}
func Join(ctx context.Context, path string, parts ...string) (string, error) {
client, conn := CreateFileShareClient(ctx, path)
if conn == nil || client == nil {
return "", fmt.Errorf(ErrorParsingConnection, path)
}
return client.Join(ctx, conn, parts...)
}
func Append(ctx context.Context, data wshrpc.FileData) error {
client, conn := CreateFileShareClient(ctx, data.Info.Path)
if conn == nil || client == nil {
return fmt.Errorf(ErrorParsingConnection, data.Info.Path)
}
return client.AppendFile(ctx, conn, data)
}