@@ -67,19 +67,6 @@ type cpError struct {
6767 Path string `json:"path,omitempty"`
6868}
6969
70- // cpStatResponse contains information about a path in the guest
71- type cpStatResponse struct {
72- Type string `json:"type"`
73- Exists bool `json:"exists"`
74- IsDir bool `json:"is_dir"`
75- IsFile bool `json:"is_file"`
76- IsSymlink bool `json:"is_symlink,omitempty"`
77- LinkTarget string `json:"link_target,omitempty"`
78- Mode uint32 `json:"mode"`
79- Size int64 `json:"size"`
80- Error string `json:"error,omitempty"`
81- }
82-
8370var cpCmd = cli.Command {
8471 Name : "cp" ,
8572 Usage : "Copy files/folders between an instance and the local filesystem" ,
@@ -181,13 +168,13 @@ func handleCp(ctx context.Context, cmd *cli.Command) error {
181168 if dstPath == "-" {
182169 return copyFromInstanceToStdout (ctx , baseURL , apiKey , instanceID , srcPath , followLinks , archive )
183170 }
184- return copyFromInstance (ctx , baseURL , apiKey , instanceID , srcPath , dstPath , followLinks , quiet , archive )
171+ return copyFromInstance (ctx , & client , baseURL , apiKey , instanceID , srcPath , dstPath , followLinks , quiet , archive )
185172 } else {
186173 // Copy from local (or stdin if srcPath is "-") to instance
187174 if srcPath == "-" {
188175 return copyFromStdinToInstance (ctx , baseURL , apiKey , instanceID , dstPath , archive )
189176 }
190- return copyToInstance (ctx , baseURL , apiKey , instanceID , srcPath , dstPath , quiet , archive , followLinks )
177+ return copyToInstance (ctx , & client , baseURL , apiKey , instanceID , srcPath , dstPath , quiet , archive , followLinks )
191178 }
192179}
193180
@@ -277,61 +264,27 @@ func sanitizeTarPath(basePath, entryName string) (string, error) {
277264 return targetPath , nil
278265}
279266
280- // statGuestPath queries the guest for information about a path
281- func statGuestPath (ctx context.Context , baseURL , apiKey , instanceID , guestPath string , followLinks bool ) (* cpStatResponse , error ) {
282- wsURL , err := buildCpWsURL (baseURL , instanceID )
283- if err != nil {
284- return nil , err
285- }
286-
287- headers := http.Header {}
288- headers .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , apiKey ))
289-
290- dialer := & websocket.Dialer {}
291- ws , resp , err := dialer .DialContext (ctx , wsURL , headers )
292- if err != nil {
293- if resp != nil {
294- defer resp .Body .Close ()
295- body , _ := io .ReadAll (resp .Body )
296- return nil , fmt .Errorf ("websocket connect failed (HTTP %d): %s" , resp .StatusCode , string (body ))
297- }
298- return nil , fmt .Errorf ("websocket connect failed: %w" , err )
299- }
300- defer ws .Close ()
301-
302- // Send stat request
303- req := cpRequest {
304- Direction : "stat" ,
305- GuestPath : guestPath ,
306- FollowLinks : followLinks ,
267+ // statGuestPath queries the guest for information about a path using the SDK's Stat endpoint
268+ func statGuestPath (ctx context.Context , client * hypeman.Client , instanceID , guestPath string , followLinks bool ) (* hypeman.PathInfo , error ) {
269+ params := hypeman.InstanceStatParams {
270+ Path : guestPath ,
307271 }
308- reqJSON , _ := json .Marshal (req )
309- if err := ws .WriteMessage (websocket .TextMessage , reqJSON ); err != nil {
310- return nil , fmt .Errorf ("send request: %w" , err )
272+ if followLinks {
273+ params .FollowLinks = hypeman .Bool (true )
311274 }
312275
313- // Read response
314- _ , message , err := ws .ReadMessage ()
276+ pathInfo , err := client .Instances .Stat (ctx , instanceID , params )
315277 if err != nil {
316- return nil , fmt .Errorf ("read response: %w" , err )
317- }
318-
319- var statResp cpStatResponse
320- if err := json .Unmarshal (message , & statResp ); err != nil {
321- return nil , fmt .Errorf ("parse response: %w" , err )
278+ return nil , fmt .Errorf ("stat path: %w" , err )
322279 }
323280
324- if statResp .Type == "error" {
325- return nil , fmt .Errorf ("stat failed: %s" , statResp .Error )
326- }
327-
328- return & statResp , nil
281+ return pathInfo , nil
329282}
330283
331284// resolveDestPath resolves the destination path following docker cp semantics
332285// srcPath is the local source path, dstPath is the guest destination path
333286// Returns the resolved guest path
334- func resolveDestPath (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string ) (string , error ) {
287+ func resolveDestPath (ctx context.Context , client * hypeman. Client , instanceID , srcPath , dstPath string ) (string , error ) {
335288 srcInfo , err := os .Stat (srcPath )
336289 if err != nil {
337290 return "" , fmt .Errorf ("cannot stat source: %w" , err )
@@ -350,11 +303,15 @@ func resolveDestPath(ctx context.Context, baseURL, apiKey, instanceID, srcPath,
350303 dstEndsWithSlash := strings .HasSuffix (dstPath , "/" )
351304
352305 // Stat the destination in guest
353- dstStat , err := statGuestPath (ctx , baseURL , apiKey , instanceID , dstPath , true )
306+ dstStat , err := statGuestPath (ctx , client , instanceID , dstPath , true )
354307 if err != nil {
355308 return "" , fmt .Errorf ("stat destination: %w" , err )
356309 }
357310
311+ // Use bool fields directly from PathInfo
312+ isDir := dstStat .IsDir
313+ isFile := dstStat .IsFile
314+
358315 // Docker cp path resolution rules:
359316 // 1. If SRC is a file:
360317 // - DEST doesn't exist: save as DEST
@@ -377,7 +334,7 @@ func resolveDestPath(ctx context.Context, baseURL, apiKey, instanceID, srcPath,
377334 // Save as DEST
378335 return dstPath , nil
379336 }
380- if dstStat . IsDir {
337+ if isDir {
381338 // Copy into directory using basename
382339 // Use path.Join for guest paths (always forward slashes)
383340 return path .Join (dstPath , filepath .Base (srcPath )), nil
@@ -387,7 +344,7 @@ func resolveDestPath(ctx context.Context, baseURL, apiKey, instanceID, srcPath,
387344 }
388345
389346 // Source is a directory
390- if dstStat .Exists && dstStat . IsFile {
347+ if dstStat .Exists && isFile {
391348 return "" , fmt .Errorf ("cannot copy a directory to a file" )
392349 }
393350
@@ -426,7 +383,7 @@ func buildCpWsURL(baseURL, instanceID string) (string, error) {
426383}
427384
428385// copyToInstance copies a local file/directory to the instance
429- func copyToInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive , followLinks bool ) error {
386+ func copyToInstance (ctx context.Context , client * hypeman. Client , baseURL , apiKey , instanceID , srcPath , dstPath string , quiet , archive , followLinks bool ) error {
430387 // Check for /. suffix (copy contents only)
431388 copyContentsOnly := strings .HasSuffix (srcPath , string (filepath .Separator )+ "." ) || strings .HasSuffix (srcPath , "/." )
432389 originalSrcPath := srcPath
@@ -442,7 +399,7 @@ func copyToInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPath, d
442399 }
443400
444401 // Resolve destination path using docker cp semantics
445- resolvedDst , err := resolveDestPath (ctx , baseURL , apiKey , instanceID , originalSrcPath , dstPath )
402+ resolvedDst , err := resolveDestPath (ctx , client , instanceID , originalSrcPath , dstPath )
446403 if err != nil {
447404 return err
448405 }
@@ -634,7 +591,7 @@ func createDirOnInstanceWithUidGid(ctx context.Context, baseURL, apiKey, instanc
634591}
635592
636593// copyFromInstance copies a file/directory from the instance to local using the SDK
637- func copyFromInstance (ctx context.Context , baseURL , apiKey , instanceID , srcPath , dstPath string , followLinks , quiet , archive bool ) error {
594+ func copyFromInstance (ctx context.Context , client * hypeman. Client , baseURL , apiKey , instanceID , srcPath , dstPath string , followLinks , quiet , archive bool ) error {
638595 // Check for /. suffix (copy contents only) on guest source path
639596 copyContentsOnly := strings .HasSuffix (srcPath , "/." )
640597 if copyContentsOnly {
@@ -645,22 +602,25 @@ func copyFromInstance(ctx context.Context, baseURL, apiKey, instanceID, srcPath,
645602 dstEndsWithSlash := strings .HasSuffix (dstPath , "/" ) || strings .HasSuffix (dstPath , string (filepath .Separator ))
646603
647604 // Stat the guest source to check if it's file or directory
648- srcStat , err := statGuestPath (ctx , baseURL , apiKey , instanceID , srcPath , followLinks )
605+ srcStat , err := statGuestPath (ctx , client , instanceID , srcPath , followLinks )
649606 if err != nil {
650607 return fmt .Errorf ("stat source: %w" , err )
651608 }
652609 if ! srcStat .Exists {
653610 return fmt .Errorf ("source path %s does not exist in guest" , srcPath )
654611 }
655612
613+ // Use bool field directly from PathInfo
614+ srcIsDir := srcStat .IsDir
615+
656616 // Stat the local destination
657617 dstInfo , dstErr := os .Stat (dstPath )
658618 dstExists := dstErr == nil
659619 dstIsDir := dstExists && dstInfo .IsDir ()
660620
661621 // Apply docker cp path resolution for "from" direction
662622 resolvedDst := dstPath
663- if ! srcStat . IsDir {
623+ if ! srcIsDir {
664624 // Source is a file
665625 if ! dstExists {
666626 if dstEndsWithSlash {
0 commit comments