file browser 2.0
This commit is contained in:
@@ -2056,6 +2056,35 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
path, _ := payload["path"].(string)
|
||||
return HandleFileExecute(ctx, env, cmdID, path)
|
||||
case "file_icon":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
items := parseIconRequestItems(payload["items"])
|
||||
goSafe("file_icon", env.Cancel, func() {
|
||||
if err := HandleFileIcon(ctx, env, cmdID, items); err != nil && err != context.Canceled {
|
||||
log.Printf("file_icon error: %v", err)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
case "file_thumb":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
items := parseThumbRequestItems(payload["items"])
|
||||
goSafe("file_thumb", env.Cancel, func() {
|
||||
if err := HandleFileThumbnail(ctx, env, cmdID, items); err != nil && err != context.Canceled {
|
||||
log.Printf("file_thumb error: %v", err)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
case "file_dirsize":
|
||||
path, _ := envelopePayloadString(envelope, "path")
|
||||
dirsizeCtx, cancel := context.WithCancel(ctx)
|
||||
registerCancellableCommand(cmdID, cancel)
|
||||
goSafe("file_dirsize", env.Cancel, func() {
|
||||
defer unregisterCommand(cmdID)
|
||||
if err := HandleFolderSize(dirsizeCtx, env, cmdID, path); err != nil && err != context.Canceled {
|
||||
log.Printf("file_dirsize error: %v", err)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
case "agent_update":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
path, _ := payload["path"].(string)
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
agentRuntime "overlord-client/cmd/agent/runtime"
|
||||
"overlord-client/cmd/agent/wire"
|
||||
)
|
||||
|
||||
func parseIconRequestItems(raw interface{}) []wire.FileIconRequestItem {
|
||||
arr, _ := raw.([]interface{})
|
||||
out := make([]wire.FileIconRequestItem, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
m := coerceStringMap(v)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
key, _ := m["key"].(string)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
path, _ := m["path"].(string)
|
||||
ext, _ := m["ext"].(string)
|
||||
out = append(out, wire.FileIconRequestItem{Key: key, Path: path, Ext: ext})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseThumbRequestItems(raw interface{}) []wire.FileThumbnailRequestItem {
|
||||
arr, _ := raw.([]interface{})
|
||||
out := make([]wire.FileThumbnailRequestItem, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
m := coerceStringMap(v)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
key, _ := m["key"].(string)
|
||||
path, _ := m["path"].(string)
|
||||
if key == "" || path == "" {
|
||||
continue
|
||||
}
|
||||
size := 0
|
||||
switch s := m["size"].(type) {
|
||||
case float64:
|
||||
size = int(s)
|
||||
case int64:
|
||||
size = int(s)
|
||||
case uint64:
|
||||
size = int(s)
|
||||
case int:
|
||||
size = s
|
||||
}
|
||||
out = append(out, wire.FileThumbnailRequestItem{Key: key, Path: path, Size: size})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func coerceStringMap(v interface{}) map[string]interface{} {
|
||||
if m, ok := v.(map[string]interface{}); ok {
|
||||
return m
|
||||
}
|
||||
if m, ok := v.(map[interface{}]interface{}); ok {
|
||||
out := make(map[string]interface{}, len(m))
|
||||
for k, val := range m {
|
||||
if ks, ok := k.(string); ok {
|
||||
out[ks] = val
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleFileIcon(ctx context.Context, env *agentRuntime.Env, cmdID string, items []wire.FileIconRequestItem) error {
|
||||
out := make([]wire.FileIconResultItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
png, err := extractFileIconPNG(item.Path, item.Ext)
|
||||
ri := wire.FileIconResultItem{Key: item.Key}
|
||||
if err != nil {
|
||||
ri.Error = err.Error()
|
||||
} else {
|
||||
ri.PNG = png
|
||||
}
|
||||
out = append(out, ri)
|
||||
}
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.FileIconResult{
|
||||
Type: "file_icon_result",
|
||||
CommandID: cmdID,
|
||||
Icons: out,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleFileThumbnail(ctx context.Context, env *agentRuntime.Env, cmdID string, items []wire.FileThumbnailRequestItem) error {
|
||||
out := make([]wire.FileThumbnailResultItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
jpegBytes, w, h, err := extractFileThumbnailJPEG(item.Path, item.Size)
|
||||
ri := wire.FileThumbnailResultItem{Key: item.Key}
|
||||
if err != nil {
|
||||
ri.Error = err.Error()
|
||||
} else {
|
||||
ri.JPEG = jpegBytes
|
||||
ri.W = w
|
||||
ri.H = h
|
||||
}
|
||||
out = append(out, ri)
|
||||
}
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.FileThumbnailResult{
|
||||
Type: "file_thumb_result",
|
||||
CommandID: cmdID,
|
||||
Thumbs: out,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleFolderSize(ctx context.Context, env *agentRuntime.Env, cmdID string, path string) error {
|
||||
log.Printf("file_dirsize: %s", path)
|
||||
|
||||
var bytes, files, dirs int64
|
||||
lastEmit := time.Now()
|
||||
|
||||
emit := func(done bool, errMsg string) error {
|
||||
return wire.WriteMsg(ctx, env.Conn, wire.FolderSizeResult{
|
||||
Type: "file_dirsize_result",
|
||||
CommandID: cmdID,
|
||||
Path: path,
|
||||
Bytes: bytes,
|
||||
Files: files,
|
||||
Dirs: dirs,
|
||||
Done: done,
|
||||
Error: errMsg,
|
||||
})
|
||||
}
|
||||
|
||||
walkErr := filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // skip unreadable entries
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
if info.IsDir() {
|
||||
dirs++
|
||||
} else {
|
||||
files++
|
||||
bytes += info.Size()
|
||||
}
|
||||
if time.Since(lastEmit) >= 400*time.Millisecond {
|
||||
lastEmit = time.Now()
|
||||
_ = emit(false, "")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
errMsg := ""
|
||||
if walkErr != nil && walkErr != context.Canceled {
|
||||
errMsg = walkErr.Error()
|
||||
}
|
||||
return emit(true, errMsg)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
|
||||
package handlers
|
||||
|
||||
import "fmt"
|
||||
|
||||
func extractFileIconPNG(_, _ string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("file icons only supported on Windows")
|
||||
}
|
||||
|
||||
func extractFileThumbnailJPEG(_ string, _ int) ([]byte, int, int, error) {
|
||||
return nil, 0, 0, fmt.Errorf("file thumbnails only supported on Windows")
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
feShell32 = windows.NewLazySystemDLL("shell32.dll")
|
||||
feOle32 = windows.NewLazySystemDLL("ole32.dll")
|
||||
pSHGetFileInfoW = feShell32.NewProc("SHGetFileInfoW")
|
||||
pSHCreateItemFromParsingName = feShell32.NewProc("SHCreateItemFromParsingName")
|
||||
pCoInitializeEx = feOle32.NewProc("CoInitializeEx")
|
||||
pCoUninitialize = feOle32.NewProc("CoUninitialize")
|
||||
)
|
||||
|
||||
const (
|
||||
shgfiIcon = 0x000000100
|
||||
shgfiSmallIcon = 0x000000001
|
||||
shgfiLargeIcon = 0x000000000
|
||||
shgfiUseFileAttributes = 0x000000010
|
||||
fileAttributeNormal = 0x00000080
|
||||
coinitApartmentThreaded = 0x2
|
||||
coinitDisableOLE1DDE = 0x4
|
||||
|
||||
siigbfResizeToFit = 0x00
|
||||
siigbfBiggerSizeOk = 0x01
|
||||
siigbfMemoryOnly = 0x02
|
||||
siigbfIconOnly = 0x04
|
||||
siigbfThumbnailOnly = 0x08
|
||||
siigbfInCacheOnly = 0x10
|
||||
)
|
||||
|
||||
type shFileInfoW struct {
|
||||
HIcon uintptr
|
||||
IIcon int32
|
||||
DwAttributes uint32
|
||||
SzDisplayName [260]uint16
|
||||
SzTypeName [80]uint16
|
||||
}
|
||||
|
||||
var iidIShellItemImageFactory = windows.GUID{
|
||||
Data1: 0xbcc18b79, Data2: 0xba16, Data3: 0x442f,
|
||||
Data4: [8]byte{0x80, 0xc4, 0x8a, 0x59, 0xc3, 0x0c, 0x46, 0x3b},
|
||||
}
|
||||
|
||||
type iShellItemImageFactoryVtbl struct {
|
||||
QueryInterface uintptr
|
||||
AddRef uintptr
|
||||
Release uintptr
|
||||
GetImage uintptr
|
||||
}
|
||||
|
||||
type iShellItemImageFactory struct {
|
||||
Vtbl *iShellItemImageFactoryVtbl
|
||||
}
|
||||
|
||||
type comJob struct {
|
||||
fn func()
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
var (
|
||||
comStartOnce sync.Once
|
||||
comQueue chan comJob
|
||||
)
|
||||
|
||||
func runOnCOMThread(fn func()) {
|
||||
comStartOnce.Do(func() {
|
||||
comQueue = make(chan comJob, 256)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
pCoInitializeEx.Call(0, uintptr(coinitApartmentThreaded|coinitDisableOLE1DDE))
|
||||
defer pCoUninitialize.Call()
|
||||
for job := range comQueue {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("com worker: recovered panic: %v", r)
|
||||
}
|
||||
}()
|
||||
job.fn()
|
||||
}()
|
||||
close(job.done)
|
||||
}
|
||||
}()
|
||||
})
|
||||
done := make(chan struct{})
|
||||
comQueue <- comJob{fn: fn, done: done}
|
||||
<-done
|
||||
}
|
||||
|
||||
func hBitmapToRGBA(hBitmap uintptr) *image.RGBA {
|
||||
var bm bitmapObj
|
||||
pGetObjectW.Call(hBitmap, unsafe.Sizeof(bm), uintptr(unsafe.Pointer(&bm)))
|
||||
if bm.BmWidth <= 0 || bm.BmHeight <= 0 {
|
||||
return nil
|
||||
}
|
||||
w := int(bm.BmWidth)
|
||||
h := int(bm.BmHeight)
|
||||
|
||||
hdc, _, _ := pCreateCompatibleDC.Call(0)
|
||||
if hdc == 0 {
|
||||
return nil
|
||||
}
|
||||
defer pDeleteDC.Call(hdc)
|
||||
|
||||
bih := bitmapInfoHeader{
|
||||
BiSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
||||
BiWidth: int32(w),
|
||||
BiHeight: -int32(h),
|
||||
BiPlanes: 1,
|
||||
BiBitCount: 32,
|
||||
}
|
||||
pixels := make([]byte, w*h*4)
|
||||
pGetDIBits.Call(hdc, hBitmap, 0, uintptr(h),
|
||||
uintptr(unsafe.Pointer(&pixels[0])),
|
||||
uintptr(unsafe.Pointer(&bih)),
|
||||
0,
|
||||
)
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
off := (y*w + x) * 4
|
||||
img.SetRGBA(x, y, color.RGBA{
|
||||
R: pixels[off+2],
|
||||
G: pixels[off+1],
|
||||
B: pixels[off+0],
|
||||
A: pixels[off+3],
|
||||
})
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func extractFileIconPNG(targetPath, ext string) ([]byte, error) {
|
||||
var queryPath string
|
||||
flags := uint32(shgfiIcon | shgfiLargeIcon)
|
||||
if targetPath != "" {
|
||||
queryPath = targetPath
|
||||
} else if ext != "" {
|
||||
queryPath = "x." + strings.TrimPrefix(strings.ToLower(ext), ".")
|
||||
flags |= shgfiUseFileAttributes
|
||||
} else {
|
||||
return nil, fmt.Errorf("path or ext required")
|
||||
}
|
||||
|
||||
pathPtr, err := windows.UTF16PtrFromString(queryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []byte
|
||||
var outErr error
|
||||
runOnCOMThread(func() {
|
||||
var sfi shFileInfoW
|
||||
ret, _, _ := pSHGetFileInfoW.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
uintptr(fileAttributeNormal),
|
||||
uintptr(unsafe.Pointer(&sfi)),
|
||||
unsafe.Sizeof(sfi),
|
||||
uintptr(flags),
|
||||
)
|
||||
if ret == 0 || sfi.HIcon == 0 {
|
||||
outErr = fmt.Errorf("SHGetFileInfo failed")
|
||||
return
|
||||
}
|
||||
defer pDestroyIcon.Call(sfi.HIcon)
|
||||
|
||||
var ii iconInfo
|
||||
r, _, _ := pGetIconInfo.Call(sfi.HIcon, uintptr(unsafe.Pointer(&ii)))
|
||||
if r == 0 {
|
||||
outErr = fmt.Errorf("GetIconInfo failed")
|
||||
return
|
||||
}
|
||||
if ii.HbmMask != 0 {
|
||||
defer pDeleteObject.Call(uintptr(ii.HbmMask))
|
||||
}
|
||||
if ii.HbmColor == 0 {
|
||||
outErr = fmt.Errorf("no color bitmap")
|
||||
return
|
||||
}
|
||||
defer pDeleteObject.Call(uintptr(ii.HbmColor))
|
||||
|
||||
img := hBitmapToRGBA(uintptr(ii.HbmColor))
|
||||
if img == nil {
|
||||
outErr = fmt.Errorf("bitmap conversion failed")
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
outErr = err
|
||||
return
|
||||
}
|
||||
out = buf.Bytes()
|
||||
})
|
||||
return out, outErr
|
||||
}
|
||||
|
||||
func extractFileThumbnailJPEG(path string, edge int) ([]byte, int, int, error) {
|
||||
if edge <= 0 {
|
||||
edge = 128
|
||||
}
|
||||
pathPtr, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
var out []byte
|
||||
var outW, outH int
|
||||
var outErr error
|
||||
runOnCOMThread(func() {
|
||||
var itemPtr uintptr
|
||||
hr, _, _ := pSHCreateItemFromParsingName.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&iidIShellItemImageFactory)),
|
||||
uintptr(unsafe.Pointer(&itemPtr)),
|
||||
)
|
||||
if hr != 0 || itemPtr == 0 {
|
||||
outErr = fmt.Errorf("SHCreateItemFromParsingName failed: 0x%x", hr)
|
||||
return
|
||||
}
|
||||
factory := (*iShellItemImageFactory)(unsafe.Pointer(itemPtr))
|
||||
defer callRelease(factory.Vtbl.Release, itemPtr)
|
||||
|
||||
flags := uint32(siigbfThumbnailOnly | siigbfBiggerSizeOk)
|
||||
var hBitmap uintptr
|
||||
hr2 := callGetImage(factory.Vtbl.GetImage, itemPtr, int32(edge), int32(edge), flags, &hBitmap)
|
||||
if hr2 != 0 || hBitmap == 0 {
|
||||
outErr = fmt.Errorf("GetImage failed: 0x%x", hr2)
|
||||
return
|
||||
}
|
||||
defer pDeleteObject.Call(hBitmap)
|
||||
|
||||
img := hBitmapToRGBA(hBitmap)
|
||||
if img == nil {
|
||||
outErr = fmt.Errorf("bitmap conversion failed")
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80}); err != nil {
|
||||
outErr = err
|
||||
return
|
||||
}
|
||||
out = buf.Bytes()
|
||||
outW = img.Bounds().Dx()
|
||||
outH = img.Bounds().Dy()
|
||||
})
|
||||
return out, outW, outH, outErr
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build windows && 386
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func callGetImage(getImageFn, this uintptr, cx, cy int32, flags uint32, hBitmapOut *uintptr) uintptr {
|
||||
hr, _, _ := syscall.SyscallN(
|
||||
getImageFn,
|
||||
this,
|
||||
uintptr(uint32(cx)),
|
||||
uintptr(uint32(cy)),
|
||||
uintptr(flags),
|
||||
uintptr(unsafe.Pointer(hBitmapOut)),
|
||||
)
|
||||
return hr
|
||||
}
|
||||
|
||||
func callRelease(releaseFn, this uintptr) {
|
||||
syscall.SyscallN(releaseFn, this)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build windows && (amd64 || arm64)
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func callGetImage(getImageFn, this uintptr, cx, cy int32, flags uint32, hBitmapOut *uintptr) uintptr {
|
||||
sizeWord := uintptr(uint64(uint32(cx)) | (uint64(uint32(cy)) << 32))
|
||||
hr, _, _ := syscall.SyscallN(
|
||||
getImageFn,
|
||||
this,
|
||||
sizeWord,
|
||||
uintptr(flags),
|
||||
uintptr(unsafe.Pointer(hBitmapOut)),
|
||||
)
|
||||
return hr
|
||||
}
|
||||
|
||||
func callRelease(releaseFn, this uintptr) {
|
||||
syscall.SyscallN(releaseFn, this)
|
||||
}
|
||||
@@ -7,12 +7,15 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"overlord-client/cmd/agent/wire"
|
||||
)
|
||||
|
||||
func enrichFileEntry(entry *wire.FileEntry, info os.FileInfo) {
|
||||
|
||||
if d, ok := info.Sys().(*syscall.Win32FileAttributeData); ok {
|
||||
entry.Attrs = d.FileAttributes
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeFilePermissions(path string, mode string) error {
|
||||
|
||||
@@ -110,6 +110,8 @@ type FileEntry struct {
|
||||
Mode string `msgpack:"mode,omitempty"`
|
||||
Owner string `msgpack:"owner,omitempty"`
|
||||
Group string `msgpack:"group,omitempty"`
|
||||
// windows file attributes
|
||||
Attrs uint32 `msgpack:"attrs,omitempty"`
|
||||
}
|
||||
|
||||
type FileListResult struct {
|
||||
@@ -187,6 +189,55 @@ type FileSearchResult struct {
|
||||
Error string `msgpack:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FileIconRequestItem struct {
|
||||
Key string `msgpack:"key"`
|
||||
Path string `msgpack:"path,omitempty"`
|
||||
Ext string `msgpack:"ext,omitempty"`
|
||||
}
|
||||
|
||||
type FileIconResultItem struct {
|
||||
Key string `msgpack:"key"`
|
||||
PNG []byte `msgpack:"png,omitempty"`
|
||||
Error string `msgpack:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FileIconResult struct {
|
||||
Type string `msgpack:"type"`
|
||||
CommandID string `msgpack:"commandId,omitempty"`
|
||||
Icons []FileIconResultItem `msgpack:"icons"`
|
||||
}
|
||||
|
||||
type FileThumbnailRequestItem struct {
|
||||
Key string `msgpack:"key"`
|
||||
Path string `msgpack:"path"`
|
||||
Size int `msgpack:"size,omitempty"`
|
||||
}
|
||||
|
||||
type FileThumbnailResultItem struct {
|
||||
Key string `msgpack:"key"`
|
||||
JPEG []byte `msgpack:"jpeg,omitempty"`
|
||||
W int `msgpack:"w,omitempty"`
|
||||
H int `msgpack:"h,omitempty"`
|
||||
Error string `msgpack:"error,omitempty"`
|
||||
}
|
||||
|
||||
type FileThumbnailResult struct {
|
||||
Type string `msgpack:"type"`
|
||||
CommandID string `msgpack:"commandId,omitempty"`
|
||||
Thumbs []FileThumbnailResultItem `msgpack:"thumbs"`
|
||||
}
|
||||
|
||||
type FolderSizeResult struct {
|
||||
Type string `msgpack:"type"`
|
||||
CommandID string `msgpack:"commandId,omitempty"`
|
||||
Path string `msgpack:"path"`
|
||||
Bytes int64 `msgpack:"bytes"`
|
||||
Files int64 `msgpack:"files"`
|
||||
Dirs int64 `msgpack:"dirs"`
|
||||
Done bool `msgpack:"done"`
|
||||
Error string `msgpack:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ScriptResult struct {
|
||||
Type string `msgpack:"type"`
|
||||
CommandID string `msgpack:"commandId,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user