package artifacts import ( "archive/tar" "context" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "github.com/containers/podman/v5/pkg/bindings" ) func Extract(ctx context.Context, artifactName string, target string, options *ExtractOptions) error { conn, err := bindings.GetClient(ctx) if err != nil { return fmt.Errorf("getting client: %w", err) } if options == nil { options = new(ExtractOptions) } // Check if target is a directory to know if we can copy more than one blob targetIsDirectory := false stat, err := os.Stat(target) if err == nil { targetIsDirectory = stat.IsDir() } else if !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("stat target %q failed: %w", target, err) } // If the target is not a directory, request API to return the blob without title. // If a blob has a malicious title it will be returned from the API without it // as the file will be written to the provided target if !targetIsDirectory { options.WithExcludeTitle(true) } params, err := options.ToParams() if err != nil { return fmt.Errorf("converting options to params: %w", err) } response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/artifacts/%s/extract", params, nil, artifactName) if err != nil { return err } defer response.Body.Close() if !response.IsSuccess() { return response.Process(nil) } multipleBlobs := false tr := tar.NewReader(response.Body) for { header, err := tr.Next() if err == io.EOF { break // End of archive } if err != nil { return err } if !targetIsDirectory && multipleBlobs { 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) } // If destination isn't a file, extract to target/filename fileTarget := target if targetIsDirectory { fileTarget = filepath.Join(target, header.Name) } if header.Typeflag == tar.TypeReg { err = extractFile(tr, fileTarget) if err != nil { return err } } // Signal that the first blob has been extracted so we can return an error if more // than one blob are being extracted when target is not a directory. multipleBlobs = true } return nil } func extractFile(tr *tar.Reader, fileTarget string) (retErr error) { outFile, err := os.Create(fileTarget) if err != nil { return err } // Use an anonymous function to enable capturing the error from // outFile.Close() upon returning. defer func() { cErr := outFile.Close() if retErr == nil { retErr = cErr } }() _, err = io.Copy(outFile, tr) if err != nil { return fmt.Errorf("failed to extract blob to %s: %w", fileTarget, err) } return nil }