Skip to main content
Glama
box_cp.go11.7 kB
package cmd import ( "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "regexp" "strings" "github.com/babelcloud/gbox/packages/cli/internal/profile" "github.com/spf13/cobra" ) // BoxPath represents the structure of a box path type BoxPath struct { BoxID string Path string } // Parse box path (format BOX_ID:PATH) func parseBoxPath(path string) (*BoxPath, error) { re := regexp.MustCompile(`^([^:]+):(.+)$`) matches := re.FindStringSubmatch(path) if len(matches) != 3 { return nil, fmt.Errorf("invalid box path format, should be BOX_ID:PATH") } return &BoxPath{ BoxID: matches[1], Path: matches[2], }, nil } // Check if path is a box path func isBoxPath(path string) bool { return strings.Contains(path, ":") } // Convert relative path to absolute path func getAbsolutePath(path string) string { if _, err := os.Stat(path); err == nil { absPath, err := filepath.Abs(path) if err == nil { return absPath } } dir := filepath.Dir(path) absDir, err := filepath.Abs(dir) if err != nil { return path } return filepath.Join(absDir, filepath.Base(path)) } // BoxCpOptions holds command options and parameters type BoxCpOptions struct { Source string Destination string } func NewBoxCpCommand() *cobra.Command { opts := &BoxCpOptions{} cmd := &cobra.Command{ Use: "cp <src> <dst>", Short: "Copy files/folders between a box and the local filesystem", Long: `usage: gbox-box-cp [-h] src dst Copy files/folders between a box and the local filesystem positional arguments: src Source path dst Destination path options: -h, --help show this help message and exit`, Example: ` gbox box cp ./local_file 550e8400-e29b-41d4-a716-446655440000:/work # Copy local file to box gbox box cp 550e8400-e29b-41d4-a716-446655440000:/var/logs/ /tmp/app_logs # Copy from box to local gbox box cp - 550e8400-e29b-41d4-a716-446655440000:/work # Copy tar stream from stdin to box gbox box cp 550e8400-e29b-41d4-a716-446655440000:/etc/hosts - # Copy from box to stdout as tar stream`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { opts.Source = args[0] opts.Destination = args[1] return runCopyCommand(opts) }, } return cmd } func runCopyCommand(opts *BoxCpOptions) error { src := opts.Source dst := opts.Destination debugEnabled := os.Getenv("DEBUG") == "true" // Get effective base URL for API calls apiURL := profile.Default.GetEffectiveBaseURL() // Debug log debug := func(msg string) { if debugEnabled { fmt.Fprintf(os.Stderr, "[DEBUG] %s\n", msg) } } // Determine copy direction and process if isBoxPath(src) && !isBoxPath(dst) { return copyFromBoxToLocal(src, dst, apiURL, debug) } else if !isBoxPath(src) && isBoxPath(dst) { return copyFromLocalToBox(src, dst, apiURL, debug) } else { return fmt.Errorf("invalid path format. One path must be a box path (BOX_ID:PATH) and the other must be a local path") } } func copyFromBoxToLocal(src, dst, apiURL string, debug func(string)) error { boxPath, err := parseBoxPath(src) if err != nil { return err } debug(fmt.Sprintf("Box ID: %s", boxPath.BoxID)) debug(fmt.Sprintf("Source path: %s", boxPath.Path)) debug(fmt.Sprintf("Destination path: %s", dst)) if dst == "-" { // Copy from box to stdout as tar stream return copyFromBoxToStdout(boxPath, apiURL, debug) } else { // Copy from box to local file return copyFromBoxToFile(boxPath, dst, apiURL, debug) } } func copyFromBoxToStdout(boxPath *BoxPath, apiURL string, debug func(string)) error { requestURL := fmt.Sprintf("%s/boxes/%s/archive?path=%s", apiURL, boxPath.BoxID, boxPath.Path) debug(fmt.Sprintf("Sending GET request to: %s", requestURL)) resp, err := http.Get(requestURL) if err != nil { return fmt.Errorf("failed to download from box: %v", err) } defer resp.Body.Close() debug(fmt.Sprintf("HTTP response status code: %d", resp.StatusCode)) if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to download from box, HTTP status code: %d", resp.StatusCode) } _, err = io.Copy(os.Stdout, resp.Body) if err != nil { return fmt.Errorf("failed to write to stdout: %v", err) } return nil } func copyFromBoxToFile(boxPath *BoxPath, dst, apiURL string, debug func(string)) error { // Convert local path to absolute path dst = getAbsolutePath(dst) // Create destination directory if it doesn't exist err := os.MkdirAll(filepath.Dir(dst), 0755) if err != nil { return fmt.Errorf("failed to create destination directory: %v", err) } // Download to temporary file tempFile, err := os.CreateTemp("", "gbox-cp-") if err != nil { return fmt.Errorf("failed to create temporary file: %v", err) } tempFilePath := tempFile.Name() defer os.Remove(tempFilePath) requestURL := fmt.Sprintf("%s/boxes/%s/archive?path=%s", apiURL, boxPath.BoxID, boxPath.Path) debug(fmt.Sprintf("Sending GET request to: %s", requestURL)) resp, err := http.Get(requestURL) if err != nil { return fmt.Errorf("failed to download from box: %v", err) } defer resp.Body.Close() debug(fmt.Sprintf("HTTP response status code: %d", resp.StatusCode)) if resp.StatusCode != http.StatusOK { // It's helpful to read the body even on error for more details bodyBytes, _ := io.ReadAll(resp.Body) debug(fmt.Sprintf("Error response body: %s", string(bodyBytes))) return fmt.Errorf("failed to download from box, HTTP status code: %d", resp.StatusCode) } bytesCopied, copyErr := io.Copy(tempFile, resp.Body) debug(fmt.Sprintf("Bytes copied to temporary file: %d", bytesCopied)) // Ensure file is closed regardless of copy errors defer tempFile.Close() // Close should happen after potential errors are checked // Handle errors after attempting copy and close if copyErr != nil { // Check if the error is specifically UnexpectedEOF, likely from the reader (resp.Body) if copyErr == io.ErrUnexpectedEOF { return fmt.Errorf("failed to download complete file from box (unexpected EOF): %v", copyErr) } // Otherwise, report it as a failure to write to the temp file return fmt.Errorf("failed to write to temporary file: %v", copyErr) } // Check format and extract dstDir := filepath.Dir(dst) srcBaseName := filepath.Base(boxPath.Path) dstBaseName := filepath.Base(dst) // Try to extract as gzip tar cmd := exec.Command("tar", "-xzf", tempFilePath, "-C", dstDir) output, err := cmd.CombinedOutput() if err != nil { // Try to extract as regular tar cmd = exec.Command("tar", "-xf", tempFilePath, "-C", dstDir) output, err = cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to extract archive: %v\nOutput:\n%s", err, string(output)) } } // After extraction, check if the destination path `dst` was intended to be a file or directory. // If `dst` does not end with a separator, assume it was a file. extractedPath := filepath.Join(dstDir, srcBaseName) // Default expected path after extraction finalDstPath := dst // Smarter check: Did the tar command create the exact dstBaseName in dstDir? // Or did it potentially create srcBaseName or a path structure? // Let's check if the srcBaseName exists first if _, statErr := os.Stat(extractedPath); statErr == nil { // If the original dst path didn't end with '/' and is different from the extracted path, // it implies the user wanted to copy INTO a file named dstBaseName. if !strings.HasSuffix(dst, string(os.PathSeparator)) && dstBaseName != srcBaseName { if renameErr := os.Rename(extractedPath, finalDstPath); renameErr != nil { return fmt.Errorf("failed to rename extracted file to destination: %v", renameErr) } } else if dstBaseName == srcBaseName { // If dst and src base names match, extraction likely overwrote/created the correct file/dir. } else { // Destination was likely a directory, files extracted inside. finalDstPath = extractedPath // The message should report the actual extracted path } } else { // If srcBaseName doesn't exist directly, maybe tar extracted with full path? // This case is harder to handle reliably without knowing archive structure. // We will assume for now the extraction target was dstDir and tar placed files inside. // The user might need to check dstDir content. // We can try to list files extracted by tar output parsing, but it's complex. // We cannot be sure what the final path is, report the directory finalDstPath = dstDir } fmt.Fprintf(os.Stderr, "Copied from box %s:%s to %s\n", boxPath.BoxID, boxPath.Path, finalDstPath) return nil } func copyFromLocalToBox(src, dst, apiURL string, debug func(string)) error { boxPath, err := parseBoxPath(dst) if err != nil { return err } if src == "-" { // Copy tar stream from stdin to box return copyFromStdinToBox(boxPath, apiURL, debug) } else { // Copy from local file to box return copyFromFileToBox(src, boxPath, apiURL, debug) } } func copyFromStdinToBox(boxPath *BoxPath, apiURL string, debug func(string)) error { requestURL := fmt.Sprintf("%s/boxes/%s/archive?path=%s", apiURL, boxPath.BoxID, boxPath.Path) debug(fmt.Sprintf("Sending PUT request to: %s", requestURL)) req, err := http.NewRequest("PUT", requestURL, os.Stdin) if err != nil { return fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", "application/x-tar") client := &http.Client{} resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to upload to box: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to upload to box, HTTP status code: %d", resp.StatusCode) } fmt.Fprintf(os.Stderr, "Copied from stdin to box %s:%s\n", boxPath.BoxID, boxPath.Path) return nil } func copyFromFileToBox(src string, boxPath *BoxPath, apiURL string, _ func(string)) error { // Convert local path to absolute path src = getAbsolutePath(src) // Check if source file exists if _, err := os.Stat(src); os.IsNotExist(err) { return fmt.Errorf("source file or directory does not exist: %s", src) } // Create temporary file for the tar tempFile, err := os.CreateTemp("", "gbox-cp-") if err != nil { return fmt.Errorf("failed to create temporary file: %v", err) } tempFilePath := tempFile.Name() tempFile.Close() defer os.Remove(tempFilePath) // Create tar archive cmd := exec.Command("tar", "--no-xattrs", "-czf", tempFilePath, "-C", filepath.Dir(src), filepath.Base(src)) err = cmd.Run() if err != nil { return fmt.Errorf("failed to create tar archive: %v", err) } // Get file size fileInfo, err := os.Stat(tempFilePath) if err != nil { return fmt.Errorf("failed to get temporary file info: %v", err) } fileSize := fileInfo.Size() // Upload archive to box file, err := os.Open(tempFilePath) if err != nil { return fmt.Errorf("failed to open temporary file: %v", err) } defer file.Close() requestURL := fmt.Sprintf("%s/boxes/%s/archive?path=%s", apiURL, boxPath.BoxID, boxPath.Path) req, err := http.NewRequest("PUT", requestURL, file) if err != nil { return fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", "application/x-tar") req.Header.Set("Content-Length", fmt.Sprintf("%d", fileSize)) client := &http.Client{} resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to upload to box: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to upload to box, HTTP status code: %d", resp.StatusCode) } fmt.Fprintf(os.Stderr, "Copied from %s to box %s:%s\n", src, boxPath.BoxID, boxPath.Path) return nil }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babelcloud/gru-sandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server