@@ -105,6 +105,12 @@ type CopyGraphOptions struct {
105
105
// OnCopySkipped will be called when the sub-DAG rooted by the current node
106
106
// is skipped.
107
107
OnCopySkipped func (ctx context.Context , desc ocispec.Descriptor ) error
108
+ // MountFrom returns the candidate repositories that desc may be mounted from.
109
+ // The OCI references will be tried in turn. If mounting fails on all of them,
110
+ // then it falls back to a copy.
111
+ MountFrom func (ctx context.Context , desc ocispec.Descriptor ) ([]string , error )
112
+ // OnMounted will be invoked when desc is mounted.
113
+ OnMounted func (ctx context.Context , desc ocispec.Descriptor ) error
108
114
// FindSuccessors finds the successors of the current node.
109
115
// fetcher provides cached access to the source storage, and is suitable
110
116
// for fetching non-leaf nodes like manifests. Since anything fetched from
@@ -259,12 +265,86 @@ func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Sto
259
265
if exists {
260
266
return copyNode (ctx , proxy .Cache , dst , desc , opts )
261
267
}
262
- return copyNode (ctx , src , dst , desc , opts )
268
+ return mountOrCopyNode (ctx , src , dst , desc , opts )
263
269
}
264
270
265
271
return syncutil .Go (ctx , limiter , fn , root )
266
272
}
267
273
274
+ // mountOrCopyNode tries to mount the node, if not falls back to copying.
275
+ func mountOrCopyNode (ctx context.Context , src content.ReadOnlyStorage , dst content.Storage , desc ocispec.Descriptor , opts CopyGraphOptions ) error {
276
+ // Need MountFrom and it must be a blob
277
+ if opts .MountFrom == nil || descriptor .IsManifest (desc ) {
278
+ return copyNode (ctx , src , dst , desc , opts )
279
+ }
280
+
281
+ mounter , ok := dst .(registry.Mounter )
282
+ if ! ok {
283
+ // mounting is not supported by the destination
284
+ return copyNode (ctx , src , dst , desc , opts )
285
+ }
286
+
287
+ sourceRepositories , err := opts .MountFrom (ctx , desc )
288
+ if err != nil {
289
+ // Technically this error is not fatal, we can still attempt to copy the node
290
+ // But for consistency with the other callbacks we bail out.
291
+ return err
292
+ }
293
+
294
+ if len (sourceRepositories ) == 0 {
295
+ return copyNode (ctx , src , dst , desc , opts )
296
+ }
297
+
298
+ skipContent := errors .New ("skip content" )
299
+ for i , sourceRepository := range sourceRepositories {
300
+ // try mounting this source repository
301
+ var mountFailed bool
302
+ getContent := func () (io.ReadCloser , error ) {
303
+ // the invocation of getContent indicates that mounting has failed
304
+ mountFailed = true
305
+
306
+ if len (sourceRepositories )- 1 == i {
307
+ // this is the last iteration so we need to actually get the content and do the copy
308
+
309
+ // call the original PreCopy function if it exists
310
+ if opts .PreCopy != nil {
311
+ if err := opts .PreCopy (ctx , desc ); err != nil {
312
+ return nil , err
313
+ }
314
+ }
315
+ return src .Fetch (ctx , desc )
316
+ }
317
+
318
+ // We want to return an error that we will test for from mounter.Mount()
319
+ return nil , skipContent
320
+ }
321
+
322
+ // Mount or copy
323
+ if err := mounter .Mount (ctx , desc , sourceRepository , getContent ); err != nil && ! errors .Is (err , skipContent ) {
324
+ return err
325
+ }
326
+
327
+ if ! mountFailed {
328
+ // mounted, success
329
+ if opts .OnMounted != nil {
330
+ if err := opts .OnMounted (ctx , desc ); err != nil {
331
+ return err
332
+ }
333
+ }
334
+ return nil
335
+ }
336
+ }
337
+
338
+ // we copied it
339
+ if opts .PostCopy != nil {
340
+ if err := opts .PostCopy (ctx , desc ); err != nil {
341
+ return err
342
+ }
343
+ }
344
+
345
+ return nil
346
+ }
347
+
268
348
// doCopyNode copies a single content from the source CAS to the destination CAS.
269
349
func doCopyNode (ctx context.Context , src content.ReadOnlyStorage , dst content.Storage , desc ocispec.Descriptor ) error {
270
350
rc , err := src .Fetch (ctx , desc )
0 commit comments