yao/tai/workspace/copy.go
Max 3f390c223c feat(sandbox/v2): enhance V2 sandbox integration and testing
- Implemented V2 sandbox initialization in the assistant loading process, allowing for standalone sandbox.yao configuration.
- Added support for V2 sandbox execution paths in the Assistant's Stream method, differentiating between V1 and V2 sandboxes.
- Introduced comprehensive tests for V2 sandbox configurations, ensuring correct loading and execution behavior.
- Updated the context and types to accommodate V2 sandbox features, including system information and workspace management.

Made-with: Cursor
2026-03-10 01:13:17 +08:00

158 lines
3.9 KiB
Go

package workspace
import (
"context"
"io/fs"
"os"
"path/filepath"
"github.com/yaoapp/yao/tai/volume"
)
// Copy implements the FS.Copy method with 4-way dispatch:
//
// ws -> ws : Volume.Copy (server-side for remote, local copy for local)
// host -> ws : Volume.SyncPush
// ws -> host : Volume.SyncPull
// host -> host : os-level recursive copy
func (w *workspaceFS) Copy(src, dst string, opts ...volume.SyncOption) (*volume.SyncResult, error) {
srcURI := parseHostURI(src)
dstURI := parseHostURI(dst)
ctx := context.Background()
switch {
case !srcURI.IsHost && !dstURI.IsHost:
return w.vol.Copy(ctx, w.session, srcURI.Path, dstURI.Path, opts...)
case srcURI.IsHost && !dstURI.IsHost:
hostPath, err := resolveAbsHostPath(srcURI)
if err != nil {
return nil, err
}
info, err := os.Stat(hostPath)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, w.pushSingleFile(ctx, hostPath, dstURI.Path, info)
}
pushOpts := append(sliceClone(opts), volume.WithRemotePath(dstURI.Path))
return w.vol.SyncPush(ctx, w.session, hostPath, pushOpts...)
case !srcURI.IsHost && dstURI.IsHost:
hostPath, err := resolveAbsHostPath(dstURI)
if err != nil {
return nil, err
}
srcInfo, statErr := w.vol.Stat(ctx, w.session, srcURI.Path)
if statErr != nil {
return nil, statErr
}
if !srcInfo.IsDir {
return nil, w.pullSingleFile(ctx, srcURI.Path, hostPath)
}
pullOpts := append(sliceClone(opts), volume.WithRemotePath(srcURI.Path))
return w.vol.SyncPull(ctx, w.session, hostPath, pullOpts...)
default:
srcPath, err := resolveAbsHostPath(srcURI)
if err != nil {
return nil, err
}
dstPath, err := resolveAbsHostPath(dstURI)
if err != nil {
return nil, err
}
cfg := volume.ApplySyncOpts(opts)
return nil, copyLocalToLocal(srcPath, dstPath, cfg.Excludes)
}
}
// pushSingleFile reads a host file and writes it into the workspace at dstPath.
func (w *workspaceFS) pushSingleFile(ctx context.Context, hostPath, dstPath string, info os.FileInfo) error {
data, err := os.ReadFile(hostPath)
if err != nil {
return err
}
dir := filepath.Dir(dstPath)
if dir != "" && dir != "." {
if err := w.vol.MkdirAll(ctx, w.session, dir); err != nil {
return err
}
}
perm := info.Mode()
if perm == 0 {
perm = 0o644
}
return w.vol.WriteFile(ctx, w.session, dstPath, data, perm)
}
// pullSingleFile reads a workspace file and writes it to hostPath.
func (w *workspaceFS) pullSingleFile(ctx context.Context, srcPath, hostPath string) error {
data, perm, err := w.vol.ReadFile(ctx, w.session, srcPath)
if err != nil {
return err
}
if perm == 0 {
perm = 0o644
}
if err := os.MkdirAll(filepath.Dir(hostPath), 0o755); err != nil {
return err
}
return os.WriteFile(hostPath, data, perm)
}
func copyLocalToLocal(src, dst string, excludes []string) error {
info, err := os.Stat(src)
if err != nil {
return err
}
if !info.IsDir() {
data, err := os.ReadFile(src)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
return os.WriteFile(dst, data, info.Mode())
}
return filepath.WalkDir(src, func(abs string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, _ := filepath.Rel(src, abs)
if rel == "." {
return os.MkdirAll(dst, 0o755)
}
for _, p := range excludes {
if matched, _ := filepath.Match(p, filepath.Base(rel)); matched {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
}
target := filepath.Join(dst, rel)
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
data, err := os.ReadFile(abs)
if err != nil {
return err
}
fi, _ := d.Info()
perm := os.FileMode(0o644)
if fi != nil {
perm = fi.Mode()
}
return os.WriteFile(target, data, perm)
})
}
func sliceClone(opts []volume.SyncOption) []volume.SyncOption {
cp := make([]volume.SyncOption, len(opts))
copy(cp, opts)
return cp
}