Lets the workflow set, e.g., public-read on the uploaded object so the HTTPS URL is actually downloadable without further configuration. Empty default means no ACL is sent — required for modern AWS buckets with Object Ownership = "Bucket owner enforced" that reject any ACL. Validates the value against the AWS canned-ACL list at config time so typos fail before the upload runs. Wires the input through action.yml, config, and the orchestrator; adds a unit test that the ACL is forwarded to PutObjectInput when set and omitted when empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
2.9 KiB
Go
93 lines
2.9 KiB
Go
package upload
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
)
|
|
|
|
// PutObjectAPI is the subset of *s3.Client used here. Tests inject a fake.
|
|
type PutObjectAPI interface {
|
|
PutObject(ctx context.Context, in *s3.PutObjectInput, opts ...func(*s3.Options)) (*s3.PutObjectOutput, error)
|
|
}
|
|
|
|
// Opts configures a single upload.
|
|
type Opts struct {
|
|
Bucket string
|
|
Key string
|
|
FilePath string
|
|
// ACL, if set, is applied to the uploaded object as a canned ACL
|
|
// (e.g. "public-read"). Empty means no ACL is sent — required for
|
|
// buckets with Object Ownership = "Bucket owner enforced".
|
|
ACL string
|
|
}
|
|
|
|
// RenderKey substitutes {version} and {filename} placeholders.
|
|
func RenderKey(template, version, filename string) string {
|
|
r := strings.NewReplacer("{version}", version, "{filename}", filename)
|
|
return r.Replace(template)
|
|
}
|
|
|
|
// Upload PUTs FilePath at s3://Bucket/Key and returns the s3:// URL.
|
|
func Upload(ctx context.Context, c PutObjectAPI, o Opts) (string, error) {
|
|
f, err := os.Open(o.FilePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open %s: %w", o.FilePath, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
in := &s3.PutObjectInput{
|
|
Bucket: aws.String(o.Bucket),
|
|
Key: aws.String(o.Key),
|
|
Body: f,
|
|
}
|
|
if o.ACL != "" {
|
|
in.ACL = types.ObjectCannedACL(o.ACL)
|
|
}
|
|
if _, err := c.PutObject(ctx, in); err != nil {
|
|
return "", fmt.Errorf("s3 put: %w", err)
|
|
}
|
|
return fmt.Sprintf("s3://%s/%s", o.Bucket, o.Key), nil
|
|
}
|
|
|
|
// HTTPSURL returns the HTTPS form of an upload location. For AWS S3 it uses
|
|
// the regional virtual-hosted form; for a custom endpoint it appends the
|
|
// bucket and key path-style, matching how NewClient configures the client.
|
|
//
|
|
// The bucket may still be private — this is purely the URL where the object
|
|
// would be served if it (or its bucket) were publicly readable.
|
|
func HTTPSURL(bucket, key, region, endpointURL string) string {
|
|
if endpointURL != "" {
|
|
return strings.TrimRight(endpointURL, "/") + "/" + bucket + "/" + key
|
|
}
|
|
return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", bucket, region, key)
|
|
}
|
|
|
|
// WriteFileForTest is exposed for use from this package's tests.
|
|
func WriteFileForTest(path string, b []byte) error {
|
|
return os.WriteFile(path, b, 0o600)
|
|
}
|
|
|
|
// NewClient builds an *s3.Client honouring an optional custom endpoint
|
|
// for S3-compatible storage.
|
|
func NewClient(ctx context.Context, region, endpointURL string) (*s3.Client, error) {
|
|
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("aws config: %w", err)
|
|
}
|
|
opts := []func(*s3.Options){}
|
|
if endpointURL != "" {
|
|
opts = append(opts, func(o *s3.Options) {
|
|
o.BaseEndpoint = aws.String(endpointURL)
|
|
o.UsePathStyle = true
|
|
})
|
|
}
|
|
return s3.NewFromConfig(cfg, opts...), nil
|
|
}
|