Skip to content

Commit 088d120

Browse files
committed
refactor: use SDK's autogenerated Stat endpoint instead of WebSocket
- Replace manual WebSocket stat implementation with client.Instances.Stat() - Update statGuestPath to use SDK client - Update copyToInstance and copyFromInstance to pass client - Remove unused cpStatResponse type - Update hypeman-go dependency to include Stat method
1 parent 0d77d5a commit 088d120

File tree

3 files changed

+30
-70
lines changed

3 files changed

+30
-70
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/gorilla/websocket v1.5.3
1313
github.com/itchyny/json2yaml v0.1.4
1414
github.com/muesli/reflow v0.3.0
15-
github.com/onkernel/hypeman-go v0.7.1-0.20251223031652-14904a03ecca
15+
github.com/onkernel/hypeman-go v0.7.1-0.20251223032806-c8e386b69845
1616
github.com/tidwall/gjson v1.18.0
1717
github.com/tidwall/pretty v1.2.1
1818
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
@@ -74,3 +74,5 @@ require (
7474
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
7575
google.golang.org/grpc v1.75.1 // indirect
7676
)
77+
78+
replace github.com/onkernel/hypeman-go => ../hypeman-go

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
105105
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
106106
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
107107
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
108-
github.com/onkernel/hypeman-go v0.7.1-0.20251223031652-14904a03ecca h1:wGYuHtNqniugAm655MU4xN+SwIGbuK3FLGq9psXXtDE=
109-
github.com/onkernel/hypeman-go v0.7.1-0.20251223031652-14904a03ecca/go.mod h1:Wtm4ewVGGPZc2ySeeuQISQyJxujyQuyDjXyksVkIyy8=
110108
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
111109
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
112110
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=

pkg/cmd/cp.go

Lines changed: 27 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
8370
var 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

Comments
 (0)