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
|
// Execute Create Hook
|
||||||
// ================================================
|
// ================================================
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package assistant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ import (
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
infraV2 "github.com/yaoapp/yao/sandbox/v2"
|
infraV2 "github.com/yaoapp/yao/sandbox/v2"
|
||||||
traceTypes "github.com/yaoapp/yao/trace/types"
|
traceTypes "github.com/yaoapp/yao/trace/types"
|
||||||
|
"github.com/yaoapp/yao/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HasSandboxV2 returns true if the assistant has a V2 sandbox configuration.
|
// 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)
|
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) {
|
func closeLoadingV2(ctx *context.Context, loadingMsgID, msgKey string) {
|
||||||
if loadingMsgID == "" || ctx == nil {
|
if loadingMsgID == "" || ctx == nil {
|
||||||
return
|
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.
|
// GetComputer returns the V2 computer if available.
|
||||||
func (ctx *Context) GetComputer() infraV2.Computer {
|
func (ctx *Context) GetComputer() infraV2.Computer {
|
||||||
return ctx.computer
|
return ctx.computer
|
||||||
|
|
@ -35,6 +41,11 @@ func (ctx *Context) HasComputer() bool {
|
||||||
return ctx.computer != nil
|
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.
|
// createComputerInstance creates the ctx.computer JavaScript object.
|
||||||
func (ctx *Context) createComputerInstance(v8ctx *v8go.Context) *v8go.Value {
|
func (ctx *Context) createComputerInstance(v8ctx *v8go.Context) *v8go.Value {
|
||||||
if ctx.computer == nil {
|
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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -14,12 +15,7 @@ import (
|
||||||
infra "github.com/yaoapp/yao/sandbox/v2"
|
infra "github.com/yaoapp/yao/sandbox/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const defaultProxyPort = 3456
|
||||||
defaultWorkDir = "/workspace"
|
|
||||||
defaultUser = "sandbox"
|
|
||||||
defaultUserHome = "/home/sandbox"
|
|
||||||
defaultProxyPort = 3456
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClaudeRunner implements the Runner interface for Claude CLI (mode=cli).
|
// ClaudeRunner implements the Runner interface for Claude CLI (mode=cli).
|
||||||
type ClaudeRunner struct {
|
type ClaudeRunner struct {
|
||||||
|
|
@ -45,33 +41,30 @@ func (r *ClaudeRunner) Prepare(ctx context.Context, req *types.PrepareRequest) e
|
||||||
r.mode = "cli"
|
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...)
|
steps := append([]types.PrepareStep{}, req.Config.Prepare...)
|
||||||
|
|
||||||
// Runner-specific: ensure .claude directory in workDir.
|
|
||||||
if req.SkillsDir != "" {
|
if req.SkillsDir != "" {
|
||||||
|
claudeDir := env.pathJoin(env.WorkDir, ".claude")
|
||||||
steps = append(steps, types.PrepareStep{
|
steps = append(steps, types.PrepareStep{
|
||||||
Action: "exec",
|
Action: "exec",
|
||||||
Cmd: fmt.Sprintf("mkdir -p %s/.claude", workDir),
|
Cmd: env.mkdirCmd(claudeDir),
|
||||||
Once: true,
|
Once: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runner-specific: write MCP config.
|
|
||||||
if len(req.MCPServers) > 0 {
|
if len(req.MCPServers) > 0 {
|
||||||
r.hasMCP = true
|
r.hasMCP = true
|
||||||
r.mcpToolPattern = buildMCPAllowedTools(req.MCPServers)
|
r.mcpToolPattern = buildMCPAllowedTools(req.MCPServers)
|
||||||
mcpJSON := buildMCPConfig(req.MCPServers)
|
mcpJSON := buildMCPConfig(req.MCPServers)
|
||||||
steps = append(steps, types.PrepareStep{
|
steps = append(steps, types.PrepareStep{
|
||||||
Action: "file",
|
Action: "file",
|
||||||
Path: path.Join(workDir, ".mcp.json"),
|
Path: env.pathJoin(env.WorkDir, ".mcp.json"),
|
||||||
Content: mcpJSON,
|
Content: mcpJSON,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute all steps via the injected callback.
|
|
||||||
if req.RunSteps != nil && len(steps) > 0 {
|
if req.RunSteps != nil && len(steps) > 0 {
|
||||||
if err := req.RunSteps(ctx, steps, req.Computer, req.Config.ID, req.ConfigHash); err != nil {
|
if err := req.RunSteps(ctx, steps, req.Computer, req.Config.ID, req.ConfigHash); err != nil {
|
||||||
return fmt.Errorf("claude prepare steps: %w", err)
|
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")
|
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 != "" {
|
if req.ChatID != "" {
|
||||||
ws := computer.Workplace()
|
ws := computer.Workplace()
|
||||||
if ws != nil {
|
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, oe)
|
||||||
isContinuation := hasExistingSession(ctx, computer, workDir)
|
|
||||||
|
|
||||||
// Build CLI command and env.
|
cmd, env, stdin := r.buildCLICommand(req, oe, isContinuation)
|
||||||
cmd, env := r.buildCLICommand(req, isContinuation)
|
|
||||||
|
|
||||||
// Create stream.
|
streamOpts := []infra.ExecOption{infra.WithWorkDir(oe.WorkDir), infra.WithEnv(env)}
|
||||||
execStream, err := computer.Stream(ctx, cmd, infra.WithWorkDir(workDir), infra.WithEnv(env))
|
if len(stdin) > 0 {
|
||||||
|
streamOpts = append(streamOpts, infra.WithStdin(stdin))
|
||||||
|
}
|
||||||
|
|
||||||
|
execStream, err := computer.Stream(ctx, cmd, streamOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("computer.Stream: %w", err)
|
return fmt.Errorf("computer.Stream: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitor for context cancellation — kill the process.
|
streamCtx, streamCancel := context.WithCancel(ctx)
|
||||||
done := make(chan struct{})
|
defer streamCancel()
|
||||||
defer func() {
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
<-streamCtx.Done()
|
||||||
case <-ctx.Done():
|
killCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
killCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
defer cancel()
|
||||||
defer cancel()
|
computer.Exec(killCtx, oe.killProcessCmd("claude"))
|
||||||
computer.Exec(killCtx, []string{"pkill", "-f", "claude"})
|
execStream.Cancel()
|
||||||
execStream.Cancel()
|
}()
|
||||||
case <-done:
|
|
||||||
|
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(streamCtx, execStream.Stdout, handler)
|
||||||
parseErr := parseStreamJSON(ctx, execStream.Stdout, handler)
|
|
||||||
|
|
||||||
// Wait for process exit.
|
|
||||||
exitCode, waitErr := execStream.Wait()
|
exitCode, waitErr := execStream.Wait()
|
||||||
|
stderrStr := strings.TrimSpace(stderrBuf.String())
|
||||||
|
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
|
if stderrStr != "" {
|
||||||
|
return fmt.Errorf("%w (stderr: %s)", parseErr, stderrStr)
|
||||||
|
}
|
||||||
return parseErr
|
return parseErr
|
||||||
}
|
}
|
||||||
if waitErr != nil {
|
if waitErr != nil {
|
||||||
|
if stderrStr != "" {
|
||||||
|
return fmt.Errorf("%w (stderr: %s)", waitErr, stderrStr)
|
||||||
|
}
|
||||||
return waitErr
|
return waitErr
|
||||||
}
|
}
|
||||||
if exitCode != 0 {
|
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 fmt.Errorf("claude CLI exited with code %d", exitCode)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup kills any remaining claude processes.
|
// Cleanup kills any remaining claude processes.
|
||||||
// mode=cli: kill all claude CLI processes.
|
|
||||||
func (r *ClaudeRunner) Cleanup(ctx context.Context, computer infra.Computer) error {
|
func (r *ClaudeRunner) Cleanup(ctx context.Context, computer infra.Computer) error {
|
||||||
if computer == nil {
|
if computer == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.mode != "service" {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasExistingSession checks if a Claude CLI session exists in the workspace.
|
// hasExistingSession checks if a Claude CLI session exists in the workspace.
|
||||||
func hasExistingSession(ctx context.Context, computer infra.Computer, workDir string) bool {
|
func hasExistingSession(ctx context.Context, computer infra.Computer, oe *osEnv) bool {
|
||||||
sessionDir := path.Join(workDir, ".claude/projects")
|
sessionDir := oe.pathJoin(oe.WorkDir, ".claude", "projects")
|
||||||
result, err := computer.Exec(ctx, []string{"ls", sessionDir})
|
result, err := computer.Exec(ctx, oe.listDirCmd(sessionDir))
|
||||||
if err != nil || result.ExitCode != 0 {
|
if err != nil || result.ExitCode != 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(result.Stdout) != ""
|
return strings.TrimSpace(result.Stdout) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildCLICommand constructs the Claude CLI command and environment variables.
|
// buildCLICommand constructs the Claude CLI command, environment variables, and optional stdin bytes.
|
||||||
func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation bool) ([]string, map[string]string) {
|
func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, oe *osEnv, isContinuation bool) ([]string, map[string]string, []byte) {
|
||||||
workDir := resolveWorkDir(req.Config)
|
|
||||||
userHome := resolveUserHome(req.Config)
|
|
||||||
|
|
||||||
env := make(map[string]string)
|
env := make(map[string]string)
|
||||||
env["HOME"] = workDir
|
|
||||||
|
|
||||||
// User-specific paths (only set when running as non-root user inside container).
|
if oe.isWindows() {
|
||||||
if userHome != "" {
|
env["USERPROFILE"] = oe.WorkDir
|
||||||
env["XAUTHORITY"] = path.Join(userHome, ".Xauthority")
|
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 {
|
if req.Connector != nil {
|
||||||
setting := req.Connector.Setting()
|
setting := req.Connector.Setting()
|
||||||
host, _ := setting["host"].(string)
|
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 {
|
if req.Config != nil && len(req.Config.Secrets) > 0 {
|
||||||
for k, v := range req.Config.Secrets {
|
for k, v := range req.Config.Secrets {
|
||||||
env[k] = v
|
env[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build system prompt.
|
|
||||||
var systemPrompt string
|
var systemPrompt string
|
||||||
envPrompt := buildSandboxEnvPrompt(workDir)
|
envPrompt := buildSandboxEnvPrompt(oe.WorkDir)
|
||||||
if !isContinuation && req.SystemPrompt != "" {
|
if !isContinuation && req.SystemPrompt != "" {
|
||||||
systemPrompt = req.SystemPrompt + "\n\n" + envPrompt
|
systemPrompt = req.SystemPrompt + "\n\n" + envPrompt
|
||||||
} else if !isContinuation {
|
} else if !isContinuation {
|
||||||
systemPrompt = envPrompt
|
systemPrompt = envPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build input JSONL.
|
|
||||||
var inputJSONL string
|
var inputJSONL string
|
||||||
if isContinuation {
|
if isContinuation {
|
||||||
inputJSONL = buildLastUserMessageJSONL(req.Messages)
|
inputJSONL = buildLastUserMessageJSONL(req.Messages)
|
||||||
|
|
@ -233,10 +250,19 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation
|
||||||
inputJSONL = buildFirstRequestJSONL(req.Messages)
|
inputJSONL = buildFirstRequestJSONL(req.Messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI args.
|
|
||||||
var args []string
|
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, "--input-format", "stream-json")
|
||||||
args = append(args, "--output-format", "stream-json")
|
args = append(args, "--output-format", "stream-json")
|
||||||
args = append(args, "--include-partial-messages")
|
args = append(args, "--include-partial-messages")
|
||||||
|
|
@ -246,7 +272,6 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation
|
||||||
args = append(args, "--continue")
|
args = append(args, "--continue")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runner options pass-through.
|
|
||||||
if req.Config != nil && req.Config.Runner.Options != nil {
|
if req.Config != nil && req.Config.Runner.Options != nil {
|
||||||
for key, val := range req.Config.Runner.Options {
|
for key, val := range req.Config.Runner.Options {
|
||||||
if flag, ok := claudeArgWhitelist[key]; ok {
|
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 {
|
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 != "" {
|
if r.mcpToolPattern != "" {
|
||||||
args = append(args, "--allowedTools", r.mcpToolPattern)
|
args = append(args, "--allowedTools", r.mcpToolPattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build bash command with heredoc.
|
script, stdin := oe.buildCLIScript(args, systemPrompt, inputJSONL)
|
||||||
var bash strings.Builder
|
return oe.shellCmd(script), env, stdin
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildMCPConfig creates the .mcp.json for Claude CLI based on declared servers.
|
// 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)
|
`, 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{
|
var claudeArgWhitelist = map[string]string{
|
||||||
"max_turns": "--max-turns",
|
"max_turns": "--max-turns",
|
||||||
"disallowed_tools": "--disallowed-tools",
|
"disallowed_tools": "--disallowed-tools",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import (
|
||||||
agentContext "github.com/yaoapp/yao/agent/context"
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
||||||
infra "github.com/yaoapp/yao/sandbox/v2"
|
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
|
// 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
|
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.
|
// Host mode: no image → host computer.
|
||||||
if cfg.Computer.Image == "" {
|
if cfg.Computer.Image == "" {
|
||||||
cfg.Kind = "host"
|
cfg.Kind = "host"
|
||||||
|
|
@ -75,6 +169,15 @@ func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *i
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Kind = "box"
|
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.
|
// Reuse: non-empty identifier → try Get first.
|
||||||
if identifier != "" {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +160,10 @@ func runExecStep(ctx context.Context, computer infra.Computer, step types.Prepar
|
||||||
return checkResult(result, label)
|
return checkResult(result, label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isWindowsComputer(computer infra.Computer) bool {
|
||||||
|
return strings.EqualFold(computer.ComputerInfo().System.OS, "windows")
|
||||||
|
}
|
||||||
|
|
||||||
// checkResult inspects ExecResult for errors.
|
// checkResult inspects ExecResult for errors.
|
||||||
func checkResult(result *infra.ExecResult, label string) error {
|
func checkResult(result *infra.ExecResult, label string) error {
|
||||||
if result.Error != "" {
|
if result.Error != "" {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import (
|
||||||
// - DELETE /:id/files/*path — delete file
|
// - DELETE /:id/files/*path — delete file
|
||||||
// - POST /:id/mkdir — create directory
|
// - POST /:id/mkdir — create directory
|
||||||
// - POST /:id/rename — rename file/directory
|
// - POST /:id/rename — rename file/directory
|
||||||
|
// - GET /:id/rootdir — get workspace root directory absolute path
|
||||||
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
group.Use(oauth.Guard)
|
group.Use(oauth.Guard)
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
group.PUT("/:id", handleUpdate)
|
group.PUT("/:id", handleUpdate)
|
||||||
group.DELETE("/:id", handleDelete)
|
group.DELETE("/:id", handleDelete)
|
||||||
|
|
||||||
|
group.GET("/:id/rootdir", handleRootDir)
|
||||||
group.GET("/:id/files", handleListFiles)
|
group.GET("/:id/files", handleListFiles)
|
||||||
group.GET("/:id/files/*path", handleReadFile)
|
group.GET("/:id/files/*path", handleReadFile)
|
||||||
group.PUT("/:id/files/*path", handleWriteFile)
|
group.PUT("/:id/files/*path", handleWriteFile)
|
||||||
|
|
@ -285,6 +287,21 @@ func handleDelete(c *gin.Context) {
|
||||||
c.Status(http.StatusNoContent)
|
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) {
|
func handleListFiles(c *gin.Context) {
|
||||||
_, ok := resolveAndCheckWS(c)
|
_, ok := resolveAndCheckWS(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ type Box struct {
|
||||||
image string
|
image string
|
||||||
workspaceID string
|
workspaceID string
|
||||||
system SystemInfo
|
system SystemInfo
|
||||||
|
workDir string
|
||||||
ws taiworkspace.FS
|
ws taiworkspace.FS
|
||||||
manager *Manager
|
manager *Manager
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +196,14 @@ func (b *Box) Workspace() taiworkspace.FS {
|
||||||
return b.ws
|
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.
|
// WorkspaceID returns the workspace ID mounted to this sandbox, or empty string.
|
||||||
func (b *Box) WorkspaceID() string { return b.workspaceID }
|
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)
|
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.
|
// NodeID returns the node ID this Host belongs to.
|
||||||
func (h *Host) NodeID() string { return h.nodeID }
|
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,
|
TempDir: res.System.TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boxWorkDir := opts.WorkDir
|
||||||
|
if boxWorkDir == "" {
|
||||||
|
boxWorkDir = "/workspace"
|
||||||
|
}
|
||||||
|
|
||||||
box := &Box{
|
box := &Box{
|
||||||
id: id,
|
id: id,
|
||||||
containerID: containerID,
|
containerID: containerID,
|
||||||
|
|
@ -214,6 +219,7 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
|
||||||
vnc: opts.VNC,
|
vnc: opts.VNC,
|
||||||
image: opts.Image,
|
image: opts.Image,
|
||||||
workspaceID: opts.WorkspaceID,
|
workspaceID: opts.WorkspaceID,
|
||||||
|
workDir: boxWorkDir,
|
||||||
system: sys,
|
system: sys,
|
||||||
}
|
}
|
||||||
box.lastCall.Store(time.Now().UnixMilli())
|
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(),
|
createdAt: time.Now(),
|
||||||
image: c.Image,
|
image: c.Image,
|
||||||
workspaceID: c.Labels["workspace-id"],
|
workspaceID: c.Labels["workspace-id"],
|
||||||
|
workDir: "/workspace",
|
||||||
manager: m,
|
manager: m,
|
||||||
}
|
}
|
||||||
box.lastCall.Store(time.Now().UnixMilli())
|
box.lastCall.Store(time.Now().UnixMilli())
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type Computer interface {
|
||||||
Proxy(ctx context.Context, port int, path string) (string, error)
|
Proxy(ctx context.Context, port int, path string) (string, error)
|
||||||
BindWorkplace(workspaceID string)
|
BindWorkplace(workspaceID string)
|
||||||
Workplace() workspace.FS
|
Workplace() workspace.FS
|
||||||
|
GetWorkDir() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComputerInfo holds identity and registry information for a Computer.
|
// ComputerInfo holds identity and registry information for a Computer.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ type TunnelHandler struct {
|
||||||
reg *registry.Registry
|
reg *registry.Registry
|
||||||
pending sync.Map // channel_id → chan taipb.TaiTunnel_ForwardServer
|
pending sync.Map // channel_id → chan taipb.TaiTunnel_ForwardServer
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
sendMu sync.Map // taiID → *sync.Mutex – serializes Send on each Register stream
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTunnelHandler creates a TunnelHandler backed by the given registry.
|
// 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),
|
Capabilities: capsFromProto(msg.Caps),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
h.sendMu.Store(resolvedTaiID, &mu)
|
||||||
|
|
||||||
h.reg.Register(node)
|
h.reg.Register(node)
|
||||||
h.reg.SetRegisterStream(resolvedTaiID, stream)
|
h.reg.SetRegisterStream(resolvedTaiID, stream)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
h.sendMu.Delete(resolvedTaiID)
|
||||||
h.reg.Unregister(resolvedTaiID)
|
h.reg.Unregister(resolvedTaiID)
|
||||||
h.logger.Info("tai gRPC tunnel disconnected", "tai_id", 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",
|
Type: "registered",
|
||||||
TaiId: resolvedTaiID,
|
TaiId: resolvedTaiID,
|
||||||
}); err != nil {
|
})
|
||||||
|
mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("send registered: %w", err)
|
return fmt.Errorf("send registered: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,8 +142,11 @@ func (h *TunnelHandler) Register(stream taipb.TaiTunnel_RegisterServer) error {
|
||||||
switch ctrl.Type {
|
switch ctrl.Type {
|
||||||
case "ping":
|
case "ping":
|
||||||
h.reg.UpdatePing(resolvedTaiID)
|
h.reg.UpdatePing(resolvedTaiID)
|
||||||
if err := stream.Send(&taipb.TunnelControl{Type: "pong"}); err != nil {
|
mu.Lock()
|
||||||
return err
|
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]
|
channelID := vals[0]
|
||||||
|
|
||||||
|
h.logger.Debug("[forward] Forward stream arrived", "channel_id", channelID[:16])
|
||||||
|
|
||||||
if ch, ok := h.pending.LoadAndDelete(channelID); ok {
|
if ch, ok := h.pending.LoadAndDelete(channelID); ok {
|
||||||
ch.(chan taipb.TaiTunnel_ForwardServer) <- stream
|
ch.(chan taipb.TaiTunnel_ForwardServer) <- stream
|
||||||
} else {
|
} else {
|
||||||
|
h.logger.Warn("[forward] no pending channel (expired?)", "channel_id", channelID[:16])
|
||||||
return fmt.Errorf("no pending channel for %s", channelID)
|
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)
|
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()
|
channelID, err := registry.GenerateChannelID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generate channel_id: %w", err)
|
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 {
|
if !ok {
|
||||||
return nil, fmt.Errorf("tai %s: register stream type mismatch", taiID)
|
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",
|
Type: "open",
|
||||||
ChannelId: channelID,
|
ChannelId: channelID,
|
||||||
TargetPort: int32(targetPort),
|
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 {
|
select {
|
||||||
case fwd := <-waitCh:
|
case fwd := <-waitCh:
|
||||||
|
h.logger.Debug("[forward] callback received",
|
||||||
|
"tai_id", taiID, "channel_id", channelID[:16])
|
||||||
return fwd, nil
|
return fwd, nil
|
||||||
case <-time.After(10 * time.Second):
|
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():
|
case <-regStream.Context().Done():
|
||||||
return nil, fmt.Errorf("tai %s: register stream closed while waiting for forward", taiID)
|
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)
|
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.
|
// Copy duplicates src to dst within the same workspace session.
|
||||||
// Supports single files and directories (recursive). Uses excludes from SyncOption
|
// Supports single files and directories (recursive). Uses excludes from SyncOption
|
||||||
// and forceFull to overwrite even when mtime+size match.
|
// 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
|
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) {
|
func (m *mockVolumeServer) ListDir(_ context.Context, req *pb.FSRequest) (*pb.FSListResponse, error) {
|
||||||
return &pb.FSListResponse{Entries: []*pb.FileInfo{
|
return &pb.FSListResponse{Entries: []*pb.FileInfo{
|
||||||
{Path: "a.txt", Size: 10},
|
{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")
|
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()) {
|
func startErrMockServer(t *testing.T) (*grpc.ClientConn, func()) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
|
@ -748,3 +756,42 @@ func TestErrRemoteCopy(t *testing.T) {
|
||||||
t.Error("expected error")
|
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 ""
|
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 {
|
type FSReadRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
|
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() {
|
func (x *FSReadRequest) Reset() {
|
||||||
*x = FSReadRequest{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -653,7 +697,7 @@ func (x *FSReadRequest) String() string {
|
||||||
func (*FSReadRequest) ProtoMessage() {}
|
func (*FSReadRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSReadRequest) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -666,7 +710,7 @@ func (x *FSReadRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSReadRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSReadRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*FSReadRequest) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSReadRequest) GetSessionId() string {
|
||||||
|
|
@ -695,7 +739,7 @@ type FSDataChunk struct {
|
||||||
|
|
||||||
func (x *FSDataChunk) Reset() {
|
func (x *FSDataChunk) Reset() {
|
||||||
*x = FSDataChunk{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -707,7 +751,7 @@ func (x *FSDataChunk) String() string {
|
||||||
func (*FSDataChunk) ProtoMessage() {}
|
func (*FSDataChunk) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSDataChunk) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -720,7 +764,7 @@ func (x *FSDataChunk) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSDataChunk.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSDataChunk.ProtoReflect.Descriptor instead.
|
||||||
func (*FSDataChunk) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSDataChunk) GetData() []byte {
|
||||||
|
|
@ -764,7 +808,7 @@ type FSWriteChunk struct {
|
||||||
|
|
||||||
func (x *FSWriteChunk) Reset() {
|
func (x *FSWriteChunk) Reset() {
|
||||||
*x = FSWriteChunk{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -776,7 +820,7 @@ func (x *FSWriteChunk) String() string {
|
||||||
func (*FSWriteChunk) ProtoMessage() {}
|
func (*FSWriteChunk) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSWriteChunk) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -789,7 +833,7 @@ func (x *FSWriteChunk) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSWriteChunk.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSWriteChunk.ProtoReflect.Descriptor instead.
|
||||||
func (*FSWriteChunk) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSWriteChunk) GetSessionId() string {
|
||||||
|
|
@ -836,7 +880,7 @@ type FSWriteResponse struct {
|
||||||
|
|
||||||
func (x *FSWriteResponse) Reset() {
|
func (x *FSWriteResponse) Reset() {
|
||||||
*x = FSWriteResponse{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -848,7 +892,7 @@ func (x *FSWriteResponse) String() string {
|
||||||
func (*FSWriteResponse) ProtoMessage() {}
|
func (*FSWriteResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSWriteResponse) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -861,7 +905,7 @@ func (x *FSWriteResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSWriteResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSWriteResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*FSWriteResponse) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSWriteResponse) GetSize() int64 {
|
||||||
|
|
@ -880,7 +924,7 @@ type FSListResponse struct {
|
||||||
|
|
||||||
func (x *FSListResponse) Reset() {
|
func (x *FSListResponse) Reset() {
|
||||||
*x = FSListResponse{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -892,7 +936,7 @@ func (x *FSListResponse) String() string {
|
||||||
func (*FSListResponse) ProtoMessage() {}
|
func (*FSListResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSListResponse) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -905,7 +949,7 @@ func (x *FSListResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSListResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSListResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*FSListResponse) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSListResponse) GetEntries() []*FileInfo {
|
||||||
|
|
@ -926,7 +970,7 @@ type FSRemoveRequest struct {
|
||||||
|
|
||||||
func (x *FSRemoveRequest) Reset() {
|
func (x *FSRemoveRequest) Reset() {
|
||||||
*x = FSRemoveRequest{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -938,7 +982,7 @@ func (x *FSRemoveRequest) String() string {
|
||||||
func (*FSRemoveRequest) ProtoMessage() {}
|
func (*FSRemoveRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSRemoveRequest) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -951,7 +995,7 @@ func (x *FSRemoveRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSRemoveRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSRemoveRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*FSRemoveRequest) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSRemoveRequest) GetSessionId() string {
|
||||||
|
|
@ -986,7 +1030,7 @@ type FSRenameRequest struct {
|
||||||
|
|
||||||
func (x *FSRenameRequest) Reset() {
|
func (x *FSRenameRequest) Reset() {
|
||||||
*x = FSRenameRequest{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -998,7 +1042,7 @@ func (x *FSRenameRequest) String() string {
|
||||||
func (*FSRenameRequest) ProtoMessage() {}
|
func (*FSRenameRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSRenameRequest) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -1011,7 +1055,7 @@ func (x *FSRenameRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSRenameRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSRenameRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*FSRenameRequest) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSRenameRequest) GetSessionId() string {
|
||||||
|
|
@ -1048,7 +1092,7 @@ type FSCopyRequest struct {
|
||||||
|
|
||||||
func (x *FSCopyRequest) Reset() {
|
func (x *FSCopyRequest) Reset() {
|
||||||
*x = FSCopyRequest{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -1060,7 +1104,7 @@ func (x *FSCopyRequest) String() string {
|
||||||
func (*FSCopyRequest) ProtoMessage() {}
|
func (*FSCopyRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *FSCopyRequest) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -1073,7 +1117,7 @@ func (x *FSCopyRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use FSCopyRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use FSCopyRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*FSCopyRequest) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *FSCopyRequest) GetSessionId() string {
|
||||||
|
|
@ -1123,7 +1167,7 @@ type ArchiveRequest struct {
|
||||||
|
|
||||||
func (x *ArchiveRequest) Reset() {
|
func (x *ArchiveRequest) Reset() {
|
||||||
*x = ArchiveRequest{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -1135,7 +1179,7 @@ func (x *ArchiveRequest) String() string {
|
||||||
func (*ArchiveRequest) ProtoMessage() {}
|
func (*ArchiveRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ArchiveRequest) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -1148,7 +1192,7 @@ func (x *ArchiveRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use ArchiveRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ArchiveRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*ArchiveRequest) Descriptor() ([]byte, []int) {
|
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 {
|
func (x *ArchiveRequest) GetSessionId() string {
|
||||||
|
|
@ -1189,7 +1233,7 @@ type ArchiveResponse struct {
|
||||||
|
|
||||||
func (x *ArchiveResponse) Reset() {
|
func (x *ArchiveResponse) Reset() {
|
||||||
*x = ArchiveResponse{}
|
*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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -1201,7 +1245,7 @@ func (x *ArchiveResponse) String() string {
|
||||||
func (*ArchiveResponse) ProtoMessage() {}
|
func (*ArchiveResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ArchiveResponse) ProtoReflect() protoreflect.Message {
|
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 {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -1214,7 +1258,7 @@ func (x *ArchiveResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use ArchiveResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ArchiveResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*ArchiveResponse) Descriptor() ([]byte, []int) {
|
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 {
|
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" +
|
"\x04path\x18\x02 \x01(\tR\x04path\"4\n" +
|
||||||
"\fFSOpResponse\x12\x0e\n" +
|
"\fFSOpResponse\x12\x0e\n" +
|
||||||
"\x02ok\x18\x01 \x01(\bR\x02ok\x12\x14\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" +
|
"\rFSReadRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"session_id\x18\x01 \x01(\tR\tsessionId\x12\x12\n" +
|
"session_id\x18\x01 \x01(\tR\tsessionId\x12\x12\n" +
|
||||||
|
|
@ -1334,7 +1380,7 @@ const file_tai_volume_pb_volume_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes\x12\x1f\n" +
|
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes\x12\x1f\n" +
|
||||||
"\vfiles_count\x18\x02 \x01(\x05R\n" +
|
"\vfiles_count\x18\x02 \x01(\x05R\n" +
|
||||||
"filesCount2\xfa\a\n" +
|
"filesCount2\xab\b\n" +
|
||||||
"\x06Volume\x128\n" +
|
"\x06Volume\x128\n" +
|
||||||
"\bSyncPush\x12\x13.volume.SyncMessage\x1a\x13.volume.SyncMessage(\x010\x01\x127\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" +
|
"\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" +
|
"\aListDir\x12\x11.volume.FSRequest\x1a\x16.volume.FSListResponse\x127\n" +
|
||||||
"\x06Remove\x12\x17.volume.FSRemoveRequest\x1a\x14.volume.FSOpResponse\x127\n" +
|
"\x06Remove\x12\x17.volume.FSRemoveRequest\x1a\x14.volume.FSOpResponse\x127\n" +
|
||||||
"\x06Rename\x12\x17.volume.FSRenameRequest\x1a\x14.volume.FSOpResponse\x123\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" +
|
"\x04Copy\x12\x15.volume.FSCopyRequest\x1a\x12.volume.SyncResult\x126\n" +
|
||||||
"\x03Zip\x12\x16.volume.ArchiveRequest\x1a\x17.volume.ArchiveResponse\x128\n" +
|
"\x03Zip\x12\x16.volume.ArchiveRequest\x1a\x17.volume.ArchiveResponse\x128\n" +
|
||||||
"\x05Unzip\x12\x16.volume.ArchiveRequest\x1a\x17.volume.ArchiveResponse\x127\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_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{
|
var file_tai_volume_pb_volume_proto_goTypes = []any{
|
||||||
(FileChunk_ChunkType)(0), // 0: volume.FileChunk.ChunkType
|
(FileChunk_ChunkType)(0), // 0: volume.FileChunk.ChunkType
|
||||||
(*FileInfo)(nil), // 1: volume.FileInfo
|
(*FileInfo)(nil), // 1: volume.FileInfo
|
||||||
|
|
@ -1379,16 +1426,17 @@ var file_tai_volume_pb_volume_proto_goTypes = []any{
|
||||||
(*SyncResult)(nil), // 6: volume.SyncResult
|
(*SyncResult)(nil), // 6: volume.SyncResult
|
||||||
(*FSRequest)(nil), // 7: volume.FSRequest
|
(*FSRequest)(nil), // 7: volume.FSRequest
|
||||||
(*FSOpResponse)(nil), // 8: volume.FSOpResponse
|
(*FSOpResponse)(nil), // 8: volume.FSOpResponse
|
||||||
(*FSReadRequest)(nil), // 9: volume.FSReadRequest
|
(*FSAbsResponse)(nil), // 9: volume.FSAbsResponse
|
||||||
(*FSDataChunk)(nil), // 10: volume.FSDataChunk
|
(*FSReadRequest)(nil), // 10: volume.FSReadRequest
|
||||||
(*FSWriteChunk)(nil), // 11: volume.FSWriteChunk
|
(*FSDataChunk)(nil), // 11: volume.FSDataChunk
|
||||||
(*FSWriteResponse)(nil), // 12: volume.FSWriteResponse
|
(*FSWriteChunk)(nil), // 12: volume.FSWriteChunk
|
||||||
(*FSListResponse)(nil), // 13: volume.FSListResponse
|
(*FSWriteResponse)(nil), // 13: volume.FSWriteResponse
|
||||||
(*FSRemoveRequest)(nil), // 14: volume.FSRemoveRequest
|
(*FSListResponse)(nil), // 14: volume.FSListResponse
|
||||||
(*FSRenameRequest)(nil), // 15: volume.FSRenameRequest
|
(*FSRemoveRequest)(nil), // 15: volume.FSRemoveRequest
|
||||||
(*FSCopyRequest)(nil), // 16: volume.FSCopyRequest
|
(*FSRenameRequest)(nil), // 16: volume.FSRenameRequest
|
||||||
(*ArchiveRequest)(nil), // 17: volume.ArchiveRequest
|
(*FSCopyRequest)(nil), // 17: volume.FSCopyRequest
|
||||||
(*ArchiveResponse)(nil), // 18: volume.ArchiveResponse
|
(*ArchiveRequest)(nil), // 18: volume.ArchiveRequest
|
||||||
|
(*ArchiveResponse)(nil), // 19: volume.ArchiveResponse
|
||||||
}
|
}
|
||||||
var file_tai_volume_pb_volume_proto_depIdxs = []int32{
|
var file_tai_volume_pb_volume_proto_depIdxs = []int32{
|
||||||
1, // 0: volume.SyncManifest.files:type_name -> volume.FileInfo
|
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
|
1, // 6: volume.FSListResponse.entries:type_name -> volume.FileInfo
|
||||||
3, // 7: volume.Volume.SyncPush:input_type -> volume.SyncMessage
|
3, // 7: volume.Volume.SyncPush:input_type -> volume.SyncMessage
|
||||||
2, // 8: volume.Volume.SyncPull:input_type -> volume.SyncManifest
|
2, // 8: volume.Volume.SyncPull:input_type -> volume.SyncManifest
|
||||||
9, // 9: volume.Volume.ReadFile:input_type -> volume.FSReadRequest
|
10, // 9: volume.Volume.ReadFile:input_type -> volume.FSReadRequest
|
||||||
11, // 10: volume.Volume.WriteFile:input_type -> volume.FSWriteChunk
|
12, // 10: volume.Volume.WriteFile:input_type -> volume.FSWriteChunk
|
||||||
7, // 11: volume.Volume.Stat:input_type -> volume.FSRequest
|
7, // 11: volume.Volume.Stat:input_type -> volume.FSRequest
|
||||||
7, // 12: volume.Volume.ListDir:input_type -> volume.FSRequest
|
7, // 12: volume.Volume.ListDir:input_type -> volume.FSRequest
|
||||||
14, // 13: volume.Volume.Remove:input_type -> volume.FSRemoveRequest
|
15, // 13: volume.Volume.Remove:input_type -> volume.FSRemoveRequest
|
||||||
15, // 14: volume.Volume.Rename:input_type -> volume.FSRenameRequest
|
16, // 14: volume.Volume.Rename:input_type -> volume.FSRenameRequest
|
||||||
7, // 15: volume.Volume.MkdirAll:input_type -> volume.FSRequest
|
7, // 15: volume.Volume.MkdirAll:input_type -> volume.FSRequest
|
||||||
16, // 16: volume.Volume.Copy:input_type -> volume.FSCopyRequest
|
7, // 16: volume.Volume.Abs:input_type -> volume.FSRequest
|
||||||
17, // 17: volume.Volume.Zip:input_type -> volume.ArchiveRequest
|
17, // 17: volume.Volume.Copy:input_type -> volume.FSCopyRequest
|
||||||
17, // 18: volume.Volume.Unzip:input_type -> volume.ArchiveRequest
|
18, // 18: volume.Volume.Zip:input_type -> volume.ArchiveRequest
|
||||||
17, // 19: volume.Volume.Gzip:input_type -> volume.ArchiveRequest
|
18, // 19: volume.Volume.Unzip:input_type -> volume.ArchiveRequest
|
||||||
17, // 20: volume.Volume.Gunzip:input_type -> volume.ArchiveRequest
|
18, // 20: volume.Volume.Gzip:input_type -> volume.ArchiveRequest
|
||||||
17, // 21: volume.Volume.Tar:input_type -> volume.ArchiveRequest
|
18, // 21: volume.Volume.Gunzip:input_type -> volume.ArchiveRequest
|
||||||
17, // 22: volume.Volume.Untar:input_type -> volume.ArchiveRequest
|
18, // 22: volume.Volume.Tar:input_type -> volume.ArchiveRequest
|
||||||
17, // 23: volume.Volume.Tgz:input_type -> volume.ArchiveRequest
|
18, // 23: volume.Volume.Untar:input_type -> volume.ArchiveRequest
|
||||||
17, // 24: volume.Volume.Untgz:input_type -> volume.ArchiveRequest
|
18, // 24: volume.Volume.Tgz:input_type -> volume.ArchiveRequest
|
||||||
3, // 25: volume.Volume.SyncPush:output_type -> volume.SyncMessage
|
18, // 25: volume.Volume.Untgz:input_type -> volume.ArchiveRequest
|
||||||
3, // 26: volume.Volume.SyncPull:output_type -> volume.SyncMessage
|
3, // 26: volume.Volume.SyncPush:output_type -> volume.SyncMessage
|
||||||
10, // 27: volume.Volume.ReadFile:output_type -> volume.FSDataChunk
|
3, // 27: volume.Volume.SyncPull:output_type -> volume.SyncMessage
|
||||||
12, // 28: volume.Volume.WriteFile:output_type -> volume.FSWriteResponse
|
11, // 28: volume.Volume.ReadFile:output_type -> volume.FSDataChunk
|
||||||
1, // 29: volume.Volume.Stat:output_type -> volume.FileInfo
|
13, // 29: volume.Volume.WriteFile:output_type -> volume.FSWriteResponse
|
||||||
13, // 30: volume.Volume.ListDir:output_type -> volume.FSListResponse
|
1, // 30: volume.Volume.Stat:output_type -> volume.FileInfo
|
||||||
8, // 31: volume.Volume.Remove:output_type -> volume.FSOpResponse
|
14, // 31: volume.Volume.ListDir:output_type -> volume.FSListResponse
|
||||||
8, // 32: volume.Volume.Rename:output_type -> volume.FSOpResponse
|
8, // 32: volume.Volume.Remove:output_type -> volume.FSOpResponse
|
||||||
8, // 33: volume.Volume.MkdirAll:output_type -> volume.FSOpResponse
|
8, // 33: volume.Volume.Rename:output_type -> volume.FSOpResponse
|
||||||
6, // 34: volume.Volume.Copy:output_type -> volume.SyncResult
|
8, // 34: volume.Volume.MkdirAll:output_type -> volume.FSOpResponse
|
||||||
18, // 35: volume.Volume.Zip:output_type -> volume.ArchiveResponse
|
9, // 35: volume.Volume.Abs:output_type -> volume.FSAbsResponse
|
||||||
18, // 36: volume.Volume.Unzip:output_type -> volume.ArchiveResponse
|
6, // 36: volume.Volume.Copy:output_type -> volume.SyncResult
|
||||||
18, // 37: volume.Volume.Gzip:output_type -> volume.ArchiveResponse
|
19, // 37: volume.Volume.Zip:output_type -> volume.ArchiveResponse
|
||||||
18, // 38: volume.Volume.Gunzip:output_type -> volume.ArchiveResponse
|
19, // 38: volume.Volume.Unzip:output_type -> volume.ArchiveResponse
|
||||||
18, // 39: volume.Volume.Tar:output_type -> volume.ArchiveResponse
|
19, // 39: volume.Volume.Gzip:output_type -> volume.ArchiveResponse
|
||||||
18, // 40: volume.Volume.Untar:output_type -> volume.ArchiveResponse
|
19, // 40: volume.Volume.Gunzip:output_type -> volume.ArchiveResponse
|
||||||
18, // 41: volume.Volume.Tgz:output_type -> volume.ArchiveResponse
|
19, // 41: volume.Volume.Tar:output_type -> volume.ArchiveResponse
|
||||||
18, // 42: volume.Volume.Untgz:output_type -> volume.ArchiveResponse
|
19, // 42: volume.Volume.Untar:output_type -> volume.ArchiveResponse
|
||||||
25, // [25:43] is the sub-list for method output_type
|
19, // 43: volume.Volume.Tgz:output_type -> volume.ArchiveResponse
|
||||||
7, // [7:25] is the sub-list for method input_type
|
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 type_name
|
||||||
7, // [7:7] is the sub-list for extension extendee
|
7, // [7:7] is the sub-list for extension extendee
|
||||||
0, // [0:7] is the sub-list for field type_name
|
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(),
|
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)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_tai_volume_pb_volume_proto_rawDesc), len(file_tai_volume_pb_volume_proto_rawDesc)),
|
||||||
NumEnums: 1,
|
NumEnums: 1,
|
||||||
NumMessages: 18,
|
NumMessages: 19,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ service Volume {
|
||||||
rpc Remove(FSRemoveRequest) returns (FSOpResponse);
|
rpc Remove(FSRemoveRequest) returns (FSOpResponse);
|
||||||
rpc Rename(FSRenameRequest) returns (FSOpResponse);
|
rpc Rename(FSRenameRequest) returns (FSOpResponse);
|
||||||
rpc MkdirAll(FSRequest) 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).
|
// Copy: copy src to dst within the same workspace (server-side when remote).
|
||||||
rpc Copy(FSCopyRequest) returns (SyncResult);
|
rpc Copy(FSCopyRequest) returns (SyncResult);
|
||||||
|
|
||||||
|
|
@ -112,6 +114,10 @@ message FSOpResponse {
|
||||||
string error = 2;
|
string error = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message FSAbsResponse {
|
||||||
|
string path = 1; // absolute path on the host filesystem
|
||||||
|
}
|
||||||
|
|
||||||
message FSReadRequest {
|
message FSReadRequest {
|
||||||
string session_id = 1;
|
string session_id = 1;
|
||||||
string path = 2;
|
string path = 2;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const (
|
||||||
Volume_Remove_FullMethodName = "/volume.Volume/Remove"
|
Volume_Remove_FullMethodName = "/volume.Volume/Remove"
|
||||||
Volume_Rename_FullMethodName = "/volume.Volume/Rename"
|
Volume_Rename_FullMethodName = "/volume.Volume/Rename"
|
||||||
Volume_MkdirAll_FullMethodName = "/volume.Volume/MkdirAll"
|
Volume_MkdirAll_FullMethodName = "/volume.Volume/MkdirAll"
|
||||||
|
Volume_Abs_FullMethodName = "/volume.Volume/Abs"
|
||||||
Volume_Copy_FullMethodName = "/volume.Volume/Copy"
|
Volume_Copy_FullMethodName = "/volume.Volume/Copy"
|
||||||
Volume_Zip_FullMethodName = "/volume.Volume/Zip"
|
Volume_Zip_FullMethodName = "/volume.Volume/Zip"
|
||||||
Volume_Unzip_FullMethodName = "/volume.Volume/Unzip"
|
Volume_Unzip_FullMethodName = "/volume.Volume/Unzip"
|
||||||
|
|
@ -64,6 +65,8 @@ type VolumeClient interface {
|
||||||
Remove(ctx context.Context, in *FSRemoveRequest, opts ...grpc.CallOption) (*FSOpResponse, error)
|
Remove(ctx context.Context, in *FSRemoveRequest, opts ...grpc.CallOption) (*FSOpResponse, error)
|
||||||
Rename(ctx context.Context, in *FSRenameRequest, 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)
|
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: copy src to dst within the same workspace (server-side when remote).
|
||||||
Copy(ctx context.Context, in *FSCopyRequest, opts ...grpc.CallOption) (*SyncResult, error)
|
Copy(ctx context.Context, in *FSCopyRequest, opts ...grpc.CallOption) (*SyncResult, error)
|
||||||
Zip(ctx context.Context, in *ArchiveRequest, opts ...grpc.CallOption) (*ArchiveResponse, 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
|
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) {
|
func (c *volumeClient) Copy(ctx context.Context, in *FSCopyRequest, opts ...grpc.CallOption) (*SyncResult, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(SyncResult)
|
out := new(SyncResult)
|
||||||
|
|
@ -313,6 +326,8 @@ type VolumeServer interface {
|
||||||
Remove(context.Context, *FSRemoveRequest) (*FSOpResponse, error)
|
Remove(context.Context, *FSRemoveRequest) (*FSOpResponse, error)
|
||||||
Rename(context.Context, *FSRenameRequest) (*FSOpResponse, error)
|
Rename(context.Context, *FSRenameRequest) (*FSOpResponse, error)
|
||||||
MkdirAll(context.Context, *FSRequest) (*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: copy src to dst within the same workspace (server-side when remote).
|
||||||
Copy(context.Context, *FSCopyRequest) (*SyncResult, error)
|
Copy(context.Context, *FSCopyRequest) (*SyncResult, error)
|
||||||
Zip(context.Context, *ArchiveRequest) (*ArchiveResponse, 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) {
|
func (UnimplementedVolumeServer) MkdirAll(context.Context, *FSRequest) (*FSOpResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method MkdirAll not implemented")
|
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) {
|
func (UnimplementedVolumeServer) Copy(context.Context, *FSCopyRequest) (*SyncResult, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method Copy not implemented")
|
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)
|
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) {
|
func _Volume_Copy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(FSCopyRequest)
|
in := new(FSCopyRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
|
|
@ -723,6 +759,10 @@ var Volume_ServiceDesc = grpc.ServiceDesc{
|
||||||
MethodName: "MkdirAll",
|
MethodName: "MkdirAll",
|
||||||
Handler: _Volume_MkdirAll_Handler,
|
Handler: _Volume_MkdirAll_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Abs",
|
||||||
|
Handler: _Volume_Abs_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "Copy",
|
MethodName: "Copy",
|
||||||
Handler: _Volume_Copy_Handler,
|
Handler: _Volume_Copy_Handler,
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,17 @@ func (r *remoteStorage) MkdirAll(ctx context.Context, sessionID, path string) er
|
||||||
return nil
|
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.
|
// 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) {
|
func (r *remoteStorage) SyncPush(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type Volume interface {
|
||||||
Remove(ctx context.Context, sessionID, path string, recursive bool) error
|
Remove(ctx context.Context, sessionID, path string, recursive bool) error
|
||||||
Rename(ctx context.Context, sessionID, oldPath, newPath string) error
|
Rename(ctx context.Context, sessionID, oldPath, newPath string) error
|
||||||
MkdirAll(ctx context.Context, sessionID, path 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)
|
SyncPush(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error)
|
||||||
SyncPull(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)
|
_ = 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
|
// Cleanup
|
||||||
_ = vol.Remove(ctx, sid, ".", true)
|
_ = 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) {
|
func TestLocalPathTraversal(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
vol := NewLocal(dir)
|
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).
|
// 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.
|
// 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)
|
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.
|
// 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)
|
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 }
|
func (w *workspaceFS) Close() error { return nil }
|
||||||
|
|
||||||
// --- fs.FileInfo adapter ---
|
// --- 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.
|
// Compile-time interface checks.
|
||||||
var (
|
var (
|
||||||
_ fs.FS = (*workspaceFS)(nil)
|
_ fs.FS = (*workspaceFS)(nil)
|
||||||
|
|
|
||||||
|
|
@ -275,20 +275,7 @@ func (m *Manager) MountPath(ctx context.Context, id string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_ = vol
|
return vol.Abs(ctx, id, ".")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- internal ---
|
// --- internal ---
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue