@@ -245,30 +245,96 @@ func (f *fetcher) headManifest(ctx context.Context, ref name.Reference, acceptab
245245 }, nil
246246}
247247
248+ // contextKey is a type for context keys used in this package
249+ type contextKey string
250+
251+ const resumeOffsetKey contextKey = "resumeOffset"
252+ const resumeOffsetsKey contextKey = "resumeOffsets"
253+
254+ // WithResumeOffset returns a context with the resume offset set for a single blob
255+ func WithResumeOffset (ctx context.Context , offset int64 ) context.Context {
256+ return context .WithValue (ctx , resumeOffsetKey , offset )
257+ }
258+
259+ // WithResumeOffsets returns a context with resume offsets for multiple blobs (keyed by digest)
260+ func WithResumeOffsets (ctx context.Context , offsets map [string ]int64 ) context.Context {
261+ return context .WithValue (ctx , resumeOffsetsKey , offsets )
262+ }
263+
264+ // getResumeOffset retrieves the resume offset from context for a given digest
265+ func getResumeOffset (ctx context.Context , digest string ) int64 {
266+ // First check if there's a specific offset for this digest
267+ if offsets , ok := ctx .Value (resumeOffsetsKey ).(map [string ]int64 ); ok {
268+ if offset , found := offsets [digest ]; found && offset > 0 {
269+ return offset
270+ }
271+ }
272+ // Fall back to single offset (for fetchBlob)
273+ if offset , ok := ctx .Value (resumeOffsetKey ).(int64 ); ok {
274+ return offset
275+ }
276+ return 0
277+ }
278+
248279func (f * fetcher ) fetchBlob (ctx context.Context , size int64 , h v1.Hash ) (io.ReadCloser , error ) {
249280 u := f .url ("blobs" , h .String ())
250281 req , err := http .NewRequest (http .MethodGet , u .String (), nil )
251282 if err != nil {
252283 return nil , err
253284 }
254285
286+ // Check if we should resume from a specific offset
287+ resumeOffset := getResumeOffset (ctx , h .String ())
288+ if resumeOffset > 0 {
289+ // Add Range header to resume download
290+ req .Header .Set ("Range" , fmt .Sprintf ("bytes=%d-" , resumeOffset ))
291+ }
292+
255293 resp , err := f .client .Do (req .WithContext (ctx ))
256294 if err != nil {
257295 return nil , redact .Error (err )
258296 }
259297
260- if err := transport .CheckError (resp , http .StatusOK ); err != nil {
298+ // Accept both 200 OK (full content) and 206 Partial Content (resumed)
299+ if resumeOffset > 0 {
300+ // If we requested a Range but got 200, the server doesn't support ranges
301+ // We'll have to download from scratch
302+ if resp .StatusCode == http .StatusOK {
303+ // Server doesn't support range requests, will download full content
304+ resumeOffset = 0
305+ }
306+ }
307+
308+ if err := transport .CheckError (resp , http .StatusOK , http .StatusPartialContent ); err != nil {
261309 resp .Body .Close ()
262310 return nil , err
263311 }
264312
265- // Do whatever we can.
266- // If we have an expected size and Content-Length doesn't match, return an error.
267- // If we don't have an expected size and we do have a Content-Length, use Content-Length.
313+ // For partial content (resumed downloads), we can't verify the hash on the stream
314+ // since we're only getting part of the file. The complete file will be verified
315+ // after all bytes are written to disk.
316+ if resumeOffset > 0 && resp .StatusCode == http .StatusPartialContent {
317+ // Verify Content-Length matches expected remaining size
318+ if hsize := resp .ContentLength ; hsize != - 1 {
319+ if size != verify .SizeUnknown {
320+ expectedRemaining := size - resumeOffset
321+ if hsize != expectedRemaining {
322+ resp .Body .Close ()
323+ return nil , fmt .Errorf ("GET %s: Content-Length header %d does not match expected remaining size %d" , u .String (), hsize , expectedRemaining )
324+ }
325+ }
326+ }
327+ // Return the body without verification - we'll verify the complete file later
328+ return io .NopCloser (resp .Body ), nil
329+ }
330+
331+ // For full downloads, verify the stream
332+ // Do whatever we can with size validation
268333 if hsize := resp .ContentLength ; hsize != - 1 {
269334 if size == verify .SizeUnknown {
270335 size = hsize
271336 } else if hsize != size {
337+ resp .Body .Close ()
272338 return nil , fmt .Errorf ("GET %s: Content-Length header %d does not match expected size %d" , u .String (), hsize , size )
273339 }
274340 }
0 commit comments