mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-20 16:19:37 +00:00
Refactors Agent Transfer into standalone /import-export page with 3 scope tabs. Teams: auto-bundle member agents (config, context files, memory, KG, cron, profiles, workspace). Agents: section picker with presets, merge mode support. Skills & MCP: custom skills + MCP server configs (stripped secrets), conflict detection. Security: scopeClauseAlias for JOINs, tenant scope on all queries, zip bomb protection, adminMiddleware on import routes. DRY: shared storeExportToken + readTarGzEntries helpers. i18n: complete en/vi/zh coverage.
134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
package http
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const maxWorkspaceFileSize = 50 << 20 // 50MB per file
|
|
|
|
// exportWorkspaceFiles walks wsPath and adds each file to tw under the "workspace/" prefix.
|
|
// Skips directories, hidden files (dot-prefixed), symlinks, and files exceeding maxWorkspaceFileSize.
|
|
func (h *AgentsHandler) exportWorkspaceFiles(ctx context.Context, tw *tar.Writer, wsPath string, progressFn func(ProgressEvent)) (int, int64, error) {
|
|
return h.exportWorkspaceFilesWithPrefix(ctx, tw, wsPath, "workspace/", progressFn)
|
|
}
|
|
|
|
// exportWorkspaceFilesWithPrefix walks wsPath and adds each file to tw under tarPrefix.
|
|
// Skips directories, hidden files (dot-prefixed), symlinks, and files exceeding maxWorkspaceFileSize.
|
|
func (h *AgentsHandler) exportWorkspaceFilesWithPrefix(ctx context.Context, tw *tar.Writer, wsPath, tarPrefix string, progressFn func(ProgressEvent)) (int, int64, error) {
|
|
info, err := os.Stat(wsPath)
|
|
if err != nil || !info.IsDir() {
|
|
return 0, 0, nil // no workspace dir = nothing to export
|
|
}
|
|
|
|
var count int
|
|
var totalBytes int64
|
|
|
|
walkErr := filepath.WalkDir(wsPath, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil // skip unreadable entries
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(d.Name(), ".") {
|
|
return nil
|
|
}
|
|
if d.Type()&fs.ModeSymlink != 0 {
|
|
return nil
|
|
}
|
|
|
|
fi, err := d.Info()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if fi.Size() > maxWorkspaceFileSize {
|
|
slog.Debug("export: skipping large workspace file", "path", path, "size", fi.Size())
|
|
return nil
|
|
}
|
|
|
|
rel, err := filepath.Rel(wsPath, path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
slog.Warn("export: failed to read workspace file", "path", path, "error", err)
|
|
return nil
|
|
}
|
|
|
|
tarPath := tarPrefix + sanitizeRelPath(filepath.ToSlash(rel))
|
|
if err := addToTar(tw, tarPath, data); err != nil {
|
|
return err
|
|
}
|
|
|
|
count++
|
|
totalBytes += fi.Size()
|
|
if progressFn != nil && count%10 == 0 {
|
|
progressFn(ProgressEvent{Phase: "workspace", Status: "running", Current: count})
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if progressFn != nil {
|
|
progressFn(ProgressEvent{Phase: "workspace", Status: "done", Current: count, Total: count})
|
|
}
|
|
return count, totalBytes, walkErr
|
|
}
|
|
|
|
// extractWorkspaceFiles writes files from the archive into baseDir.
|
|
// Security: rejects path traversal and absolute paths.
|
|
// When overwrite is false, existing files are skipped.
|
|
func extractWorkspaceFiles(baseDir string, files map[string][]byte, overwrite bool) (int, error) {
|
|
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
|
return 0, fmt.Errorf("create workspace dir: %w", err)
|
|
}
|
|
|
|
// Ensure baseDir ends with separator for prefix check
|
|
baseDirClean := filepath.Clean(baseDir) + string(filepath.Separator)
|
|
|
|
var count int
|
|
for rel, data := range files {
|
|
clean := filepath.Clean(rel)
|
|
if strings.Contains(clean, "..") {
|
|
continue
|
|
}
|
|
if filepath.IsAbs(clean) {
|
|
continue
|
|
}
|
|
|
|
target := filepath.Join(baseDir, clean)
|
|
// Verify target stays inside baseDir after path resolution
|
|
if !strings.HasPrefix(filepath.Clean(target)+string(filepath.Separator), baseDirClean) {
|
|
continue
|
|
}
|
|
|
|
if !overwrite {
|
|
if _, err := os.Stat(target); err == nil {
|
|
continue // skip existing
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
|
continue
|
|
}
|
|
|
|
if err := os.WriteFile(target, data, 0644); err != nil {
|
|
return count, fmt.Errorf("write %s: %w", rel, err)
|
|
}
|
|
count++
|
|
}
|
|
return count, nil
|
|
}
|