mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-23 02:51:26 +01:00
implement remotewritefile
This commit is contained in:
parent
c962391b71
commit
961502916b
@ -4,23 +4,13 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshremote"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MaxFileSize = 50 * 1024 * 1024 // 10M
|
|
||||||
|
|
||||||
var serverCmd = &cobra.Command{
|
var serverCmd = &cobra.Command{
|
||||||
Use: "server",
|
Use: "server",
|
||||||
Short: "remote server to power wave blocks",
|
Short: "remote server to power wave blocks",
|
||||||
@ -28,207 +18,13 @@ var serverCmd = &cobra.Command{
|
|||||||
Run: serverRun,
|
Run: serverRun,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerImpl struct{}
|
|
||||||
|
|
||||||
func (*ServerImpl) WshServerImpl() {}
|
|
||||||
|
|
||||||
func (*ServerImpl) MessageCommand(ctx context.Context, data wshrpc.CommandMessageData) error {
|
|
||||||
WriteStderr("[message] %q\n", data.Message)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func respErr(err error) wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData] {
|
|
||||||
return wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData]{Error: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ByteRangeType struct {
|
|
||||||
All bool
|
|
||||||
Start int64
|
|
||||||
End int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseByteRange(rangeStr string) (ByteRangeType, error) {
|
|
||||||
if rangeStr == "" {
|
|
||||||
return ByteRangeType{All: true}, nil
|
|
||||||
}
|
|
||||||
var start, end int64
|
|
||||||
_, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end)
|
|
||||||
if err != nil {
|
|
||||||
return ByteRangeType{}, errors.New("invalid byte range")
|
|
||||||
}
|
|
||||||
if start < 0 || end < 0 || start > end {
|
|
||||||
return ByteRangeType{}, errors.New("invalid byte range")
|
|
||||||
}
|
|
||||||
return ByteRangeType{Start: start, End: end}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (impl *ServerImpl) remoteStreamFileDir(ctx context.Context, path string, byteRange ByteRangeType, dataCallback func(fileInfo *wshrpc.FileInfo, data []byte)) error {
|
|
||||||
innerFilesEntries, err := os.ReadDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot open dir %q: %w", path, err)
|
|
||||||
}
|
|
||||||
if byteRange.All {
|
|
||||||
if len(innerFilesEntries) > 1000 {
|
|
||||||
innerFilesEntries = innerFilesEntries[:1000]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if byteRange.Start >= int64(len(innerFilesEntries)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
realEnd := byteRange.End
|
|
||||||
if realEnd > int64(len(innerFilesEntries)) {
|
|
||||||
realEnd = int64(len(innerFilesEntries))
|
|
||||||
}
|
|
||||||
innerFilesEntries = innerFilesEntries[byteRange.Start:realEnd]
|
|
||||||
}
|
|
||||||
parent := filepath.Dir(path)
|
|
||||||
parentFileInfo, err := impl.RemoteFileInfoCommand(ctx, parent)
|
|
||||||
if err == nil && parent != path {
|
|
||||||
parentFileInfo.Name = ".."
|
|
||||||
parentFileInfo.Size = -1
|
|
||||||
dataCallback(parentFileInfo, nil)
|
|
||||||
}
|
|
||||||
for _, innerFileEntry := range innerFilesEntries {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
innerFileInfoInt, err := innerFileEntry.Info()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mimeType := utilfn.DetectMimeType(filepath.Join(path, innerFileInfoInt.Name()))
|
|
||||||
var fileSize int64
|
|
||||||
if mimeType == "directory" {
|
|
||||||
fileSize = -1
|
|
||||||
} else {
|
|
||||||
fileSize = innerFileInfoInt.Size()
|
|
||||||
}
|
|
||||||
innerFileInfo := wshrpc.FileInfo{
|
|
||||||
Path: filepath.Join(path, innerFileInfoInt.Name()),
|
|
||||||
Name: innerFileInfoInt.Name(),
|
|
||||||
Size: fileSize,
|
|
||||||
Mode: innerFileInfoInt.Mode(),
|
|
||||||
ModeStr: innerFileInfoInt.Mode().String(),
|
|
||||||
ModTime: innerFileInfoInt.ModTime().UnixMilli(),
|
|
||||||
IsDir: innerFileInfoInt.IsDir(),
|
|
||||||
MimeType: mimeType,
|
|
||||||
}
|
|
||||||
dataCallback(&innerFileInfo, nil)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (impl *ServerImpl) remoteStreamFileRegular(ctx context.Context, path string, byteRange ByteRangeType, dataCallback func(fileInfo *wshrpc.FileInfo, data []byte)) error {
|
|
||||||
fd, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot open file %q: %w", path, err)
|
|
||||||
}
|
|
||||||
defer fd.Close()
|
|
||||||
var filePos int64
|
|
||||||
if !byteRange.All && byteRange.Start > 0 {
|
|
||||||
_, err := fd.Seek(byteRange.Start, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("seeking file %q: %w", path, err)
|
|
||||||
}
|
|
||||||
filePos = byteRange.Start
|
|
||||||
}
|
|
||||||
buf := make([]byte, 4096)
|
|
||||||
for {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
n, err := fd.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
if !byteRange.All && filePos+int64(n) > byteRange.End {
|
|
||||||
n = int(byteRange.End - filePos)
|
|
||||||
}
|
|
||||||
filePos += int64(n)
|
|
||||||
dataCallback(nil, buf[:n])
|
|
||||||
}
|
|
||||||
if filePos >= byteRange.End {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading file %q: %w", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (impl *ServerImpl) remoteStreamFileInternal(ctx context.Context, data wshrpc.CommandRemoteStreamFileData, dataCallback func(fileInfo *wshrpc.FileInfo, data []byte)) error {
|
|
||||||
byteRange, err := parseByteRange(data.ByteRange)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
path := data.Path
|
|
||||||
path = wavebase.ExpandHomeDir(path)
|
|
||||||
finfo, err := impl.RemoteFileInfoCommand(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot stat file %q: %w", path, err)
|
|
||||||
}
|
|
||||||
dataCallback(finfo, nil)
|
|
||||||
if finfo.NotFound {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if finfo.Size > MaxFileSize {
|
|
||||||
return fmt.Errorf("file %q is too large to read, use /wave/stream-file", path)
|
|
||||||
}
|
|
||||||
if finfo.IsDir {
|
|
||||||
return impl.remoteStreamFileDir(ctx, path, byteRange, dataCallback)
|
|
||||||
} else {
|
|
||||||
return impl.remoteStreamFileRegular(ctx, path, byteRange, dataCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (impl *ServerImpl) RemoteStreamFileCommand(ctx context.Context, data wshrpc.CommandRemoteStreamFileData) chan wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData] {
|
|
||||||
ch := make(chan wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData], 16)
|
|
||||||
defer close(ch)
|
|
||||||
err := impl.remoteStreamFileInternal(ctx, data, func(fileInfo *wshrpc.FileInfo, data []byte) {
|
|
||||||
resp := wshrpc.CommandRemoteStreamFileRtnData{}
|
|
||||||
resp.FileInfo = fileInfo
|
|
||||||
if len(data) > 0 {
|
|
||||||
resp.Data64 = base64.RawStdEncoding.EncodeToString(data)
|
|
||||||
}
|
|
||||||
ch <- wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData]{Response: resp}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ch <- respErr(err)
|
|
||||||
}
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string) (*wshrpc.FileInfo, error) {
|
|
||||||
cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path))
|
|
||||||
finfo, err := os.Stat(cleanedPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return &wshrpc.FileInfo{Path: wavebase.ReplaceHomeDir(path), NotFound: true}, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot stat file %q: %w", path, err)
|
|
||||||
}
|
|
||||||
mimeType := utilfn.DetectMimeType(cleanedPath)
|
|
||||||
return &wshrpc.FileInfo{
|
|
||||||
Path: cleanedPath,
|
|
||||||
Name: finfo.Name(),
|
|
||||||
Size: finfo.Size(),
|
|
||||||
Mode: finfo.Mode(),
|
|
||||||
ModeStr: finfo.Mode().String(),
|
|
||||||
ModTime: finfo.ModTime().UnixMilli(),
|
|
||||||
IsDir: finfo.IsDir(),
|
|
||||||
MimeType: mimeType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(serverCmd)
|
rootCmd.AddCommand(serverCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serverRun(cmd *cobra.Command, args []string) {
|
func serverRun(cmd *cobra.Command, args []string) {
|
||||||
WriteStdout("running wsh server\n")
|
WriteStdout("running wsh server\n")
|
||||||
RpcClient.SetServerImpl(&ServerImpl{})
|
RpcClient.SetServerImpl(&wshremote.ServerImpl{LogWriter: os.Stdout})
|
||||||
err := wshclient.TestCommand(RpcClient, "hello", nil)
|
err := wshclient.TestCommand(RpcClient, "hello", nil)
|
||||||
WriteStdout("got test rtn: %v\n", err)
|
WriteStdout("got test rtn: %v\n", err)
|
||||||
|
|
||||||
|
@ -97,6 +97,11 @@ class WshServerType {
|
|||||||
return WOS.wshServerRpcHelper_responsestream("remotestreamfile", data, opts);
|
return WOS.wshServerRpcHelper_responsestream("remotestreamfile", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "remotewritefile" [call]
|
||||||
|
RemoteWriteFileCommand(data: CommandRemoteWriteFileData, opts?: RpcOpts): Promise<void> {
|
||||||
|
return WOS.wshServerRpcHelper_call("remotewritefile", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
// command "resolveids" [call]
|
// command "resolveids" [call]
|
||||||
ResolveIdsCommand(data: CommandResolveIdsData, opts?: RpcOpts): Promise<CommandResolveIdsRtnData> {
|
ResolveIdsCommand(data: CommandResolveIdsData, opts?: RpcOpts): Promise<CommandResolveIdsRtnData> {
|
||||||
return WOS.wshServerRpcHelper_call("resolveids", data, opts);
|
return WOS.wshServerRpcHelper_call("resolveids", data, opts);
|
||||||
|
7
frontend/types/gotypes.d.ts
vendored
7
frontend/types/gotypes.d.ts
vendored
@ -128,6 +128,13 @@ declare global {
|
|||||||
data64?: string;
|
data64?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// wshrpc.CommandRemoteWriteFileData
|
||||||
|
type CommandRemoteWriteFileData = {
|
||||||
|
path: string;
|
||||||
|
data64: string;
|
||||||
|
createmode?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// wshrpc.CommandResolveIdsData
|
// wshrpc.CommandResolveIdsData
|
||||||
type CommandResolveIdsData = {
|
type CommandResolveIdsData = {
|
||||||
blockid: string;
|
blockid: string;
|
||||||
|
@ -118,6 +118,12 @@ func RemoteStreamFileCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteStreamF
|
|||||||
return sendRpcRequestResponseStreamHelper[wshrpc.CommandRemoteStreamFileRtnData](w, "remotestreamfile", data, opts)
|
return sendRpcRequestResponseStreamHelper[wshrpc.CommandRemoteStreamFileRtnData](w, "remotestreamfile", data, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "remotewritefile", wshserver.RemoteWriteFileCommand
|
||||||
|
func RemoteWriteFileCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteWriteFileData, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "remotewritefile", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// command "resolveids", wshserver.ResolveIdsCommand
|
// command "resolveids", wshserver.ResolveIdsCommand
|
||||||
func ResolveIdsCommand(w *wshutil.WshRpc, data wshrpc.CommandResolveIdsData, opts *wshrpc.RpcOpts) (wshrpc.CommandResolveIdsRtnData, error) {
|
func ResolveIdsCommand(w *wshutil.WshRpc, data wshrpc.CommandResolveIdsData, opts *wshrpc.RpcOpts) (wshrpc.CommandResolveIdsRtnData, error) {
|
||||||
resp, err := sendRpcRequestCallHelper[wshrpc.CommandResolveIdsRtnData](w, "resolveids", data, opts)
|
resp, err := sendRpcRequestCallHelper[wshrpc.CommandResolveIdsRtnData](w, "resolveids", data, opts)
|
||||||
|
244
pkg/wshrpc/wshremote/wshremote.go
Normal file
244
pkg/wshrpc/wshremote/wshremote.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package wshremote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MaxFileSize = 50 * 1024 * 1024 // 10M
|
||||||
|
|
||||||
|
type ServerImpl struct {
|
||||||
|
LogWriter io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerImpl) WshServerImpl() {}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) Log(format string, args ...interface{}) {
|
||||||
|
if impl.LogWriter != nil {
|
||||||
|
fmt.Fprintf(impl.LogWriter, format, args...)
|
||||||
|
} else {
|
||||||
|
log.Printf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) MessageCommand(ctx context.Context, data wshrpc.CommandMessageData) error {
|
||||||
|
impl.Log("[message] %q\n", data.Message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func respErr(err error) wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData] {
|
||||||
|
return wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData]{Error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByteRangeType struct {
|
||||||
|
All bool
|
||||||
|
Start int64
|
||||||
|
End int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseByteRange(rangeStr string) (ByteRangeType, error) {
|
||||||
|
if rangeStr == "" {
|
||||||
|
return ByteRangeType{All: true}, nil
|
||||||
|
}
|
||||||
|
var start, end int64
|
||||||
|
_, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end)
|
||||||
|
if err != nil {
|
||||||
|
return ByteRangeType{}, errors.New("invalid byte range")
|
||||||
|
}
|
||||||
|
if start < 0 || end < 0 || start > end {
|
||||||
|
return ByteRangeType{}, errors.New("invalid byte range")
|
||||||
|
}
|
||||||
|
return ByteRangeType{Start: start, End: end}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) remoteStreamFileDir(ctx context.Context, path string, byteRange ByteRangeType, dataCallback func(fileInfo *wshrpc.FileInfo, data []byte)) error {
|
||||||
|
innerFilesEntries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot open dir %q: %w", path, err)
|
||||||
|
}
|
||||||
|
if byteRange.All {
|
||||||
|
if len(innerFilesEntries) > 1000 {
|
||||||
|
innerFilesEntries = innerFilesEntries[:1000]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if byteRange.Start >= int64(len(innerFilesEntries)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
realEnd := byteRange.End
|
||||||
|
if realEnd > int64(len(innerFilesEntries)) {
|
||||||
|
realEnd = int64(len(innerFilesEntries))
|
||||||
|
}
|
||||||
|
innerFilesEntries = innerFilesEntries[byteRange.Start:realEnd]
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(path)
|
||||||
|
parentFileInfo, err := impl.RemoteFileInfoCommand(ctx, parent)
|
||||||
|
if err == nil && parent != path {
|
||||||
|
parentFileInfo.Name = ".."
|
||||||
|
parentFileInfo.Size = -1
|
||||||
|
dataCallback(parentFileInfo, nil)
|
||||||
|
}
|
||||||
|
for _, innerFileEntry := range innerFilesEntries {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
innerFileInfoInt, err := innerFileEntry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mimeType := utilfn.DetectMimeType(filepath.Join(path, innerFileInfoInt.Name()))
|
||||||
|
var fileSize int64
|
||||||
|
if mimeType == "directory" {
|
||||||
|
fileSize = -1
|
||||||
|
} else {
|
||||||
|
fileSize = innerFileInfoInt.Size()
|
||||||
|
}
|
||||||
|
innerFileInfo := wshrpc.FileInfo{
|
||||||
|
Path: filepath.Join(path, innerFileInfoInt.Name()),
|
||||||
|
Name: innerFileInfoInt.Name(),
|
||||||
|
Size: fileSize,
|
||||||
|
Mode: innerFileInfoInt.Mode(),
|
||||||
|
ModeStr: innerFileInfoInt.Mode().String(),
|
||||||
|
ModTime: innerFileInfoInt.ModTime().UnixMilli(),
|
||||||
|
IsDir: innerFileInfoInt.IsDir(),
|
||||||
|
MimeType: mimeType,
|
||||||
|
}
|
||||||
|
dataCallback(&innerFileInfo, nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) remoteStreamFileRegular(ctx context.Context, path string, byteRange ByteRangeType, dataCallback func(fileInfo *wshrpc.FileInfo, data []byte)) error {
|
||||||
|
fd, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot open file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
var filePos int64
|
||||||
|
if !byteRange.All && byteRange.Start > 0 {
|
||||||
|
_, err := fd.Seek(byteRange.Start, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("seeking file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
filePos = byteRange.Start
|
||||||
|
}
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
n, err := fd.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if !byteRange.All && filePos+int64(n) > byteRange.End {
|
||||||
|
n = int(byteRange.End - filePos)
|
||||||
|
}
|
||||||
|
filePos += int64(n)
|
||||||
|
dataCallback(nil, buf[:n])
|
||||||
|
}
|
||||||
|
if filePos >= byteRange.End {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) remoteStreamFileInternal(ctx context.Context, data wshrpc.CommandRemoteStreamFileData, dataCallback func(fileInfo *wshrpc.FileInfo, data []byte)) error {
|
||||||
|
byteRange, err := parseByteRange(data.ByteRange)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := data.Path
|
||||||
|
path = wavebase.ExpandHomeDir(path)
|
||||||
|
finfo, err := impl.RemoteFileInfoCommand(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot stat file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
dataCallback(finfo, nil)
|
||||||
|
if finfo.NotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if finfo.Size > MaxFileSize {
|
||||||
|
return fmt.Errorf("file %q is too large to read, use /wave/stream-file", path)
|
||||||
|
}
|
||||||
|
if finfo.IsDir {
|
||||||
|
return impl.remoteStreamFileDir(ctx, path, byteRange, dataCallback)
|
||||||
|
} else {
|
||||||
|
return impl.remoteStreamFileRegular(ctx, path, byteRange, dataCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) RemoteStreamFileCommand(ctx context.Context, data wshrpc.CommandRemoteStreamFileData) chan wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData] {
|
||||||
|
ch := make(chan wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData], 16)
|
||||||
|
defer close(ch)
|
||||||
|
err := impl.remoteStreamFileInternal(ctx, data, func(fileInfo *wshrpc.FileInfo, data []byte) {
|
||||||
|
resp := wshrpc.CommandRemoteStreamFileRtnData{}
|
||||||
|
resp.FileInfo = fileInfo
|
||||||
|
if len(data) > 0 {
|
||||||
|
resp.Data64 = base64.RawStdEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
ch <- wshrpc.RespOrErrorUnion[wshrpc.CommandRemoteStreamFileRtnData]{Response: resp}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ch <- respErr(err)
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string) (*wshrpc.FileInfo, error) {
|
||||||
|
cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path))
|
||||||
|
finfo, err := os.Stat(cleanedPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &wshrpc.FileInfo{Path: wavebase.ReplaceHomeDir(path), NotFound: true}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot stat file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
mimeType := utilfn.DetectMimeType(cleanedPath)
|
||||||
|
return &wshrpc.FileInfo{
|
||||||
|
Path: cleanedPath,
|
||||||
|
Name: finfo.Name(),
|
||||||
|
Size: finfo.Size(),
|
||||||
|
Mode: finfo.Mode(),
|
||||||
|
ModeStr: finfo.Mode().String(),
|
||||||
|
ModTime: finfo.ModTime().UnixMilli(),
|
||||||
|
IsDir: finfo.IsDir(),
|
||||||
|
MimeType: mimeType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
|
||||||
|
path := wavebase.ExpandHomeDir(data.Path)
|
||||||
|
createMode := data.CreateMode
|
||||||
|
if createMode == 0 {
|
||||||
|
createMode = 0644
|
||||||
|
}
|
||||||
|
dataSize := base64.StdEncoding.DecodedLen(len(data.Data64))
|
||||||
|
dataBytes := make([]byte, dataSize)
|
||||||
|
n, err := base64.StdEncoding.Decode(dataBytes, []byte(data.Data64))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot decode base64 data: %w", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path, dataBytes[:n], createMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot write file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -49,6 +49,7 @@ const (
|
|||||||
Command_Test = "test"
|
Command_Test = "test"
|
||||||
Command_RemoteStreamFile = "remotestreamfile"
|
Command_RemoteStreamFile = "remotestreamfile"
|
||||||
Command_RemoteFileInfo = "remotefileinfo"
|
Command_RemoteFileInfo = "remotefileinfo"
|
||||||
|
Command_RemoteWriteFile = "remotewritefile"
|
||||||
Command_Event = "event"
|
Command_Event = "event"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ type WshRpcInterface interface {
|
|||||||
// remotes
|
// remotes
|
||||||
RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData]
|
RemoteStreamFileCommand(ctx context.Context, data CommandRemoteStreamFileData) chan RespOrErrorUnion[CommandRemoteStreamFileRtnData]
|
||||||
RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error)
|
RemoteFileInfoCommand(ctx context.Context, path string) (*FileInfo, error)
|
||||||
|
RemoteWriteFileCommand(ctx context.Context, data CommandRemoteWriteFileData) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// for frontend
|
// for frontend
|
||||||
@ -278,3 +280,9 @@ type CommandRemoteStreamFileRtnData struct {
|
|||||||
FileInfo *FileInfo `json:"fileinfo,omitempty"`
|
FileInfo *FileInfo `json:"fileinfo,omitempty"`
|
||||||
Data64 string `json:"data64,omitempty"`
|
Data64 string `json:"data64,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommandRemoteWriteFileData struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Data64 string `json:"data64"`
|
||||||
|
CreateMode os.FileMode `json:"createmode,omitempty"`
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user