mirror of
https://github.com/containers/podman.git
synced 2025-10-13 17:26:13 +08:00

Add the Go bindings implementation necessary to support Artifacts. Implement the tunnel interface that consumes the Artifacts Go bindings. With this patch, users of the Podman remote clients will now be able to manage OCI artifacts via the Podman CLI and Podman machine. Jira: https://issues.redhat.com/browse/RUN-2714# Signed-off-by: Lewis Roy <lewis@redhat.com>
114 lines
2.7 KiB
Go
114 lines
2.7 KiB
Go
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
|
|
}
|