@@ -101,22 +101,43 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio
101101 storeOpts = append (storeOpts , transferimage .WithExtraReference (imageRef ))
102102 }
103103
104- w := nopWriteCloser {options .Stdout }
104+ w := & syncWriteCloser {
105+ Writer : options .Stdout ,
106+ done : make (chan struct {}),
107+ }
105108
106109 pf , done := transferutil .ProgressHandler (ctx , os .Stderr )
107110 defer done ()
108111
109- return client .Transfer (ctx ,
112+ err = client .Transfer (ctx ,
110113 transferimage .NewStore ("" , storeOpts ... ),
111114 tarchive .NewImageExportStream (w , "" , exportOpts ... ),
112115 transfer .WithProgress (pf ),
113116 )
117+ if err != nil {
118+ return err
119+ }
120+
121+ // Wait for the stream copy goroutine to finish writing before returning.
122+ // client.Transfer returns when the server-side export completes, but the
123+ // client-side goroutine in containerd's ImageExportStream.MarshalAny may
124+ // still be copying streamed data to the writer. Without this wait, the
125+ // caller may close the output file while the goroutine is still writing,
126+ // resulting in "file already closed" errors and a truncated tar archive.
127+ <- w .done
128+
129+ return nil
114130}
115131
116- type nopWriteCloser struct {
132+ // syncWriteCloser wraps an io.Writer and signals when Close is called,
133+ // allowing the caller to wait until the transfer stream goroutine has
134+ // finished writing all data.
135+ type syncWriteCloser struct {
117136 io.Writer
137+ done chan struct {}
118138}
119139
120- func (nopWriteCloser ) Close () error {
140+ func (s * syncWriteCloser ) Close () error {
141+ close (s .done )
121142 return nil
122143}
0 commit comments