Skip to content

Commit 0639fd2

Browse files
committed
fix(cp): stream stdout tar output instead of accumulating in memory
Use header.Size to write tar header upfront, then stream binary data directly to tar.Writer. This enables copying large files without accumulating them entirely in memory.
1 parent 3010f65 commit 0639fd2

File tree

1 file changed

+31
-33
lines changed

1 file changed

+31
-33
lines changed

pkg/cmd/cp.go

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
859859
defer tw.Close()
860860

861861
var currentHeader *cpFileHeader
862-
var fileData []byte
862+
var bytesWritten int64
863863
var receivedFinal bool
864864

865865
for {
@@ -881,19 +881,19 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
881881

882882
switch msgTypeStr {
883883
case "header":
884-
// Flush previous file if any (but not symlinks - they have no data)
884+
// Verify previous file was completely written
885885
if currentHeader != nil && !currentHeader.IsDir && !currentHeader.IsSymlink {
886-
if err := writeTarEntry(tw, currentHeader, fileData, archive); err != nil {
887-
return err
886+
if bytesWritten != currentHeader.Size {
887+
return fmt.Errorf("file %s: expected %d bytes, got %d", currentHeader.Path, currentHeader.Size, bytesWritten)
888888
}
889-
fileData = nil
890889
}
891890

892891
var header cpFileHeader
893892
if err := json.Unmarshal(message, &header); err != nil {
894893
return fmt.Errorf("parse header: %w", err)
895894
}
896895
currentHeader = &header
896+
bytesWritten = 0
897897

898898
if header.IsDir {
899899
// Write directory entry to tar
@@ -928,15 +928,31 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
928928
if err := tw.WriteHeader(tarHeader); err != nil {
929929
return fmt.Errorf("write tar symlink header: %w", err)
930930
}
931+
} else {
932+
// Write regular file header with known size - enables streaming
933+
tarHeader := &tar.Header{
934+
Typeflag: tar.TypeReg,
935+
Name: header.Path,
936+
Size: header.Size,
937+
Mode: int64(header.Mode),
938+
ModTime: time.Unix(header.Mtime, 0),
939+
}
940+
// Only preserve UID/GID in archive mode
941+
if archive {
942+
tarHeader.Uid = int(header.Uid)
943+
tarHeader.Gid = int(header.Gid)
944+
}
945+
if err := tw.WriteHeader(tarHeader); err != nil {
946+
return fmt.Errorf("write tar header: %w", err)
947+
}
931948
}
932949

933950
case "end":
934-
// Write final file data if any
951+
// Verify file was completely written
935952
if currentHeader != nil && !currentHeader.IsDir && !currentHeader.IsSymlink {
936-
if err := writeTarEntry(tw, currentHeader, fileData, archive); err != nil {
937-
return err
953+
if bytesWritten != currentHeader.Size {
954+
return fmt.Errorf("file %s: expected %d bytes, got %d", currentHeader.Path, currentHeader.Size, bytesWritten)
938955
}
939-
fileData = nil
940956
}
941957
currentHeader = nil
942958

@@ -953,8 +969,12 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
953969
return fmt.Errorf("copy error at %s: %s", cpErr.Path, cpErr.Message)
954970
}
955971
} else if msgType == websocket.BinaryMessage {
956-
// Accumulate file data
957-
fileData = append(fileData, message...)
972+
// Stream file data directly to tar archive
973+
n, err := tw.Write(message)
974+
if err != nil {
975+
return fmt.Errorf("write tar data: %w", err)
976+
}
977+
bytesWritten += int64(n)
958978
}
959979
}
960980

@@ -965,26 +985,4 @@ func copyFromInstanceToStdout(ctx context.Context, baseURL, apiKey, instanceID,
965985
return nil
966986
}
967987

968-
// writeTarEntry writes a file entry to the tar archive
969-
func writeTarEntry(tw *tar.Writer, header *cpFileHeader, data []byte, archive bool) error {
970-
tarHeader := &tar.Header{
971-
Typeflag: tar.TypeReg,
972-
Name: header.Path,
973-
Size: int64(len(data)),
974-
Mode: int64(header.Mode),
975-
ModTime: time.Unix(header.Mtime, 0),
976-
}
977-
// Only preserve UID/GID in archive mode
978-
if archive {
979-
tarHeader.Uid = int(header.Uid)
980-
tarHeader.Gid = int(header.Gid)
981-
}
982-
if err := tw.WriteHeader(tarHeader); err != nil {
983-
return fmt.Errorf("write tar header: %w", err)
984-
}
985-
if _, err := tw.Write(data); err != nil {
986-
return fmt.Errorf("write tar data: %w", err)
987-
}
988-
return nil
989-
}
990988

0 commit comments

Comments
 (0)