feat(workspace): add root directory retrieval for workspaces
- Implemented a new endpoint to retrieve the absolute path of a workspace's root directory. - Enhanced the workspace interface with a GetRoot method to facilitate this functionality. - Updated the workspace manager to utilize the new method for improved path resolution. Made-with: Cursor
This commit is contained in:
parent
59004de3e8
commit
82bb44cbda
24 changed files with 834 additions and 211 deletions
|
|
@ -204,6 +204,15 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
|||
}
|
||||
}()
|
||||
|
||||
// ================================================
|
||||
// Standalone Workspace Loading (no sandbox required)
|
||||
// ================================================
|
||||
// When no sandbox is configured but the user selected a workspace,
|
||||
// load the workspace FS into context so hooks can access ctx.workspace.
|
||||
if !ctx.HasWorkspace() {
|
||||
ast.initStandaloneWorkspace(ctx)
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// Execute Create Hook
|
||||
// ================================================
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package assistant
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/yaoapp/yao/config"
|
||||
infraV2 "github.com/yaoapp/yao/sandbox/v2"
|
||||
traceTypes "github.com/yaoapp/yao/trace/types"
|
||||
"github.com/yaoapp/yao/workspace"
|
||||
)
|
||||
|
||||
// HasSandboxV2 returns true if the assistant has a V2 sandbox configuration.
|
||||
|
|
@ -167,6 +169,26 @@ func (ast *Assistant) executeSandboxV2Stream(
|
|||
return sandboxv2.ExecuteSandboxStream(ctx, execReq, streamHandler)
|
||||
}
|
||||
|
||||
// initStandaloneWorkspace loads the workspace FS into context when no sandbox
|
||||
// is configured but the user selected a workspace (metadata["workspace_id"]).
|
||||
func (ast *Assistant) initStandaloneWorkspace(ctx *context.Context) {
|
||||
if ctx.Metadata == nil {
|
||||
return
|
||||
}
|
||||
wsID, _ := ctx.Metadata["workspace_id"].(string)
|
||||
if wsID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
stdCtx := ctx.Context
|
||||
wsFS, err := workspace.M().FS(stdCtx, wsID)
|
||||
if err != nil {
|
||||
log.Printf("[assistant] initStandaloneWorkspace: failed to load workspace %s: %v", wsID, err)
|
||||
return
|
||||
}
|
||||
ctx.SetWorkspace(wsFS)
|
||||
}
|
||||
|
||||
func closeLoadingV2(ctx *context.Context, loadingMsgID, msgKey string) {
|
||||
if loadingMsgID == "" || ctx == nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ func (ctx *Context) SetComputer(computer infraV2.Computer) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetWorkspace sets the workspace FS directly without requiring a Computer.
|
||||
// Use this when the user selected a workspace but no sandbox is configured.
|
||||
func (ctx *Context) SetWorkspace(ws workspace.FS) {
|
||||
ctx.workspace = ws
|
||||
}
|
||||
|
||||
// GetComputer returns the V2 computer if available.
|
||||
func (ctx *Context) GetComputer() infraV2.Computer {
|
||||
return ctx.computer
|
||||
|
|
@ -35,6 +41,11 @@ func (ctx *Context) HasComputer() bool {
|
|||
return ctx.computer != nil
|
||||
}
|
||||
|
||||
// HasWorkspace returns true if workspace FS is available.
|
||||
func (ctx *Context) HasWorkspace() bool {
|
||||
return ctx.workspace != nil
|
||||
}
|
||||
|
||||
// createComputerInstance creates the ctx.computer JavaScript object.
|
||||
func (ctx *Context) createComputerInstance(v8ctx *v8go.Context) *v8go.Value {
|
||||
if ctx.computer == nil {
|
||||
|
|
|
|||
164
agent/sandbox/v2/claude/oscompat.go
Normal file
164
agent/sandbox/v2/claude/oscompat.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package claude
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
||||
infra "github.com/yaoapp/yao/sandbox/v2"
|
||||
)
|
||||
|
||||
// osEnv captures OS-dependent paths and shell settings derived from the
|
||||
// Computer's SystemInfo. All runner code should use osEnv instead of
|
||||
// hardcoded Linux constants.
|
||||
type osEnv struct {
|
||||
OS string // "windows", "linux", "darwin", ...
|
||||
Shell string // preferred shell binary: "bash", "pwsh", "cmd.exe", ...
|
||||
WorkDir string // working directory on the target machine
|
||||
UserHome string // user home directory (empty if irrelevant)
|
||||
TempDir string // system temp directory
|
||||
}
|
||||
|
||||
func (e *osEnv) isWindows() bool {
|
||||
return strings.EqualFold(e.OS, "windows")
|
||||
}
|
||||
|
||||
// resolveOSEnv builds an osEnv from the Computer's reported SystemInfo,
|
||||
// falling back to SandboxConfig values where available, then to per-OS defaults.
|
||||
func resolveOSEnv(computer infra.Computer, _ *types.SandboxConfig) *osEnv {
|
||||
sys := computer.ComputerInfo().System
|
||||
|
||||
env := &osEnv{
|
||||
OS: strings.ToLower(sys.OS),
|
||||
Shell: sys.Shell,
|
||||
TempDir: sys.TempDir,
|
||||
WorkDir: computer.GetWorkDir(),
|
||||
}
|
||||
|
||||
if env.TempDir == "" {
|
||||
env.TempDir = env.pathJoin(env.WorkDir, ".tmp")
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// shellCmd returns the command slice to run a script through the appropriate shell.
|
||||
func (e *osEnv) shellCmd(script string) []string {
|
||||
shell := strings.ToLower(e.Shell)
|
||||
switch shell {
|
||||
case "pwsh":
|
||||
return []string{"pwsh", "-NoProfile", "-Command", script}
|
||||
case "powershell":
|
||||
return []string{"powershell", "-NoProfile", "-Command", script}
|
||||
case "cmd.exe", "cmd":
|
||||
return []string{"cmd.exe", "/C", script}
|
||||
default:
|
||||
return []string{"bash", "-c", script}
|
||||
}
|
||||
}
|
||||
|
||||
// mkdirCmd returns a shell command string to create a directory (with parents).
|
||||
func (e *osEnv) mkdirCmd(dir string) string {
|
||||
if e.isWindows() {
|
||||
return fmt.Sprintf(`if (!(Test-Path '%s')) { New-Item -ItemType Directory -Path '%s' -Force | Out-Null }`, dir, dir)
|
||||
}
|
||||
return fmt.Sprintf("mkdir -p %s", dir)
|
||||
}
|
||||
|
||||
// listDirCmd returns a command slice to list directory contents.
|
||||
func (e *osEnv) listDirCmd(dir string) []string {
|
||||
if e.isWindows() {
|
||||
return e.shellCmd(fmt.Sprintf("Get-ChildItem -Name '%s'", dir))
|
||||
}
|
||||
return []string{"ls", dir}
|
||||
}
|
||||
|
||||
// killProcessCmd returns a command slice to kill processes matching a pattern.
|
||||
func (e *osEnv) killProcessCmd(pattern string) []string {
|
||||
if e.isWindows() {
|
||||
script := fmt.Sprintf("Get-Process | Where-Object {$_.ProcessName -like '*%s*'} | Stop-Process -Force -ErrorAction SilentlyContinue", pattern)
|
||||
return e.shellCmd(script)
|
||||
}
|
||||
return []string{"sh", "-c", fmt.Sprintf("pkill -f '%s' || true", pattern)}
|
||||
}
|
||||
|
||||
// rootDir returns the filesystem root for the target OS.
|
||||
func (e *osEnv) rootDir() string {
|
||||
if e.isWindows() {
|
||||
return `C:\`
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
// pathJoin joins path segments using the appropriate separator.
|
||||
func (e *osEnv) pathJoin(parts ...string) string {
|
||||
if e.isWindows() {
|
||||
return strings.Join(parts, `\`)
|
||||
}
|
||||
return path.Join(parts...)
|
||||
}
|
||||
|
||||
// buildCLIScript builds the complete CLI invocation script for the target OS.
|
||||
// Returns (script, stdin) — on Linux stdin is nil (heredoc handles it),
|
||||
// on Windows stdin contains inputJSONL bytes to pass via gRPC Stdin.
|
||||
func (e *osEnv) buildCLIScript(args []string, systemPrompt, inputJSONL string) (string, []byte) {
|
||||
workDir := e.WorkDir
|
||||
promptFile := e.pathJoin(workDir, ".yao", ".system-prompt.txt")
|
||||
|
||||
if e.isWindows() {
|
||||
return e.buildPowerShellScript(args, systemPrompt, inputJSONL, workDir, promptFile)
|
||||
}
|
||||
return e.buildBashScript(args, systemPrompt, inputJSONL, workDir, promptFile), nil
|
||||
}
|
||||
|
||||
func (e *osEnv) buildBashScript(args []string, systemPrompt, inputJSONL, workDir, promptFile string) string {
|
||||
var b strings.Builder
|
||||
|
||||
if e.UserHome != "" {
|
||||
b.WriteString(fmt.Sprintf("touch %s/.Xauthority 2>/dev/null; ", e.UserHome))
|
||||
}
|
||||
b.WriteString("touch \"$HOME/.Xauthority\" 2>/dev/null\n")
|
||||
|
||||
if systemPrompt != "" {
|
||||
b.WriteString(fmt.Sprintf("mkdir -p %s/.yao\n", workDir))
|
||||
b.WriteString(fmt.Sprintf("cat << 'PROMPTEOF' > %s\n", promptFile))
|
||||
b.WriteString(systemPrompt)
|
||||
b.WriteString("\nPROMPTEOF\n")
|
||||
args = append(args, "--append-system-prompt-file", promptFile)
|
||||
}
|
||||
|
||||
b.WriteString("cat << 'INPUTEOF' | claude -p")
|
||||
for _, arg := range args {
|
||||
b.WriteString(fmt.Sprintf(" %q", arg))
|
||||
}
|
||||
b.WriteString(" 2>&1\n")
|
||||
b.WriteString(inputJSONL)
|
||||
b.WriteString("\nINPUTEOF")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// buildPowerShellScript builds a script that writes the system prompt file,
|
||||
// then launches claude -p. inputJSONL is returned as stdin bytes to be passed
|
||||
// directly via gRPC, bypassing PowerShell's encoding entirely.
|
||||
func (e *osEnv) buildPowerShellScript(args []string, systemPrompt, inputJSONL, workDir, promptFile string) (string, []byte) {
|
||||
var b strings.Builder
|
||||
noBOM := "(New-Object System.Text.UTF8Encoding $false)"
|
||||
|
||||
yaoDir := e.pathJoin(workDir, ".yao")
|
||||
b.WriteString(fmt.Sprintf("if (!(Test-Path '%s')) { New-Item -ItemType Directory -Path '%s' -Force | Out-Null }\n", yaoDir, yaoDir))
|
||||
|
||||
if systemPrompt != "" {
|
||||
escaped := strings.ReplaceAll(systemPrompt, "'", "''")
|
||||
b.WriteString(fmt.Sprintf("[IO.File]::WriteAllText('%s', @'\n%s\n'@, %s)\n", promptFile, escaped, noBOM))
|
||||
args = append(args, "--append-system-prompt-file", promptFile)
|
||||
}
|
||||
|
||||
b.WriteString("claude -p")
|
||||
for _, arg := range args {
|
||||
b.WriteString(fmt.Sprintf(" '%s'", strings.ReplaceAll(arg, "'", "''")))
|
||||
}
|
||||
|
||||
return b.String(), []byte(inputJSONL + "\n")
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -14,12 +15,7 @@ import (
|
|||
infra "github.com/yaoapp/yao/sandbox/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWorkDir = "/workspace"
|
||||
defaultUser = "sandbox"
|
||||
defaultUserHome = "/home/sandbox"
|
||||
defaultProxyPort = 3456
|
||||
)
|
||||
const defaultProxyPort = 3456
|
||||
|
||||
// ClaudeRunner implements the Runner interface for Claude CLI (mode=cli).
|
||||
type ClaudeRunner struct {
|
||||
|
|
@ -45,33 +41,30 @@ func (r *ClaudeRunner) Prepare(ctx context.Context, req *types.PrepareRequest) e
|
|||
r.mode = "cli"
|
||||
}
|
||||
|
||||
workDir := resolveWorkDir(req.Config)
|
||||
env := resolveOSEnv(req.Computer, req.Config)
|
||||
|
||||
// Merge user-defined steps with runner-specific steps.
|
||||
steps := append([]types.PrepareStep{}, req.Config.Prepare...)
|
||||
|
||||
// Runner-specific: ensure .claude directory in workDir.
|
||||
if req.SkillsDir != "" {
|
||||
claudeDir := env.pathJoin(env.WorkDir, ".claude")
|
||||
steps = append(steps, types.PrepareStep{
|
||||
Action: "exec",
|
||||
Cmd: fmt.Sprintf("mkdir -p %s/.claude", workDir),
|
||||
Cmd: env.mkdirCmd(claudeDir),
|
||||
Once: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Runner-specific: write MCP config.
|
||||
if len(req.MCPServers) > 0 {
|
||||
r.hasMCP = true
|
||||
r.mcpToolPattern = buildMCPAllowedTools(req.MCPServers)
|
||||
mcpJSON := buildMCPConfig(req.MCPServers)
|
||||
steps = append(steps, types.PrepareStep{
|
||||
Action: "file",
|
||||
Path: path.Join(workDir, ".mcp.json"),
|
||||
Path: env.pathJoin(env.WorkDir, ".mcp.json"),
|
||||
Content: mcpJSON,
|
||||
})
|
||||
}
|
||||
|
||||
// Execute all steps via the injected callback.
|
||||
if req.RunSteps != nil && len(steps) > 0 {
|
||||
if err := req.RunSteps(ctx, steps, req.Computer, req.Config.ID, req.ConfigHash); err != nil {
|
||||
return fmt.Errorf("claude prepare steps: %w", err)
|
||||
|
|
@ -88,9 +81,8 @@ func (r *ClaudeRunner) Stream(ctx context.Context, req *types.StreamRequest, han
|
|||
return fmt.Errorf("computer is nil")
|
||||
}
|
||||
|
||||
workDir := resolveWorkDir(req.Config)
|
||||
oe := resolveOSEnv(computer, req.Config)
|
||||
|
||||
// Prepare attachments: resolve __yao.attachment:// URLs, copy files to workspace.
|
||||
if req.ChatID != "" {
|
||||
ws := computer.Workplace()
|
||||
if ws != nil {
|
||||
|
|
@ -102,90 +94,118 @@ func (r *ClaudeRunner) Stream(ctx context.Context, req *types.StreamRequest, han
|
|||
}
|
||||
}
|
||||
|
||||
// Detect continuation (existing .claude/projects/ directory).
|
||||
isContinuation := hasExistingSession(ctx, computer, workDir)
|
||||
isContinuation := hasExistingSession(ctx, computer, oe)
|
||||
|
||||
// Build CLI command and env.
|
||||
cmd, env := r.buildCLICommand(req, isContinuation)
|
||||
cmd, env, stdin := r.buildCLICommand(req, oe, isContinuation)
|
||||
|
||||
// Create stream.
|
||||
execStream, err := computer.Stream(ctx, cmd, infra.WithWorkDir(workDir), infra.WithEnv(env))
|
||||
streamOpts := []infra.ExecOption{infra.WithWorkDir(oe.WorkDir), infra.WithEnv(env)}
|
||||
if len(stdin) > 0 {
|
||||
streamOpts = append(streamOpts, infra.WithStdin(stdin))
|
||||
}
|
||||
|
||||
execStream, err := computer.Stream(ctx, cmd, streamOpts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("computer.Stream: %w", err)
|
||||
}
|
||||
|
||||
// Monitor for context cancellation — kill the process.
|
||||
done := make(chan struct{})
|
||||
defer func() {
|
||||
close(done)
|
||||
}()
|
||||
streamCtx, streamCancel := context.WithCancel(ctx)
|
||||
defer streamCancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
killCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
computer.Exec(killCtx, []string{"pkill", "-f", "claude"})
|
||||
execStream.Cancel()
|
||||
case <-done:
|
||||
<-streamCtx.Done()
|
||||
killCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
computer.Exec(killCtx, oe.killProcessCmd("claude"))
|
||||
execStream.Cancel()
|
||||
}()
|
||||
|
||||
var stderrBuf strings.Builder
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := execStream.Stderr.Read(buf)
|
||||
if n > 0 {
|
||||
stderrBuf.Write(buf[:n])
|
||||
chunk := string(buf[:n])
|
||||
if strings.Contains(strings.ToLower(chunk), "error") {
|
||||
streamCancel()
|
||||
io.Copy(&stderrBuf, execStream.Stderr)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Parse streaming output.
|
||||
parseErr := parseStreamJSON(ctx, execStream.Stdout, handler)
|
||||
parseErr := parseStreamJSON(streamCtx, execStream.Stdout, handler)
|
||||
|
||||
// Wait for process exit.
|
||||
exitCode, waitErr := execStream.Wait()
|
||||
stderrStr := strings.TrimSpace(stderrBuf.String())
|
||||
|
||||
if parseErr != nil {
|
||||
if stderrStr != "" {
|
||||
return fmt.Errorf("%w (stderr: %s)", parseErr, stderrStr)
|
||||
}
|
||||
return parseErr
|
||||
}
|
||||
if waitErr != nil {
|
||||
if stderrStr != "" {
|
||||
return fmt.Errorf("%w (stderr: %s)", waitErr, stderrStr)
|
||||
}
|
||||
return waitErr
|
||||
}
|
||||
if exitCode != 0 {
|
||||
if stderrStr != "" {
|
||||
return fmt.Errorf("claude CLI exited with code %d: %s", exitCode, stderrStr)
|
||||
}
|
||||
return fmt.Errorf("claude CLI exited with code %d", exitCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup kills any remaining claude processes.
|
||||
// mode=cli: kill all claude CLI processes.
|
||||
func (r *ClaudeRunner) Cleanup(ctx context.Context, computer infra.Computer) error {
|
||||
if computer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.mode != "service" {
|
||||
computer.Exec(ctx, []string{"sh", "-c", "pkill -f 'claude' || true"})
|
||||
oe := resolveOSEnv(computer, nil)
|
||||
computer.Exec(ctx, oe.killProcessCmd("claude"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasExistingSession checks if a Claude CLI session exists in the workspace.
|
||||
func hasExistingSession(ctx context.Context, computer infra.Computer, workDir string) bool {
|
||||
sessionDir := path.Join(workDir, ".claude/projects")
|
||||
result, err := computer.Exec(ctx, []string{"ls", sessionDir})
|
||||
func hasExistingSession(ctx context.Context, computer infra.Computer, oe *osEnv) bool {
|
||||
sessionDir := oe.pathJoin(oe.WorkDir, ".claude", "projects")
|
||||
result, err := computer.Exec(ctx, oe.listDirCmd(sessionDir))
|
||||
if err != nil || result.ExitCode != 0 {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(result.Stdout) != ""
|
||||
}
|
||||
|
||||
// buildCLICommand constructs the Claude CLI command and environment variables.
|
||||
func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation bool) ([]string, map[string]string) {
|
||||
workDir := resolveWorkDir(req.Config)
|
||||
userHome := resolveUserHome(req.Config)
|
||||
|
||||
// buildCLICommand constructs the Claude CLI command, environment variables, and optional stdin bytes.
|
||||
func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, oe *osEnv, isContinuation bool) ([]string, map[string]string, []byte) {
|
||||
env := make(map[string]string)
|
||||
env["HOME"] = workDir
|
||||
|
||||
// User-specific paths (only set when running as non-root user inside container).
|
||||
if userHome != "" {
|
||||
env["XAUTHORITY"] = path.Join(userHome, ".Xauthority")
|
||||
if oe.isWindows() {
|
||||
env["USERPROFILE"] = oe.WorkDir
|
||||
if len(oe.WorkDir) >= 2 && oe.WorkDir[1] == ':' {
|
||||
env["HOMEDRIVE"] = oe.WorkDir[:2]
|
||||
env["HOMEPATH"] = oe.WorkDir[2:]
|
||||
}
|
||||
} else {
|
||||
env["HOME"] = oe.WorkDir
|
||||
if oe.UserHome != "" {
|
||||
env["XAUTHORITY"] = path.Join(oe.UserHome, ".Xauthority")
|
||||
}
|
||||
}
|
||||
|
||||
// Connector environment.
|
||||
if req.Connector != nil {
|
||||
setting := req.Connector.Setting()
|
||||
host, _ := setting["host"].(string)
|
||||
|
|
@ -209,23 +229,20 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation
|
|||
}
|
||||
}
|
||||
|
||||
// Secrets from config.
|
||||
if req.Config != nil && len(req.Config.Secrets) > 0 {
|
||||
for k, v := range req.Config.Secrets {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Build system prompt.
|
||||
var systemPrompt string
|
||||
envPrompt := buildSandboxEnvPrompt(workDir)
|
||||
envPrompt := buildSandboxEnvPrompt(oe.WorkDir)
|
||||
if !isContinuation && req.SystemPrompt != "" {
|
||||
systemPrompt = req.SystemPrompt + "\n\n" + envPrompt
|
||||
} else if !isContinuation {
|
||||
systemPrompt = envPrompt
|
||||
}
|
||||
|
||||
// Build input JSONL.
|
||||
var inputJSONL string
|
||||
if isContinuation {
|
||||
inputJSONL = buildLastUserMessageJSONL(req.Messages)
|
||||
|
|
@ -233,10 +250,19 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation
|
|||
inputJSONL = buildFirstRequestJSONL(req.Messages)
|
||||
}
|
||||
|
||||
// CLI args.
|
||||
var args []string
|
||||
args = append(args, "--dangerously-skip-permissions")
|
||||
args = append(args, "--permission-mode", "bypassPermissions")
|
||||
|
||||
permMode := ""
|
||||
if req.Config != nil && req.Config.Runner.Options != nil {
|
||||
if v, ok := req.Config.Runner.Options["permission_mode"]; ok {
|
||||
permMode = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
if permMode == "bypassPermissions" {
|
||||
args = append(args, "--dangerously-skip-permissions")
|
||||
args = append(args, "--permission-mode", permMode)
|
||||
}
|
||||
|
||||
args = append(args, "--input-format", "stream-json")
|
||||
args = append(args, "--output-format", "stream-json")
|
||||
args = append(args, "--include-partial-messages")
|
||||
|
|
@ -246,7 +272,6 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation
|
|||
args = append(args, "--continue")
|
||||
}
|
||||
|
||||
// Runner options pass-through.
|
||||
if req.Config != nil && req.Config.Runner.Options != nil {
|
||||
for key, val := range req.Config.Runner.Options {
|
||||
if flag, ok := claudeArgWhitelist[key]; ok {
|
||||
|
|
@ -255,39 +280,16 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation
|
|||
}
|
||||
}
|
||||
|
||||
// MCP config (set by Prepare if MCPServers were present).
|
||||
if r.hasMCP {
|
||||
args = append(args, "--mcp-config", path.Join(workDir, ".mcp.json"))
|
||||
mcpPath := oe.pathJoin(oe.WorkDir, ".mcp.json")
|
||||
args = append(args, "--mcp-config", mcpPath)
|
||||
if r.mcpToolPattern != "" {
|
||||
args = append(args, "--allowedTools", r.mcpToolPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Build bash command with heredoc.
|
||||
var bash strings.Builder
|
||||
if userHome != "" {
|
||||
bash.WriteString(fmt.Sprintf("touch %s/.Xauthority 2>/dev/null; ", userHome))
|
||||
}
|
||||
bash.WriteString("touch \"$HOME/.Xauthority\" 2>/dev/null\n")
|
||||
|
||||
if systemPrompt != "" {
|
||||
promptFile := path.Join(workDir, ".yao/.system-prompt.txt")
|
||||
bash.WriteString(fmt.Sprintf("mkdir -p %s/.yao\n", workDir))
|
||||
bash.WriteString(fmt.Sprintf("cat << 'PROMPTEOF' > %s\n", promptFile))
|
||||
bash.WriteString(systemPrompt)
|
||||
bash.WriteString("\nPROMPTEOF\n")
|
||||
args = append(args, "--append-system-prompt-file", promptFile)
|
||||
}
|
||||
|
||||
bash.WriteString("cat << 'INPUTEOF' | claude -p")
|
||||
for _, arg := range args {
|
||||
bash.WriteString(fmt.Sprintf(" %q", arg))
|
||||
}
|
||||
bash.WriteString(" 2>&1\n")
|
||||
bash.WriteString(inputJSONL)
|
||||
bash.WriteString("\nINPUTEOF")
|
||||
|
||||
return []string{"bash", "-c", bash.String()}, env
|
||||
script, stdin := oe.buildCLIScript(args, systemPrompt, inputJSONL)
|
||||
return oe.shellCmd(script), env, stdin
|
||||
}
|
||||
|
||||
// buildMCPConfig creates the .mcp.json for Claude CLI based on declared servers.
|
||||
|
|
@ -374,30 +376,6 @@ When working with GitHub and a token is provided:
|
|||
`, workDir)
|
||||
}
|
||||
|
||||
// resolveWorkDir returns the configured working directory, falling back to default.
|
||||
func resolveWorkDir(cfg *types.SandboxConfig) string {
|
||||
if cfg != nil && cfg.Computer.WorkDir != "" {
|
||||
return cfg.Computer.WorkDir
|
||||
}
|
||||
return defaultWorkDir
|
||||
}
|
||||
|
||||
// resolveUserHome returns the home directory for the container user.
|
||||
// Returns empty string if no user is configured (root or unspecified).
|
||||
func resolveUserHome(cfg *types.SandboxConfig) string {
|
||||
if cfg == nil {
|
||||
return defaultUserHome
|
||||
}
|
||||
user := cfg.Computer.User
|
||||
if user == "" {
|
||||
user = defaultUser
|
||||
}
|
||||
if user == "root" {
|
||||
return "/root"
|
||||
}
|
||||
return fmt.Sprintf("/home/%s", user)
|
||||
}
|
||||
|
||||
var claudeArgWhitelist = map[string]string{
|
||||
"max_turns": "--max-turns",
|
||||
"disallowed_tools": "--disallowed-tools",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import (
|
|||
agentContext "github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
||||
infra "github.com/yaoapp/yao/sandbox/v2"
|
||||
"github.com/yaoapp/yao/tai"
|
||||
"github.com/yaoapp/yao/workspace"
|
||||
)
|
||||
|
||||
// BuildIdentifier determines the Computer identifier based on lifecycle policy
|
||||
|
|
@ -59,6 +61,98 @@ func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *i
|
|||
}
|
||||
cfg.WorkspaceID = workspaceID
|
||||
|
||||
// Resolve computer_id from metadata to determine kind and nodeID.
|
||||
computerID := ""
|
||||
if ctx.Metadata != nil {
|
||||
if cid, ok := ctx.Metadata["computer_id"].(string); ok && cid != "" {
|
||||
computerID = cid
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace-wins rule: when both workspace_id and computer_id are present,
|
||||
// the workspace's bound node takes precedence over computer_id.
|
||||
if workspaceID != "" && workspaceID != ownerID {
|
||||
wsNode, err := workspace.M().NodeForWorkspace(context.Background(), workspaceID)
|
||||
if err == nil && wsNode != "" {
|
||||
if computerID != "" && computerID != wsNode {
|
||||
log.Printf("[sandbox/v2] workspace %s bound to node %s overrides computer_id %s", workspaceID, wsNode, computerID)
|
||||
}
|
||||
computerID = wsNode
|
||||
}
|
||||
}
|
||||
|
||||
if computerID != "" {
|
||||
return resolveComputerByID(cfg, manager, computerID, ownerID, identifier, workspaceID, conn...)
|
||||
}
|
||||
|
||||
// No computer_id: fall back to DSL-based dispatch (original logic).
|
||||
return resolveComputerByDSL(cfg, manager, ownerID, identifier, workspaceID, conn...)
|
||||
}
|
||||
|
||||
// resolveComputerByID dispatches based on the runtime computer_id from metadata.
|
||||
// It queries the registry and sandbox manager to determine the computer kind.
|
||||
func resolveComputerByID(
|
||||
cfg *types.SandboxConfig, manager *infra.Manager,
|
||||
computerID, ownerID, identifier, workspaceID string,
|
||||
conn ...connector.Connector,
|
||||
) (infra.Computer, string, error) {
|
||||
|
||||
// 1) Check if computer_id is a known Tai node (host or node kind).
|
||||
if node, ok := tai.GetNodeMeta(computerID); ok {
|
||||
cfg.NodeID = computerID
|
||||
hasContainerRuntime := node.Capabilities.Docker || node.Capabilities.K8s
|
||||
|
||||
if node.Capabilities.HostExec && !hasContainerRuntime {
|
||||
// Host-only node: must use host mode regardless of DSL image config.
|
||||
cfg.Kind = "host"
|
||||
host, err := manager.Host(context.Background(), computerID)
|
||||
if err != nil {
|
||||
return nil, identifier, fmt.Errorf("get host computer: %w", err)
|
||||
}
|
||||
host.BindWorkplace(workspaceID)
|
||||
return host, identifier, nil
|
||||
}
|
||||
|
||||
if node.Capabilities.HostExec && hasContainerRuntime && cfg.Computer.Image == "" {
|
||||
// Dual-capable node with no image in DSL: prefer host mode.
|
||||
cfg.Kind = "host"
|
||||
host, err := manager.Host(context.Background(), computerID)
|
||||
if err != nil {
|
||||
return nil, identifier, fmt.Errorf("get host computer: %w", err)
|
||||
}
|
||||
host.BindWorkplace(workspaceID)
|
||||
return host, identifier, nil
|
||||
}
|
||||
|
||||
if !hasContainerRuntime {
|
||||
return nil, identifier, fmt.Errorf("node %q has no container runtime and no host_exec capability", computerID)
|
||||
}
|
||||
|
||||
// Node with container runtime and DSL has image: create/reuse a box.
|
||||
cfg.Kind = "box"
|
||||
return resolveBox(cfg, manager, ownerID, identifier, workspaceID, conn...)
|
||||
}
|
||||
|
||||
// 2) Check if computer_id is an existing box ID.
|
||||
if manager != nil {
|
||||
box, err := manager.Get(context.Background(), computerID)
|
||||
if err == nil && box != nil {
|
||||
cfg.Kind = "box"
|
||||
box.BindWorkplace(workspaceID)
|
||||
return box, computerID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, identifier, fmt.Errorf("computer %q not found in registry or sandbox manager", computerID)
|
||||
}
|
||||
|
||||
// resolveComputerByDSL dispatches based on DSL static configuration (cfg.Computer.Image).
|
||||
func resolveComputerByDSL(
|
||||
cfg *types.SandboxConfig, manager *infra.Manager,
|
||||
ownerID, identifier, workspaceID string,
|
||||
conn ...connector.Connector,
|
||||
) (infra.Computer, string, error) {
|
||||
|
||||
// Host mode: no image → host computer.
|
||||
if cfg.Computer.Image == "" {
|
||||
cfg.Kind = "host"
|
||||
|
|
@ -75,6 +169,15 @@ func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *i
|
|||
}
|
||||
|
||||
cfg.Kind = "box"
|
||||
return resolveBox(cfg, manager, ownerID, identifier, workspaceID, conn...)
|
||||
}
|
||||
|
||||
// resolveBox reuses or creates a box container.
|
||||
func resolveBox(
|
||||
cfg *types.SandboxConfig, manager *infra.Manager,
|
||||
ownerID, identifier, workspaceID string,
|
||||
conn ...connector.Connector,
|
||||
) (infra.Computer, string, error) {
|
||||
|
||||
// Reuse: non-empty identifier → try Get first.
|
||||
if identifier != "" {
|
||||
|
|
|
|||
|
|
@ -144,7 +144,12 @@ func runExecStep(ctx context.Context, computer infra.Computer, step types.Prepar
|
|||
}
|
||||
}
|
||||
|
||||
result, err := computer.Exec(ctx, shellWrap(kind, script), infra.WithWorkDir("/"))
|
||||
rootDir := "/"
|
||||
if isWindowsComputer(computer) {
|
||||
rootDir = `C:\`
|
||||
}
|
||||
|
||||
result, err := computer.Exec(ctx, shellWrap(kind, script), infra.WithWorkDir(rootDir))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -155,6 +160,10 @@ func runExecStep(ctx context.Context, computer infra.Computer, step types.Prepar
|
|||
return checkResult(result, label)
|
||||
}
|
||||
|
||||
func isWindowsComputer(computer infra.Computer) bool {
|
||||
return strings.EqualFold(computer.ComputerInfo().System.OS, "windows")
|
||||
}
|
||||
|
||||
// checkResult inspects ExecResult for errors.
|
||||
func checkResult(result *infra.ExecResult, label string) error {
|
||||
if result.Error != "" {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
// - DELETE /:id/files/*path — delete file
|
||||
// - POST /:id/mkdir — create directory
|
||||
// - POST /:id/rename — rename file/directory
|
||||
// - GET /:id/rootdir — get workspace root directory absolute path
|
||||
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||
group.Use(oauth.Guard)
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
group.PUT("/:id", handleUpdate)
|
||||
group.DELETE("/:id", handleDelete)
|
||||
|
||||
group.GET("/:id/rootdir", handleRootDir)
|
||||
group.GET("/:id/files", handleListFiles)
|
||||
group.GET("/:id/files/*path", handleReadFile)
|
||||
group.PUT("/:id/files/*path", handleWriteFile)
|
||||
|
|
@ -285,6 +287,21 @@ func handleDelete(c *gin.Context) {
|
|||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleRootDir(c *gin.Context) {
|
||||
_, ok := resolveAndCheckWS(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rootDir, err := mgr().MountPath(context.Background(), c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, gin.H{"root_dir": rootDir})
|
||||
}
|
||||
|
||||
func handleListFiles(c *gin.Context) {
|
||||
_, ok := resolveAndCheckWS(c)
|
||||
if !ok {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type Box struct {
|
|||
image string
|
||||
workspaceID string
|
||||
system SystemInfo
|
||||
workDir string
|
||||
ws taiworkspace.FS
|
||||
manager *Manager
|
||||
}
|
||||
|
|
@ -195,6 +196,14 @@ func (b *Box) Workspace() taiworkspace.FS {
|
|||
return b.ws
|
||||
}
|
||||
|
||||
// GetWorkDir returns the container-internal working directory for command execution.
|
||||
func (b *Box) GetWorkDir() string {
|
||||
if b.workDir != "" {
|
||||
return b.workDir
|
||||
}
|
||||
return "/workspace"
|
||||
}
|
||||
|
||||
// WorkspaceID returns the workspace ID mounted to this sandbox, or empty string.
|
||||
func (b *Box) WorkspaceID() string { return b.workspaceID }
|
||||
|
||||
|
|
|
|||
|
|
@ -233,6 +233,21 @@ func (h *Host) Workplace() taiworkspace.FS {
|
|||
return taiworkspace.New(res.Volume, h.workplaceID)
|
||||
}
|
||||
|
||||
// GetWorkDir returns the host working directory for command execution.
|
||||
// Resolves from the bound workspace's root path on disk, falling back to
|
||||
// the system temp directory if no workspace is bound or root resolution fails.
|
||||
func (h *Host) GetWorkDir() string {
|
||||
if ws := h.Workplace(); ws != nil {
|
||||
if root, err := ws.GetRoot(); err == nil && root != "" {
|
||||
return root
|
||||
}
|
||||
}
|
||||
if h.system.TempDir != "" {
|
||||
return h.system.TempDir
|
||||
}
|
||||
return "/tmp"
|
||||
}
|
||||
|
||||
// NodeID returns the node ID this Host belongs to.
|
||||
func (h *Host) NodeID() string { return h.nodeID }
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,11 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
|
|||
TempDir: res.System.TempDir,
|
||||
}
|
||||
|
||||
boxWorkDir := opts.WorkDir
|
||||
if boxWorkDir == "" {
|
||||
boxWorkDir = "/workspace"
|
||||
}
|
||||
|
||||
box := &Box{
|
||||
id: id,
|
||||
containerID: containerID,
|
||||
|
|
@ -214,6 +219,7 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
|
|||
vnc: opts.VNC,
|
||||
image: opts.Image,
|
||||
workspaceID: opts.WorkspaceID,
|
||||
workDir: boxWorkDir,
|
||||
system: sys,
|
||||
}
|
||||
box.lastCall.Store(time.Now().UnixMilli())
|
||||
|
|
@ -459,6 +465,7 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn
|
|||
createdAt: time.Now(),
|
||||
image: c.Image,
|
||||
workspaceID: c.Labels["workspace-id"],
|
||||
workDir: "/workspace",
|
||||
manager: m,
|
||||
}
|
||||
box.lastCall.Store(time.Now().UnixMilli())
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type Computer interface {
|
|||
Proxy(ctx context.Context, port int, path string) (string, error)
|
||||
BindWorkplace(workspaceID string)
|
||||
Workplace() workspace.FS
|
||||
GetWorkDir() string
|
||||
}
|
||||
|
||||
// ComputerInfo holds identity and registry information for a Computer.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ type TunnelHandler struct {
|
|||
reg *registry.Registry
|
||||
pending sync.Map // channel_id → chan taipb.TaiTunnel_ForwardServer
|
||||
logger *slog.Logger
|
||||
|
||||
sendMu sync.Map // taiID → *sync.Mutex – serializes Send on each Register stream
|
||||
}
|
||||
|
||||
// NewTunnelHandler creates a TunnelHandler backed by the given registry.
|
||||
|
|
@ -84,17 +86,24 @@ func (h *TunnelHandler) Register(stream taipb.TaiTunnel_RegisterServer) error {
|
|||
Capabilities: capsFromProto(msg.Caps),
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
h.sendMu.Store(resolvedTaiID, &mu)
|
||||
|
||||
h.reg.Register(node)
|
||||
h.reg.SetRegisterStream(resolvedTaiID, stream)
|
||||
defer func() {
|
||||
h.sendMu.Delete(resolvedTaiID)
|
||||
h.reg.Unregister(resolvedTaiID)
|
||||
h.logger.Info("tai gRPC tunnel disconnected", "tai_id", resolvedTaiID)
|
||||
}()
|
||||
|
||||
if err := stream.Send(&taipb.TunnelControl{
|
||||
mu.Lock()
|
||||
err = stream.Send(&taipb.TunnelControl{
|
||||
Type: "registered",
|
||||
TaiId: resolvedTaiID,
|
||||
}); err != nil {
|
||||
})
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("send registered: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -133,8 +142,11 @@ func (h *TunnelHandler) Register(stream taipb.TaiTunnel_RegisterServer) error {
|
|||
switch ctrl.Type {
|
||||
case "ping":
|
||||
h.reg.UpdatePing(resolvedTaiID)
|
||||
if err := stream.Send(&taipb.TunnelControl{Type: "pong"}); err != nil {
|
||||
return err
|
||||
mu.Lock()
|
||||
sendErr := stream.Send(&taipb.TunnelControl{Type: "pong"})
|
||||
mu.Unlock()
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,9 +175,12 @@ func (h *TunnelHandler) Forward(stream taipb.TaiTunnel_ForwardServer) error {
|
|||
}
|
||||
channelID := vals[0]
|
||||
|
||||
h.logger.Debug("[forward] Forward stream arrived", "channel_id", channelID[:16])
|
||||
|
||||
if ch, ok := h.pending.LoadAndDelete(channelID); ok {
|
||||
ch.(chan taipb.TaiTunnel_ForwardServer) <- stream
|
||||
} else {
|
||||
h.logger.Warn("[forward] no pending channel (expired?)", "channel_id", channelID[:16])
|
||||
return fmt.Errorf("no pending channel for %s", channelID)
|
||||
}
|
||||
|
||||
|
|
@ -181,6 +196,12 @@ func (h *TunnelHandler) RequestForward(taiID string, targetPort int) (taipb.TaiT
|
|||
return nil, fmt.Errorf("tai %s: no active register stream", taiID)
|
||||
}
|
||||
|
||||
muVal, ok := h.sendMu.Load(taiID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tai %s: no send mutex (stream closing?)", taiID)
|
||||
}
|
||||
mu := muVal.(*sync.Mutex)
|
||||
|
||||
channelID, err := registry.GenerateChannelID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate channel_id: %w", err)
|
||||
|
|
@ -194,19 +215,31 @@ func (h *TunnelHandler) RequestForward(taiID string, targetPort int) (taipb.TaiT
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("tai %s: register stream type mismatch", taiID)
|
||||
}
|
||||
if err := regStream.Send(&taipb.TunnelControl{
|
||||
|
||||
h.logger.Debug("[forward] sending open command",
|
||||
"tai_id", taiID, "port", targetPort, "channel_id", channelID[:16])
|
||||
|
||||
mu.Lock()
|
||||
sendErr := regStream.Send(&taipb.TunnelControl{
|
||||
Type: "open",
|
||||
ChannelId: channelID,
|
||||
TargetPort: int32(targetPort),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("send open: %w", err)
|
||||
})
|
||||
mu.Unlock()
|
||||
if sendErr != nil {
|
||||
return nil, fmt.Errorf("send open: %w", sendErr)
|
||||
}
|
||||
|
||||
h.logger.Debug("[forward] open sent, waiting for callback",
|
||||
"tai_id", taiID, "port", targetPort, "channel_id", channelID[:16])
|
||||
|
||||
select {
|
||||
case fwd := <-waitCh:
|
||||
h.logger.Debug("[forward] callback received",
|
||||
"tai_id", taiID, "channel_id", channelID[:16])
|
||||
return fwd, nil
|
||||
case <-time.After(10 * time.Second):
|
||||
return nil, fmt.Errorf("tai %s: forward timeout (10s)", taiID)
|
||||
return nil, fmt.Errorf("tai %s: forward timeout (10s) channel=%s", taiID, channelID[:16])
|
||||
case <-regStream.Context().Done():
|
||||
return nil, fmt.Errorf("tai %s: register stream closed while waiting for forward", taiID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ func (l *localStorage) MkdirAll(_ context.Context, sessionID, path string) error
|
|||
return os.MkdirAll(abs, 0o755)
|
||||
}
|
||||
|
||||
func (l *localStorage) Abs(_ context.Context, sessionID, path string) (string, error) {
|
||||
return l.abs(sessionID, path)
|
||||
}
|
||||
|
||||
// Copy duplicates src to dst within the same workspace session.
|
||||
// Supports single files and directories (recursive). Uses excludes from SyncOption
|
||||
// and forceFull to overwrite even when mtime+size match.
|
||||
|
|
|
|||
|
|
@ -190,6 +190,10 @@ func (m *mockVolumeServer) SyncPull(req *pb.SyncManifest, stream grpc.ServerStre
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVolumeServer) Abs(_ context.Context, req *pb.FSRequest) (*pb.FSAbsResponse, error) {
|
||||
return &pb.FSAbsResponse{Path: "/data/" + req.SessionId + "/" + req.Path}, nil
|
||||
}
|
||||
|
||||
func (m *mockVolumeServer) ListDir(_ context.Context, req *pb.FSRequest) (*pb.FSListResponse, error) {
|
||||
return &pb.FSListResponse{Entries: []*pb.FileInfo{
|
||||
{Path: "a.txt", Size: 10},
|
||||
|
|
@ -560,6 +564,10 @@ func (m *errMockVolumeServer) Copy(_ context.Context, _ *pb.FSCopyRequest) (*pb.
|
|||
return nil, fmt.Errorf("injected copy error")
|
||||
}
|
||||
|
||||
func (m *errMockVolumeServer) Abs(_ context.Context, _ *pb.FSRequest) (*pb.FSAbsResponse, error) {
|
||||
return nil, fmt.Errorf("injected abs error")
|
||||
}
|
||||
|
||||
func startErrMockServer(t *testing.T) (*grpc.ClientConn, func()) {
|
||||
t.Helper()
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
|
@ -748,3 +756,42 @@ func TestErrRemoteCopy(t *testing.T) {
|
|||
t.Error("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockRemoteAbs(t *testing.T) {
|
||||
conn, cleanup := startMockServer(t, &mockVolumeServer{})
|
||||
defer cleanup()
|
||||
|
||||
vol := NewRemote(conn)
|
||||
got, err := vol.Abs(context.Background(), "s1", ".")
|
||||
if err != nil {
|
||||
t.Fatalf("Abs: %v", err)
|
||||
}
|
||||
if got != "/data/s1/." {
|
||||
t.Errorf("Abs = %q, want %q", got, "/data/s1/.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockRemoteAbsRelative(t *testing.T) {
|
||||
conn, cleanup := startMockServer(t, &mockVolumeServer{})
|
||||
defer cleanup()
|
||||
|
||||
vol := NewRemote(conn)
|
||||
got, err := vol.Abs(context.Background(), "s1", "sub/file.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Abs: %v", err)
|
||||
}
|
||||
if got != "/data/s1/sub/file.txt" {
|
||||
t.Errorf("Abs = %q, want %q", got, "/data/s1/sub/file.txt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrRemoteAbs(t *testing.T) {
|
||||
conn, cleanup := startErrMockServer(t)
|
||||
defer cleanup()
|
||||
|
||||
vol := NewRemote(conn)
|
||||
_, err := vol.Abs(context.Background(), "s1", ".")
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -631,6 +631,50 @@ func (x *FSOpResponse) GetError() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
type FSAbsResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // absolute path on the host filesystem
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FSAbsResponse) Reset() {
|
||||
*x = FSAbsResponse{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FSAbsResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FSAbsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *FSAbsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FSAbsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*FSAbsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *FSAbsResponse) GetPath() string {
|
||||
if x != nil {
|
||||
return x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type FSReadRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
|
||||
|
|
@ -641,7 +685,7 @@ type FSReadRequest struct {
|
|||
|
||||
func (x *FSReadRequest) Reset() {
|
||||
*x = FSReadRequest{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[8]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -653,7 +697,7 @@ func (x *FSReadRequest) String() string {
|
|||
func (*FSReadRequest) ProtoMessage() {}
|
||||
|
||||
func (x *FSReadRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[8]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -666,7 +710,7 @@ func (x *FSReadRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSReadRequest.ProtoReflect.Descriptor instead.
|
||||
func (*FSReadRequest) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{8}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *FSReadRequest) GetSessionId() string {
|
||||
|
|
@ -695,7 +739,7 @@ type FSDataChunk struct {
|
|||
|
||||
func (x *FSDataChunk) Reset() {
|
||||
*x = FSDataChunk{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[9]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -707,7 +751,7 @@ func (x *FSDataChunk) String() string {
|
|||
func (*FSDataChunk) ProtoMessage() {}
|
||||
|
||||
func (x *FSDataChunk) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[9]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -720,7 +764,7 @@ func (x *FSDataChunk) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSDataChunk.ProtoReflect.Descriptor instead.
|
||||
func (*FSDataChunk) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{9}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *FSDataChunk) GetData() []byte {
|
||||
|
|
@ -764,7 +808,7 @@ type FSWriteChunk struct {
|
|||
|
||||
func (x *FSWriteChunk) Reset() {
|
||||
*x = FSWriteChunk{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[10]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -776,7 +820,7 @@ func (x *FSWriteChunk) String() string {
|
|||
func (*FSWriteChunk) ProtoMessage() {}
|
||||
|
||||
func (x *FSWriteChunk) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[10]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -789,7 +833,7 @@ func (x *FSWriteChunk) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSWriteChunk.ProtoReflect.Descriptor instead.
|
||||
func (*FSWriteChunk) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{10}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *FSWriteChunk) GetSessionId() string {
|
||||
|
|
@ -836,7 +880,7 @@ type FSWriteResponse struct {
|
|||
|
||||
func (x *FSWriteResponse) Reset() {
|
||||
*x = FSWriteResponse{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[11]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -848,7 +892,7 @@ func (x *FSWriteResponse) String() string {
|
|||
func (*FSWriteResponse) ProtoMessage() {}
|
||||
|
||||
func (x *FSWriteResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[11]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -861,7 +905,7 @@ func (x *FSWriteResponse) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSWriteResponse.ProtoReflect.Descriptor instead.
|
||||
func (*FSWriteResponse) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{11}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *FSWriteResponse) GetSize() int64 {
|
||||
|
|
@ -880,7 +924,7 @@ type FSListResponse struct {
|
|||
|
||||
func (x *FSListResponse) Reset() {
|
||||
*x = FSListResponse{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[12]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -892,7 +936,7 @@ func (x *FSListResponse) String() string {
|
|||
func (*FSListResponse) ProtoMessage() {}
|
||||
|
||||
func (x *FSListResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[12]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -905,7 +949,7 @@ func (x *FSListResponse) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSListResponse.ProtoReflect.Descriptor instead.
|
||||
func (*FSListResponse) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{12}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *FSListResponse) GetEntries() []*FileInfo {
|
||||
|
|
@ -926,7 +970,7 @@ type FSRemoveRequest struct {
|
|||
|
||||
func (x *FSRemoveRequest) Reset() {
|
||||
*x = FSRemoveRequest{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[13]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -938,7 +982,7 @@ func (x *FSRemoveRequest) String() string {
|
|||
func (*FSRemoveRequest) ProtoMessage() {}
|
||||
|
||||
func (x *FSRemoveRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[13]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -951,7 +995,7 @@ func (x *FSRemoveRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSRemoveRequest.ProtoReflect.Descriptor instead.
|
||||
func (*FSRemoveRequest) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{13}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *FSRemoveRequest) GetSessionId() string {
|
||||
|
|
@ -986,7 +1030,7 @@ type FSRenameRequest struct {
|
|||
|
||||
func (x *FSRenameRequest) Reset() {
|
||||
*x = FSRenameRequest{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[14]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -998,7 +1042,7 @@ func (x *FSRenameRequest) String() string {
|
|||
func (*FSRenameRequest) ProtoMessage() {}
|
||||
|
||||
func (x *FSRenameRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[14]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -1011,7 +1055,7 @@ func (x *FSRenameRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSRenameRequest.ProtoReflect.Descriptor instead.
|
||||
func (*FSRenameRequest) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{14}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *FSRenameRequest) GetSessionId() string {
|
||||
|
|
@ -1048,7 +1092,7 @@ type FSCopyRequest struct {
|
|||
|
||||
func (x *FSCopyRequest) Reset() {
|
||||
*x = FSCopyRequest{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[15]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -1060,7 +1104,7 @@ func (x *FSCopyRequest) String() string {
|
|||
func (*FSCopyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *FSCopyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[15]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[16]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -1073,7 +1117,7 @@ func (x *FSCopyRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use FSCopyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*FSCopyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{15}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{16}
|
||||
}
|
||||
|
||||
func (x *FSCopyRequest) GetSessionId() string {
|
||||
|
|
@ -1123,7 +1167,7 @@ type ArchiveRequest struct {
|
|||
|
||||
func (x *ArchiveRequest) Reset() {
|
||||
*x = ArchiveRequest{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[16]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -1135,7 +1179,7 @@ func (x *ArchiveRequest) String() string {
|
|||
func (*ArchiveRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ArchiveRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[16]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -1148,7 +1192,7 @@ func (x *ArchiveRequest) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use ArchiveRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ArchiveRequest) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{16}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *ArchiveRequest) GetSessionId() string {
|
||||
|
|
@ -1189,7 +1233,7 @@ type ArchiveResponse struct {
|
|||
|
||||
func (x *ArchiveResponse) Reset() {
|
||||
*x = ArchiveResponse{}
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[17]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -1201,7 +1245,7 @@ func (x *ArchiveResponse) String() string {
|
|||
func (*ArchiveResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ArchiveResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[17]
|
||||
mi := &file_tai_volume_pb_volume_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -1214,7 +1258,7 @@ func (x *ArchiveResponse) ProtoReflect() protoreflect.Message {
|
|||
|
||||
// Deprecated: Use ArchiveResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ArchiveResponse) Descriptor() ([]byte, []int) {
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{17}
|
||||
return file_tai_volume_pb_volume_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *ArchiveResponse) GetSizeBytes() int64 {
|
||||
|
|
@ -1285,7 +1329,9 @@ const file_tai_volume_pb_volume_proto_rawDesc = "" +
|
|||
"\x04path\x18\x02 \x01(\tR\x04path\"4\n" +
|
||||
"\fFSOpResponse\x12\x0e\n" +
|
||||
"\x02ok\x18\x01 \x01(\bR\x02ok\x12\x14\n" +
|
||||
"\x05error\x18\x02 \x01(\tR\x05error\"B\n" +
|
||||
"\x05error\x18\x02 \x01(\tR\x05error\"#\n" +
|
||||
"\rFSAbsResponse\x12\x12\n" +
|
||||
"\x04path\x18\x01 \x01(\tR\x04path\"B\n" +
|
||||
"\rFSReadRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"session_id\x18\x01 \x01(\tR\tsessionId\x12\x12\n" +
|
||||
|
|
@ -1334,7 +1380,7 @@ const file_tai_volume_pb_volume_proto_rawDesc = "" +
|
|||
"\n" +
|
||||
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes\x12\x1f\n" +
|
||||
"\vfiles_count\x18\x02 \x01(\x05R\n" +
|
||||
"filesCount2\xfa\a\n" +
|
||||
"filesCount2\xab\b\n" +
|
||||
"\x06Volume\x128\n" +
|
||||
"\bSyncPush\x12\x13.volume.SyncMessage\x1a\x13.volume.SyncMessage(\x010\x01\x127\n" +
|
||||
"\bSyncPull\x12\x14.volume.SyncManifest\x1a\x13.volume.SyncMessage0\x01\x128\n" +
|
||||
|
|
@ -1344,7 +1390,8 @@ const file_tai_volume_pb_volume_proto_rawDesc = "" +
|
|||
"\aListDir\x12\x11.volume.FSRequest\x1a\x16.volume.FSListResponse\x127\n" +
|
||||
"\x06Remove\x12\x17.volume.FSRemoveRequest\x1a\x14.volume.FSOpResponse\x127\n" +
|
||||
"\x06Rename\x12\x17.volume.FSRenameRequest\x1a\x14.volume.FSOpResponse\x123\n" +
|
||||
"\bMkdirAll\x12\x11.volume.FSRequest\x1a\x14.volume.FSOpResponse\x121\n" +
|
||||
"\bMkdirAll\x12\x11.volume.FSRequest\x1a\x14.volume.FSOpResponse\x12/\n" +
|
||||
"\x03Abs\x12\x11.volume.FSRequest\x1a\x15.volume.FSAbsResponse\x121\n" +
|
||||
"\x04Copy\x12\x15.volume.FSCopyRequest\x1a\x12.volume.SyncResult\x126\n" +
|
||||
"\x03Zip\x12\x16.volume.ArchiveRequest\x1a\x17.volume.ArchiveResponse\x128\n" +
|
||||
"\x05Unzip\x12\x16.volume.ArchiveRequest\x1a\x17.volume.ArchiveResponse\x127\n" +
|
||||
|
|
@ -1368,7 +1415,7 @@ func file_tai_volume_pb_volume_proto_rawDescGZIP() []byte {
|
|||
}
|
||||
|
||||
var file_tai_volume_pb_volume_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_tai_volume_pb_volume_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
|
||||
var file_tai_volume_pb_volume_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
|
||||
var file_tai_volume_pb_volume_proto_goTypes = []any{
|
||||
(FileChunk_ChunkType)(0), // 0: volume.FileChunk.ChunkType
|
||||
(*FileInfo)(nil), // 1: volume.FileInfo
|
||||
|
|
@ -1379,16 +1426,17 @@ var file_tai_volume_pb_volume_proto_goTypes = []any{
|
|||
(*SyncResult)(nil), // 6: volume.SyncResult
|
||||
(*FSRequest)(nil), // 7: volume.FSRequest
|
||||
(*FSOpResponse)(nil), // 8: volume.FSOpResponse
|
||||
(*FSReadRequest)(nil), // 9: volume.FSReadRequest
|
||||
(*FSDataChunk)(nil), // 10: volume.FSDataChunk
|
||||
(*FSWriteChunk)(nil), // 11: volume.FSWriteChunk
|
||||
(*FSWriteResponse)(nil), // 12: volume.FSWriteResponse
|
||||
(*FSListResponse)(nil), // 13: volume.FSListResponse
|
||||
(*FSRemoveRequest)(nil), // 14: volume.FSRemoveRequest
|
||||
(*FSRenameRequest)(nil), // 15: volume.FSRenameRequest
|
||||
(*FSCopyRequest)(nil), // 16: volume.FSCopyRequest
|
||||
(*ArchiveRequest)(nil), // 17: volume.ArchiveRequest
|
||||
(*ArchiveResponse)(nil), // 18: volume.ArchiveResponse
|
||||
(*FSAbsResponse)(nil), // 9: volume.FSAbsResponse
|
||||
(*FSReadRequest)(nil), // 10: volume.FSReadRequest
|
||||
(*FSDataChunk)(nil), // 11: volume.FSDataChunk
|
||||
(*FSWriteChunk)(nil), // 12: volume.FSWriteChunk
|
||||
(*FSWriteResponse)(nil), // 13: volume.FSWriteResponse
|
||||
(*FSListResponse)(nil), // 14: volume.FSListResponse
|
||||
(*FSRemoveRequest)(nil), // 15: volume.FSRemoveRequest
|
||||
(*FSRenameRequest)(nil), // 16: volume.FSRenameRequest
|
||||
(*FSCopyRequest)(nil), // 17: volume.FSCopyRequest
|
||||
(*ArchiveRequest)(nil), // 18: volume.ArchiveRequest
|
||||
(*ArchiveResponse)(nil), // 19: volume.ArchiveResponse
|
||||
}
|
||||
var file_tai_volume_pb_volume_proto_depIdxs = []int32{
|
||||
1, // 0: volume.SyncManifest.files:type_name -> volume.FileInfo
|
||||
|
|
@ -1400,42 +1448,44 @@ var file_tai_volume_pb_volume_proto_depIdxs = []int32{
|
|||
1, // 6: volume.FSListResponse.entries:type_name -> volume.FileInfo
|
||||
3, // 7: volume.Volume.SyncPush:input_type -> volume.SyncMessage
|
||||
2, // 8: volume.Volume.SyncPull:input_type -> volume.SyncManifest
|
||||
9, // 9: volume.Volume.ReadFile:input_type -> volume.FSReadRequest
|
||||
11, // 10: volume.Volume.WriteFile:input_type -> volume.FSWriteChunk
|
||||
10, // 9: volume.Volume.ReadFile:input_type -> volume.FSReadRequest
|
||||
12, // 10: volume.Volume.WriteFile:input_type -> volume.FSWriteChunk
|
||||
7, // 11: volume.Volume.Stat:input_type -> volume.FSRequest
|
||||
7, // 12: volume.Volume.ListDir:input_type -> volume.FSRequest
|
||||
14, // 13: volume.Volume.Remove:input_type -> volume.FSRemoveRequest
|
||||
15, // 14: volume.Volume.Rename:input_type -> volume.FSRenameRequest
|
||||
15, // 13: volume.Volume.Remove:input_type -> volume.FSRemoveRequest
|
||||
16, // 14: volume.Volume.Rename:input_type -> volume.FSRenameRequest
|
||||
7, // 15: volume.Volume.MkdirAll:input_type -> volume.FSRequest
|
||||
16, // 16: volume.Volume.Copy:input_type -> volume.FSCopyRequest
|
||||
17, // 17: volume.Volume.Zip:input_type -> volume.ArchiveRequest
|
||||
17, // 18: volume.Volume.Unzip:input_type -> volume.ArchiveRequest
|
||||
17, // 19: volume.Volume.Gzip:input_type -> volume.ArchiveRequest
|
||||
17, // 20: volume.Volume.Gunzip:input_type -> volume.ArchiveRequest
|
||||
17, // 21: volume.Volume.Tar:input_type -> volume.ArchiveRequest
|
||||
17, // 22: volume.Volume.Untar:input_type -> volume.ArchiveRequest
|
||||
17, // 23: volume.Volume.Tgz:input_type -> volume.ArchiveRequest
|
||||
17, // 24: volume.Volume.Untgz:input_type -> volume.ArchiveRequest
|
||||
3, // 25: volume.Volume.SyncPush:output_type -> volume.SyncMessage
|
||||
3, // 26: volume.Volume.SyncPull:output_type -> volume.SyncMessage
|
||||
10, // 27: volume.Volume.ReadFile:output_type -> volume.FSDataChunk
|
||||
12, // 28: volume.Volume.WriteFile:output_type -> volume.FSWriteResponse
|
||||
1, // 29: volume.Volume.Stat:output_type -> volume.FileInfo
|
||||
13, // 30: volume.Volume.ListDir:output_type -> volume.FSListResponse
|
||||
8, // 31: volume.Volume.Remove:output_type -> volume.FSOpResponse
|
||||
8, // 32: volume.Volume.Rename:output_type -> volume.FSOpResponse
|
||||
8, // 33: volume.Volume.MkdirAll:output_type -> volume.FSOpResponse
|
||||
6, // 34: volume.Volume.Copy:output_type -> volume.SyncResult
|
||||
18, // 35: volume.Volume.Zip:output_type -> volume.ArchiveResponse
|
||||
18, // 36: volume.Volume.Unzip:output_type -> volume.ArchiveResponse
|
||||
18, // 37: volume.Volume.Gzip:output_type -> volume.ArchiveResponse
|
||||
18, // 38: volume.Volume.Gunzip:output_type -> volume.ArchiveResponse
|
||||
18, // 39: volume.Volume.Tar:output_type -> volume.ArchiveResponse
|
||||
18, // 40: volume.Volume.Untar:output_type -> volume.ArchiveResponse
|
||||
18, // 41: volume.Volume.Tgz:output_type -> volume.ArchiveResponse
|
||||
18, // 42: volume.Volume.Untgz:output_type -> volume.ArchiveResponse
|
||||
25, // [25:43] is the sub-list for method output_type
|
||||
7, // [7:25] is the sub-list for method input_type
|
||||
7, // 16: volume.Volume.Abs:input_type -> volume.FSRequest
|
||||
17, // 17: volume.Volume.Copy:input_type -> volume.FSCopyRequest
|
||||
18, // 18: volume.Volume.Zip:input_type -> volume.ArchiveRequest
|
||||
18, // 19: volume.Volume.Unzip:input_type -> volume.ArchiveRequest
|
||||
18, // 20: volume.Volume.Gzip:input_type -> volume.ArchiveRequest
|
||||
18, // 21: volume.Volume.Gunzip:input_type -> volume.ArchiveRequest
|
||||
18, // 22: volume.Volume.Tar:input_type -> volume.ArchiveRequest
|
||||
18, // 23: volume.Volume.Untar:input_type -> volume.ArchiveRequest
|
||||
18, // 24: volume.Volume.Tgz:input_type -> volume.ArchiveRequest
|
||||
18, // 25: volume.Volume.Untgz:input_type -> volume.ArchiveRequest
|
||||
3, // 26: volume.Volume.SyncPush:output_type -> volume.SyncMessage
|
||||
3, // 27: volume.Volume.SyncPull:output_type -> volume.SyncMessage
|
||||
11, // 28: volume.Volume.ReadFile:output_type -> volume.FSDataChunk
|
||||
13, // 29: volume.Volume.WriteFile:output_type -> volume.FSWriteResponse
|
||||
1, // 30: volume.Volume.Stat:output_type -> volume.FileInfo
|
||||
14, // 31: volume.Volume.ListDir:output_type -> volume.FSListResponse
|
||||
8, // 32: volume.Volume.Remove:output_type -> volume.FSOpResponse
|
||||
8, // 33: volume.Volume.Rename:output_type -> volume.FSOpResponse
|
||||
8, // 34: volume.Volume.MkdirAll:output_type -> volume.FSOpResponse
|
||||
9, // 35: volume.Volume.Abs:output_type -> volume.FSAbsResponse
|
||||
6, // 36: volume.Volume.Copy:output_type -> volume.SyncResult
|
||||
19, // 37: volume.Volume.Zip:output_type -> volume.ArchiveResponse
|
||||
19, // 38: volume.Volume.Unzip:output_type -> volume.ArchiveResponse
|
||||
19, // 39: volume.Volume.Gzip:output_type -> volume.ArchiveResponse
|
||||
19, // 40: volume.Volume.Gunzip:output_type -> volume.ArchiveResponse
|
||||
19, // 41: volume.Volume.Tar:output_type -> volume.ArchiveResponse
|
||||
19, // 42: volume.Volume.Untar:output_type -> volume.ArchiveResponse
|
||||
19, // 43: volume.Volume.Tgz:output_type -> volume.ArchiveResponse
|
||||
19, // 44: volume.Volume.Untgz:output_type -> volume.ArchiveResponse
|
||||
26, // [26:45] is the sub-list for method output_type
|
||||
7, // [7:26] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
7, // [7:7] is the sub-list for extension extendee
|
||||
0, // [0:7] is the sub-list for field type_name
|
||||
|
|
@ -1458,7 +1508,7 @@ func file_tai_volume_pb_volume_proto_init() {
|
|||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_tai_volume_pb_volume_proto_rawDesc), len(file_tai_volume_pb_volume_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 18,
|
||||
NumMessages: 19,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ service Volume {
|
|||
rpc Remove(FSRemoveRequest) returns (FSOpResponse);
|
||||
rpc Rename(FSRenameRequest) returns (FSOpResponse);
|
||||
rpc MkdirAll(FSRequest) returns (FSOpResponse);
|
||||
// Abs: resolve a session-relative path to its absolute path on the host.
|
||||
rpc Abs(FSRequest) returns (FSAbsResponse);
|
||||
// Copy: copy src to dst within the same workspace (server-side when remote).
|
||||
rpc Copy(FSCopyRequest) returns (SyncResult);
|
||||
|
||||
|
|
@ -112,6 +114,10 @@ message FSOpResponse {
|
|||
string error = 2;
|
||||
}
|
||||
|
||||
message FSAbsResponse {
|
||||
string path = 1; // absolute path on the host filesystem
|
||||
}
|
||||
|
||||
message FSReadRequest {
|
||||
string session_id = 1;
|
||||
string path = 2;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const (
|
|||
Volume_Remove_FullMethodName = "/volume.Volume/Remove"
|
||||
Volume_Rename_FullMethodName = "/volume.Volume/Rename"
|
||||
Volume_MkdirAll_FullMethodName = "/volume.Volume/MkdirAll"
|
||||
Volume_Abs_FullMethodName = "/volume.Volume/Abs"
|
||||
Volume_Copy_FullMethodName = "/volume.Volume/Copy"
|
||||
Volume_Zip_FullMethodName = "/volume.Volume/Zip"
|
||||
Volume_Unzip_FullMethodName = "/volume.Volume/Unzip"
|
||||
|
|
@ -64,6 +65,8 @@ type VolumeClient interface {
|
|||
Remove(ctx context.Context, in *FSRemoveRequest, opts ...grpc.CallOption) (*FSOpResponse, error)
|
||||
Rename(ctx context.Context, in *FSRenameRequest, opts ...grpc.CallOption) (*FSOpResponse, error)
|
||||
MkdirAll(ctx context.Context, in *FSRequest, opts ...grpc.CallOption) (*FSOpResponse, error)
|
||||
// Abs: resolve a session-relative path to its absolute path on the host.
|
||||
Abs(ctx context.Context, in *FSRequest, opts ...grpc.CallOption) (*FSAbsResponse, error)
|
||||
// Copy: copy src to dst within the same workspace (server-side when remote).
|
||||
Copy(ctx context.Context, in *FSCopyRequest, opts ...grpc.CallOption) (*SyncResult, error)
|
||||
Zip(ctx context.Context, in *ArchiveRequest, opts ...grpc.CallOption) (*ArchiveResponse, error)
|
||||
|
|
@ -198,6 +201,16 @@ func (c *volumeClient) MkdirAll(ctx context.Context, in *FSRequest, opts ...grpc
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *volumeClient) Abs(ctx context.Context, in *FSRequest, opts ...grpc.CallOption) (*FSAbsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FSAbsResponse)
|
||||
err := c.cc.Invoke(ctx, Volume_Abs_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *volumeClient) Copy(ctx context.Context, in *FSCopyRequest, opts ...grpc.CallOption) (*SyncResult, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SyncResult)
|
||||
|
|
@ -313,6 +326,8 @@ type VolumeServer interface {
|
|||
Remove(context.Context, *FSRemoveRequest) (*FSOpResponse, error)
|
||||
Rename(context.Context, *FSRenameRequest) (*FSOpResponse, error)
|
||||
MkdirAll(context.Context, *FSRequest) (*FSOpResponse, error)
|
||||
// Abs: resolve a session-relative path to its absolute path on the host.
|
||||
Abs(context.Context, *FSRequest) (*FSAbsResponse, error)
|
||||
// Copy: copy src to dst within the same workspace (server-side when remote).
|
||||
Copy(context.Context, *FSCopyRequest) (*SyncResult, error)
|
||||
Zip(context.Context, *ArchiveRequest) (*ArchiveResponse, error)
|
||||
|
|
@ -360,6 +375,9 @@ func (UnimplementedVolumeServer) Rename(context.Context, *FSRenameRequest) (*FSO
|
|||
func (UnimplementedVolumeServer) MkdirAll(context.Context, *FSRequest) (*FSOpResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method MkdirAll not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServer) Abs(context.Context, *FSRequest) (*FSAbsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Abs not implemented")
|
||||
}
|
||||
func (UnimplementedVolumeServer) Copy(context.Context, *FSCopyRequest) (*SyncResult, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Copy not implemented")
|
||||
}
|
||||
|
|
@ -534,6 +552,24 @@ func _Volume_MkdirAll_Handler(srv interface{}, ctx context.Context, dec func(int
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Volume_Abs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FSRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(VolumeServer).Abs(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Volume_Abs_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(VolumeServer).Abs(ctx, req.(*FSRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Volume_Copy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FSCopyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
|
|
@ -723,6 +759,10 @@ var Volume_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "MkdirAll",
|
||||
Handler: _Volume_MkdirAll_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Abs",
|
||||
Handler: _Volume_Abs_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Copy",
|
||||
Handler: _Volume_Copy_Handler,
|
||||
|
|
|
|||
|
|
@ -167,6 +167,17 @@ func (r *remoteStorage) MkdirAll(ctx context.Context, sessionID, path string) er
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *remoteStorage) Abs(ctx context.Context, sessionID, path string) (string, error) {
|
||||
resp, err := r.client.Abs(ctx, &pb.FSRequest{
|
||||
SessionId: sessionID,
|
||||
Path: path,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Path, nil
|
||||
}
|
||||
|
||||
// SyncPush sends local files to Tai using the manifest-first bidi streaming protocol.
|
||||
func (r *remoteStorage) SyncPush(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error) {
|
||||
start := time.Now()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type Volume interface {
|
|||
Remove(ctx context.Context, sessionID, path string, recursive bool) error
|
||||
Rename(ctx context.Context, sessionID, oldPath, newPath string) error
|
||||
MkdirAll(ctx context.Context, sessionID, path string) error
|
||||
Abs(ctx context.Context, sessionID, path string) (string, error)
|
||||
|
||||
SyncPush(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error)
|
||||
SyncPull(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error)
|
||||
|
|
|
|||
|
|
@ -560,10 +560,83 @@ func TestRemoteVolume(t *testing.T) {
|
|||
_ = vol.Remove(ctx, arcSid, ".", true)
|
||||
})
|
||||
|
||||
t.Run("Abs dot", func(t *testing.T) {
|
||||
absSid := "abs-remote-test"
|
||||
_ = vol.MkdirAll(ctx, absSid, ".")
|
||||
got, err := vol.Abs(ctx, absSid, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("Abs: %v", err)
|
||||
}
|
||||
if got == "" {
|
||||
t.Error("Abs returned empty")
|
||||
}
|
||||
_ = vol.Remove(ctx, absSid, ".", true)
|
||||
})
|
||||
|
||||
t.Run("Abs relative", func(t *testing.T) {
|
||||
got, err := vol.Abs(ctx, sid, "sub/file.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Abs: %v", err)
|
||||
}
|
||||
if got == "" {
|
||||
t.Error("Abs returned empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Abs path traversal", func(t *testing.T) {
|
||||
_, err := vol.Abs(ctx, sid, "../../etc/passwd")
|
||||
if err == nil {
|
||||
t.Error("expected error for Abs path traversal")
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
_ = vol.Remove(ctx, sid, ".", true)
|
||||
}
|
||||
|
||||
func TestLocalAbs_Dot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
vol := NewLocal(dir)
|
||||
ctx := context.Background()
|
||||
sid := "abs-test"
|
||||
|
||||
got, err := vol.Abs(ctx, sid, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("Abs: %v", err)
|
||||
}
|
||||
want := dir + "/" + sid
|
||||
if got != want {
|
||||
t.Errorf("Abs(\".\") = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalAbs_RelativePath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
vol := NewLocal(dir)
|
||||
ctx := context.Background()
|
||||
sid := "abs-rel"
|
||||
|
||||
got, err := vol.Abs(ctx, sid, "sub/file.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Abs: %v", err)
|
||||
}
|
||||
want := dir + "/" + sid + "/sub/file.txt"
|
||||
if got != want {
|
||||
t.Errorf("Abs = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalAbs_PathTraversal(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
vol := NewLocal(dir)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := vol.Abs(ctx, "test", "../../etc/passwd")
|
||||
if err == nil {
|
||||
t.Error("expected error for path traversal in Abs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalPathTraversal(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
vol := NewLocal(dir)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ type FS interface {
|
|||
// ws↔ws uses Volume.Copy (server-side for remote volumes, avoiding 2N network round-trips).
|
||||
// Returns non-nil *SyncResult for host↔workspace and ws↔ws transfers; nil for host↔host.
|
||||
Copy(src, dst string, opts ...volume.SyncOption) (*volume.SyncResult, error)
|
||||
|
||||
// GetRoot returns the absolute path of this workspace's root directory on the host filesystem.
|
||||
GetRoot() (string, error)
|
||||
}
|
||||
|
||||
// New creates an FS backed by the given Volume for the specified session.
|
||||
|
|
@ -119,6 +122,10 @@ func (w *workspaceFS) MkdirAll(name string, _ os.FileMode) error {
|
|||
return w.vol.MkdirAll(context.Background(), w.session, name)
|
||||
}
|
||||
|
||||
func (w *workspaceFS) GetRoot() (string, error) {
|
||||
return w.vol.Abs(context.Background(), w.session, ".")
|
||||
}
|
||||
|
||||
func (w *workspaceFS) Close() error { return nil }
|
||||
|
||||
// --- fs.FileInfo adapter ---
|
||||
|
|
|
|||
|
|
@ -210,6 +210,25 @@ func TestWorkspaceFS(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetRoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
vol := volume.NewLocal(dir)
|
||||
defer vol.Close()
|
||||
|
||||
sid := "getroot-test"
|
||||
wfs := New(vol, sid)
|
||||
defer wfs.Close()
|
||||
|
||||
root, err := wfs.GetRoot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRoot: %v", err)
|
||||
}
|
||||
want := dir + "/" + sid
|
||||
if root != want {
|
||||
t.Errorf("GetRoot() = %q, want %q", root, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ fs.FS = (*workspaceFS)(nil)
|
||||
|
|
|
|||
|
|
@ -275,20 +275,7 @@ func (m *Manager) MountPath(ctx context.Context, id string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = vol
|
||||
for _, snap := range listNodes() {
|
||||
res, ok := tai.GetResources(snap.TaiID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if res.Volume == vol {
|
||||
if res.DataDir == "" {
|
||||
return "", nil
|
||||
}
|
||||
return res.DataDir + "/" + id, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
return vol.Abs(ctx, id, ".")
|
||||
}
|
||||
|
||||
// --- internal ---
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue