@@ -8,10 +8,12 @@ import (
8
8
"errors"
9
9
"fmt"
10
10
"io"
11
+ "io/fs"
11
12
"maps"
12
13
"net/http"
13
14
"os"
14
15
"path/filepath"
16
+ "strings"
15
17
16
18
"github.com/containers/common/libimage"
17
19
"github.com/containers/image/v5/manifest"
@@ -254,6 +256,161 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op
254
256
return & artifactManifestDigest , nil
255
257
}
256
258
259
+ // Inspect an artifact in a local store
260
+ func (as ArtifactStore ) Extract (ctx context.Context , nameOrDigest string , target string , options * libartTypes.ExtractOptions ) error {
261
+ if len (options .Digest ) > 0 && len (options .Title ) > 0 {
262
+ return errors .New ("cannot specify both digest and title" )
263
+ }
264
+ if len (nameOrDigest ) == 0 {
265
+ return ErrEmptyArtifactName
266
+ }
267
+
268
+ artifacts , err := as .getArtifacts (ctx , nil )
269
+ if err != nil {
270
+ return err
271
+ }
272
+
273
+ arty , nameIsDigest , err := artifacts .GetByNameOrDigest (nameOrDigest )
274
+ if err != nil {
275
+ return err
276
+ }
277
+ name := nameOrDigest
278
+ if nameIsDigest {
279
+ name = arty .Name
280
+ }
281
+
282
+ if len (arty .Manifest .Layers ) == 0 {
283
+ return fmt .Errorf ("the artifact has no blobs, nothing to extract" )
284
+ }
285
+
286
+ ir , err := layout .NewReference (as .storePath , name )
287
+ if err != nil {
288
+ return err
289
+ }
290
+ imgSrc , err := ir .NewImageSource (ctx , as .SystemContext )
291
+ if err != nil {
292
+ return err
293
+ }
294
+ defer imgSrc .Close ()
295
+
296
+ // check if dest is a dir to know if we can copy more than one blob
297
+ destIsFile := true
298
+ stat , err := os .Stat (target )
299
+ if err == nil {
300
+ destIsFile = ! stat .IsDir ()
301
+ } else if ! errors .Is (err , fs .ErrNotExist ) {
302
+ return err
303
+ }
304
+
305
+ if destIsFile {
306
+ var digest digest.Digest
307
+ if len (arty .Manifest .Layers ) > 1 {
308
+ if len (options .Digest ) == 0 && len (options .Title ) == 0 {
309
+ return fmt .Errorf ("the artifact consists of several blobs and the target %q is not a directory and neither digest or title was specified to only copy a single blob" , target )
310
+ }
311
+ digest , err = findDigest (arty , options )
312
+ if err != nil {
313
+ return err
314
+ }
315
+ } else {
316
+ digest = arty .Manifest .Layers [0 ].Digest
317
+ }
318
+
319
+ return copyImageBlobToFile (ctx , imgSrc , digest , target )
320
+ }
321
+
322
+ if len (options .Digest ) > 0 || len (options .Title ) > 0 {
323
+ digest , err := findDigest (arty , options )
324
+ if err != nil {
325
+ return err
326
+ }
327
+ // In case the digest is set we always use it as target name
328
+ // so we do not have to get the actual title annotation form the blob.
329
+ // Passing options.Title is enough because we know it is empty when digest
330
+ // is set as we only allow either one.
331
+ filename , err := generateArtifactBlobName (options .Title , digest )
332
+ if err != nil {
333
+ return err
334
+ }
335
+ return copyImageBlobToFile (ctx , imgSrc , digest , filepath .Join (target , filename ))
336
+ }
337
+
338
+ for _ , l := range arty .Manifest .Layers {
339
+ title := l .Annotations [specV1 .AnnotationTitle ]
340
+ filename , err := generateArtifactBlobName (title , l .Digest )
341
+ if err != nil {
342
+ return err
343
+ }
344
+ err = copyImageBlobToFile (ctx , imgSrc , l .Digest , filepath .Join (target , filename ))
345
+ if err != nil {
346
+ return err
347
+ }
348
+ }
349
+
350
+ return nil
351
+ }
352
+
353
+ func generateArtifactBlobName (title string , digest digest.Digest ) (string , error ) {
354
+ if len (title ) > 0 {
355
+ // Important: A potentially malicious artifact could contain a title name with "/"
356
+ // and could try via relative paths such as "../" try to overwrite files on the host
357
+ // the user did not intend. As there is no use for directories in this path we
358
+ // disallow all of them and not try to "make it safe" via securejoin or others.
359
+ if strings .ContainsRune (title , os .PathSeparator ) {
360
+ return "" , fmt .Errorf ("invalid name: title %q cannot contain %c" , title , os .PathSeparator )
361
+ }
362
+ return title , nil
363
+ }
364
+ // No filename given, use the digest. But because ":" is not a valid path char
365
+ // on all platforms replace it with "-".
366
+ return strings .ReplaceAll (digest .String (), ":" , "-" ), nil
367
+ }
368
+
369
+ func findDigest (arty * libartifact.Artifact , options * libartTypes.ExtractOptions ) (digest.Digest , error ) {
370
+ var digest digest.Digest
371
+ for _ , l := range arty .Manifest .Layers {
372
+ if options .Digest == l .Digest .String () {
373
+ if len (digest .String ()) > 0 {
374
+ return digest , fmt .Errorf ("more than one match for the digest %q" , options .Digest )
375
+ }
376
+ digest = l .Digest
377
+ }
378
+ if len (options .Title ) > 0 {
379
+ if val , ok := l .Annotations [specV1 .AnnotationTitle ]; ok &&
380
+ val == options .Title {
381
+ if len (digest .String ()) > 0 {
382
+ return digest , fmt .Errorf ("more than one match for the title %q" , options .Title )
383
+ }
384
+ digest = l .Digest
385
+ }
386
+ }
387
+ }
388
+ if len (digest .String ()) == 0 {
389
+ if len (options .Title ) > 0 {
390
+ return digest , fmt .Errorf ("no blob with the title %q" , options .Title )
391
+ }
392
+ return digest , fmt .Errorf ("no blob with the digest %q" , options .Digest )
393
+ }
394
+ return digest , nil
395
+ }
396
+
397
+ func copyImageBlobToFile (ctx context.Context , imgSrc types.ImageSource , digest digest.Digest , target string ) error {
398
+ src , _ , err := imgSrc .GetBlob (ctx , types.BlobInfo {Digest : digest }, nil )
399
+ if err != nil {
400
+ return fmt .Errorf ("failed to get artifact file: %w" , err )
401
+ }
402
+ defer src .Close ()
403
+ dest , err := os .Create (target )
404
+ if err != nil {
405
+ return fmt .Errorf ("failed to create target file: %w" , err )
406
+ }
407
+ defer dest .Close ()
408
+
409
+ // TODO use reflink is possible
410
+ _ , err = io .Copy (dest , src )
411
+ return err
412
+ }
413
+
257
414
// readIndex is currently unused but I want to keep this around until
258
415
// the artifact code is more mature.
259
416
func (as ArtifactStore ) readIndex () (* specV1.Index , error ) { //nolint:unused
0 commit comments