s3

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

s3 — S3 client for TinyGo

The maintained Go clients do not build with TinyGo. aws-sdk-go-v2 reaches for the full net/http.Transport API, which TinyGo declares as an empty struct, and its transport layer imports net/http/httputil, which does not compile under TinyGo at all; minio-go fails even earlier, on net/http/cookiejar. This package therefore speaks the S3 REST API directly, over the SigV4 signer in cloud/aws, which nosql/dynamodb shares.

import "github.com/shibukawa/tinygodriver/storage/s3"

client, err := s3.New(
	s3.WithRegion("ap-northeast-1"),
	s3.WithCredentials(s3.Credentials{AccessKeyID: id, SecretAccessKey: secret}),
)

obj, err := client.Get(ctx, "bucket", "photos/cat.jpg")
defer obj.Body.Close()

Implementation selection

Signing, request building, and XML decoding are shared code. The builds differ only in how a request reaches the network:

Build HTTP stack (s3.Backend)
Standard Go net/http with crypto/tls
TinyGo, or -tags force_tinygo_logic https, TLS through the host OS

Neither build lets http.Client follow redirects, because a redirected request goes to a different host and its signature covers that host. Client follows them itself and signs each hop, so a bucket in another region works the same on both builds. TinyGo's http.Client never follows redirects anyway, which is what makes this the only correct design.

API

Method S3 operation
Get, GetRange GetObject, with Range
Put PutObject
Head HeadObject
Delete DeleteObject
List ListObjectsV2, one page
CreateBucket, DeleteBucket CreateBucket, DeleteBucket
Presign none; a SigV4 query-signed URL for GetObject, PutObject, HeadObject or DeleteObject
CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload the multipart upload operations

Put takes WithContentType, WithContentEncoding, WithMetadata, and WithContentLength. List takes WithPrefix, WithDelimiter, WithMaxKeys, WithStartAfter, and WithContinuationToken.

List returns one page. A truncated page carries NextToken:

var token string
for {
	page, err := client.List(ctx, "bucket",
		s3.WithPrefix("photos/"), s3.WithContinuationToken(token))
	if err != nil {
		return err
	}
	for _, obj := range page.Objects {
		fmt.Println(obj.Key, obj.Size)
	}
	if !page.IsTruncated {
		break
	}
	token = page.NextToken
}

Multipart upload

An object above the single-request limit goes up in parts. CreateMultipartUpload fixes the object's content type and metadata and returns a MultipartUpload; the other three calls take it back, so bucket, key and upload ID cannot drift apart. Part numbers run from 1 to 10000, and every part but the last must be at least 5 MiB, which the endpoint checks at completion.

upload, err := client.CreateMultipartUpload(ctx, "bucket", "video.mp4",
	s3.WithContentType("video/mp4"))
var parts []s3.CompletedPart
for n := 1; ; n++ {
	chunk, done := nextChunk()
	part, err := client.UploadPart(ctx, *upload, n, chunk)
	if err != nil {
		client.AbortMultipartUpload(ctx, *upload)
		return err
	}
	parts = append(parts, *part)
	if done {
		break
	}
}
res, err := client.CompleteMultipartUpload(ctx, *upload, parts)

An upload that is neither completed nor aborted keeps its parts, and AWS bills them, until a lifecycle rule removes it: abort on every failure path. A part body follows Put's rules, and WithContentLength is the one Put option that applies to it. To let a browser send a part, presign it with the upload ID and part number as query parameters:

u, err := client.Presign(ctx, "bucket", "video.mp4", s3.PresignOptions{
	Method: "PUT",
	Query:  map[string]string{"uploadId": upload.UploadID, "partNumber": "3"},
})

Presigned URLs

Presign returns a URL that authorizes one request without the credentials, for a bounded time, so a browser can upload to or download from the bucket directly and the application only issues the permission. It uses the client's endpoint, region, addressing style and credentials, so a URL for RustFS, MinIO or Cloudflare R2 needs no second configuration. No request is made.

upload, err := client.Presign(ctx, "bucket", "uploads/cat.jpg", s3.PresignOptions{
	Method:      "PUT",
	Expires:     15 * time.Minute,
	ContentType: "image/jpeg",
})

download, err := client.Presign(ctx, "bucket", "uploads/cat.jpg", s3.PresignOptions{
	Query: map[string]string{"response-content-disposition": `attachment; filename="cat.jpg"`},
})

The signature carries UNSIGNED-PAYLOAD: the body is sent by someone who never sees the credentials, so the signer cannot hash it. Only the host and the headers the options name are signed, and each signed header is one the sender must reproduce exactly. That is why ContentType and Headers constrain a PUT, and why a GET names none: a link in a page cannot add a header, so the response-content-* parameters go in Query, signed with the URL.

Method defaults to GET and Expires to fifteen minutes. S3 refuses more than seven days, and so does Presign, with ErrPresignExpiry.

Configuration

New falls back to the environment, so a configured shell needs no options: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION (or AWS_DEFAULT_REGION), and AWS_ENDPOINT_URL_S3 (or AWS_ENDPOINT_URL).

Option Effect
WithEndpoint endpoint URL, for S3-compatible servers
WithRegion signing region
WithCredentials static credentials
WithCredentialsFromEnv read credentials from the environment
WithPathStyle endpoint/bucket/key instead of bucket.endpoint/key
WithUnsignedPayload sign headers only, so large streams are not buffered
WithTimeout logical-operation timeout including redirects and response body, default 60s
WithHTTPClient supply the http.Client

Addressing defaults to virtual-host style for amazonaws.com endpoints and path style everywhere else, which is what S3-compatible servers expect.

client, err := s3.New(
	s3.WithEndpoint("http://127.0.0.1:9000"),
	s3.WithRegion("us-east-1"),
	s3.WithCredentials(s3.Credentials{AccessKeyID: "admin", SecretAccessKey: "admin"}),
)

Errors

S3 error codes map onto sentinels, so application code branches without matching strings. *s3.Error carries the status, code, message, and request ID.

obj, err := client.Get(ctx, "bucket", "missing")
if errors.Is(err, s3.ErrNoSuchKey) {
	// ...
}

var s3err *s3.Error
if errors.As(err, &s3err) {
	log.Println(s3err.StatusCode, s3err.Code, s3err.RequestID)
}

ErrNoSuchKey, ErrNoSuchBucket, ErrAccessDenied, ErrBucketExists, ErrBucketNotEmpty, ErrInvalidRange, ErrBadCredentials, ErrNoSuchUpload, ErrInvalidPart, ErrPresignExpiry, ErrNoCredentials, ErrNoRegion, ErrTooManyRedirect.

Limitations

  • Put is one request. The endpoint's single-request limit applies (5 GiB on AWS); anything larger goes through the multipart calls, and nothing here splits a stream into parts for you.
  • Put reads the body twice. SigV4 signs a hash of the payload. A body that implements io.Seeker is hashed and rewound; anything else is buffered in memory. WithUnsignedPayload streams instead, at the cost of the signature no longer covering the body — use it only over https. A stream that reports no length goes out chunked, which AWS rejects for PutObject, so pass s3.WithContentLength(n) with it.
  • No connection reuse on TinyGo. The https transport opens a connection per request, so every call pays a TLS handshake.
  • Credentials are static or environment values. No shared credentials file, no SSO, no IMDS.
  • No versioning, ACL, tagging, or lifecycle APIs.

Testing

Unit tests run against a fake endpoint under both build configurations:

go test ./storage/s3/
go test -tags force_tinygo_logic ./storage/s3/

Integration tests need an S3-compatible endpoint, for example RustFS:

docker run -d --name rustfs -p 19000:9000 -e RUSTFS_ACCESS_KEY=rustfsadmin -e RUSTFS_SECRET_KEY=rustfsadmin -e RUSTFS_VOLUMES=/data rustfs/rustfs
S3_TEST_ENDPOINT=http://127.0.0.1:19000 S3_TEST_ACCESS_KEY=rustfsadmin S3_TEST_SECRET_KEY=rustfsadmin go test ./storage/s3/

Documentation

Overview

Package s3 is an S3 client that builds with TinyGo.

The maintained Go clients cannot be used here. aws-sdk-go-v2 reaches for the full net/http.Transport API, which TinyGo declares as an empty struct, and its transport layer imports net/http/httputil, which does not compile under TinyGo at all. This package therefore speaks the S3 REST API directly: a SigV4 signer, request builders, and XML decoding, over whichever HTTP stack the build selects.

client, err := s3.New(
	s3.WithRegion("ap-northeast-1"),
	s3.WithCredentials(s3.Credentials{AccessKeyID: id, SecretAccessKey: secret}),
)
obj, err := client.Get(ctx, "bucket", "photos/cat.jpg")
defer obj.Body.Close()

Implementation selection

Signing, request building, and response decoding are shared code: the two builds differ only in how a request reaches the network.

  • standard Go builds use net/http and crypto/tls
  • TinyGo builds use github.com/shibukawa/tinygodriver/https, which performs TLS through the TLS stack of the host OS
  • go build -tags force_tinygo_logic selects the TinyGo path under host Go, which is how that path is tested without a TinyGo toolchain

Neither build follows redirects through http.Client, because a redirected request must be signed again for its new host. Redirects are handled here instead, so a bucket in another region works the same way on both builds.

Credentials

Credentials are static values or environment variables. There is no shared credentials file, no SSO, and no IMDS lookup:

s3.WithCredentials(s3.Credentials{AccessKeyID: id, SecretAccessKey: secret})
s3.WithCredentialsFromEnv() // AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, ...

New reads the environment when no credentials option is given.

Scope

Put sends one request and is bounded by what the endpoint accepts in a single PUT (5 GiB on AWS). Above that, CreateMultipartUpload, UploadPart, CompleteMultipartUpload and AbortMultipartUpload are the operations; nothing here splits a stream into parts, since the caller knows the part boundaries and the failure policy.

Presign is the one operation that makes no request: it returns a SigV4 query-signed URL for a GET, PUT, HEAD or DELETE, so a browser can talk to the bucket directly for a bounded time while the application holds the credentials.

Index

Constants

View Source
const (
	MinPartNumber = 1
	MaxPartNumber = 10000
)

Part number bounds, which S3 fixes. A part below the 5 MiB minimum is refused by the endpoint at completion, not here: the last part may be smaller, and only the endpoint knows which one that is.

View Source
const (
	DefaultPresignExpiry = 15 * time.Minute
	MaxPresignExpiry     = 7 * 24 * time.Hour
)

Presign expiry bounds. S3 rejects X-Amz-Expires above seven days; the default is the one the AWS SDKs use.

View Source
const Backend = aws.Backend

Backend identifies the HTTP stack selected by build constraints: "net/http" on standard Go builds, "https" on TinyGo builds.

View Source
const DefaultTimeout = 60 * time.Second

DefaultTimeout bounds one logical operation when no timeout is configured.

Variables

View Source
var (
	ErrNoSuchKey      = errors.New("s3: no such key")
	ErrNoSuchBucket   = errors.New("s3: no such bucket")
	ErrAccessDenied   = errors.New("s3: access denied")
	ErrBucketExists   = errors.New("s3: bucket already exists")
	ErrBucketNotEmpty = errors.New("s3: bucket not empty")
	ErrInvalidRange   = errors.New("s3: requested range not satisfiable")
	ErrBadCredentials = errors.New("s3: credentials rejected")
	ErrNoSuchUpload   = errors.New("s3: no such multipart upload")
	ErrInvalidPart    = errors.New("s3: invalid part")

	// Configuration failures are the shared ones, so errors.Is matches whether
	// the caller compares against s3 or cloud/aws.
	ErrNoCredentials   = aws.ErrNoCredentials
	ErrNoRegion        = aws.ErrNoRegion
	ErrTooManyRedirect = errors.New("s3: too many redirects")

	// ErrPresignExpiry reports a Presign expiry S3 would refuse: negative, or
	// more than MaxPresignExpiry.
	ErrPresignExpiry = errors.New("s3: presign expiry out of range")
)

Sentinel errors. S3 error codes are mapped onto them so application code can branch with errors.Is without matching strings.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to one S3 endpoint. It is safe for concurrent use.

func New

func New(opts ...Option) (*Client, error)

New builds a Client. Region, endpoint, and credentials fall back to the environment, so a configured shell needs no options at all.

func (*Client) AbortMultipartUpload added in v1.2.12

func (c *Client) AbortMultipartUpload(ctx context.Context, upload MultipartUpload) error

AbortMultipartUpload discards upload and every part it holds. Aborting an upload that no longer exists is ErrNoSuchUpload.

func (*Client) CompleteMultipartUpload added in v1.2.12

func (c *Client) CompleteMultipartUpload(ctx context.Context, upload MultipartUpload, parts []CompletedPart) (*PutResult, error)

CompleteMultipartUpload assembles the parts, in the order given, into the object. parts must be in ascending part number order, which is what S3 requires; each ETag is the one UploadPart reported.

S3 may answer 200 and then carry an error document in the body when the assembly fails part way, and that reply is reported as an error here, as a 4xx would be.

func (*Client) CreateBucket

func (c *Client) CreateBucket(ctx context.Context, bucket string) error

CreateBucket creates a bucket in the client's region. A bucket that already belongs to the caller reports ErrBucketExists.

func (*Client) CreateMultipartUpload added in v1.2.12

func (c *Client) CreateMultipartUpload(ctx context.Context, bucket, key string, opts ...PutOption) (*MultipartUpload, error)

CreateMultipartUpload starts an upload of key in parts. The Put options set the object's Content-Type, Content-Encoding and metadata; they are fixed here, since the parts carry only bytes.

An upload that is neither completed nor aborted keeps its parts, and AWS bills them, until a lifecycle rule removes it. Abort on every failure path.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, bucket, key string) error

Delete removes an object. Deleting a key that does not exist succeeds, which is how S3 itself behaves.

func (*Client) DeleteBucket

func (c *Client) DeleteBucket(ctx context.Context, bucket string) error

DeleteBucket removes an empty bucket.

func (*Client) Endpoint

func (c *Client) Endpoint() string

Endpoint reports the endpoint URL in use.

func (*Client) Get

func (c *Client) Get(ctx context.Context, bucket, key string) (*Object, error)

Get retrieves an object. The caller closes Object.Body.

func (*Client) GetRange

func (c *Client) GetRange(ctx context.Context, bucket, key string, offset, length int64) (*Object, error)

GetRange retrieves length bytes starting at offset. A length of zero or less reads to the end of the object.

func (*Client) Head

func (c *Client) Head(ctx context.Context, bucket, key string) (*ObjectInfo, error)

Head reports an object's metadata without transferring it.

func (*Client) List

func (c *Client) List(ctx context.Context, bucket string, opts ...ListOption) (*ListResult, error)

List returns one page of a bucket listing. A truncated page carries NextToken, which WithContinuationToken feeds back to fetch the next one.

func (*Client) Presign added in v1.2.12

func (c *Client) Presign(ctx context.Context, bucket, key string, opts PresignOptions) (*url.URL, error)

Presign returns a URL that authorizes one request against key without the caller's credentials, signed through SigV4 query parameters. The endpoint, region, addressing style and credentials are the ones Get and Put use, so a URL for an S3-compatible server needs no second configuration.

The body is not covered: the signature carries UNSIGNED-PAYLOAD, because whoever sends the request never sees the credentials and the signer never sees the body. Only the host and the headers named in opts are signed.

Presigning is a pure function of the client and its arguments; ctx is here so the signature matches the rest of the client, and it goes unused.

func (*Client) Put

func (c *Client) Put(ctx context.Context, bucket, key string, body io.Reader, opts ...PutOption) (*PutResult, error)

Put stores body under key.

There is no multipart upload, so the whole object is sent in one request and the endpoint's single-request limit applies (5 GiB on AWS).

SigV4 signs a hash of the body, so the body must be read twice. A body that implements io.Seeker (an *os.File, a *bytes.Reader) is hashed and rewound; anything else is buffered in memory. Configure WithUnsignedPayload to stream instead, at the cost of the signature no longer covering the body.

func (*Client) Region

func (c *Client) Region() string

Region reports the signing region, which redirect handling may have updated.

func (*Client) UploadPart added in v1.2.12

func (c *Client) UploadPart(ctx context.Context, upload MultipartUpload, partNumber int, body io.Reader, opts ...PutOption) (*CompletedPart, error)

UploadPart sends one part of upload. Part numbers run from MinPartNumber to MaxPartNumber and need not be contiguous or in order; the list handed to CompleteMultipartUpload decides the object's order. Sending a number twice replaces the earlier part.

The body follows Put's rules: a seekable body is hashed and rewound, anything else is buffered, and WithUnsignedPayload streams. WithContentLength is the one Put option that applies here.

type CompletedPart added in v1.2.12

type CompletedPart struct {
	PartNumber int
	ETag       string
}

CompletedPart is what UploadPart reports and CompleteMultipartUpload needs: the part's number and the ETag the endpoint assigned it.

type Credentials

type Credentials = aws.Credentials

Credentials are the values SigV4 signs with. It is an alias, so credentials built here work with any other client in this repository.

func CredentialsFromEnv

func CredentialsFromEnv() Credentials

CredentialsFromEnv reads AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN.

type Error

type Error struct {
	Op         string // "Get", "Put", "List", ...
	Bucket     string
	Key        string
	StatusCode int
	Code       string
	Message    string
	RequestID  string
	// contains filtered or unexported fields
}

Error is a failed S3 operation. Code and Message come from the XML error document when the endpoint sent one.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the sentinel this failure maps onto, so errors.Is works.

type ListOption

type ListOption func(*listConfig)

ListOption configures a single List call.

func WithContinuationToken

func WithContinuationToken(token string) ListOption

WithContinuationToken resumes a truncated listing from ListResult.NextToken.

func WithDelimiter

func WithDelimiter(delimiter string) ListOption

WithDelimiter groups keys sharing a prefix up to the delimiter, reporting them in ListResult.CommonPrefixes. Use "/" to walk a listing like a directory tree.

func WithMaxKeys

func WithMaxKeys(maxKeys int) ListOption

WithMaxKeys caps how many keys one page returns.

func WithPrefix

func WithPrefix(prefix string) ListOption

WithPrefix limits the listing to keys with this prefix.

func WithStartAfter

func WithStartAfter(key string) ListOption

WithStartAfter begins the listing after this key.

type ListResult

type ListResult struct {
	Objects        []ObjectInfo
	CommonPrefixes []string
	IsTruncated    bool

	// NextToken continues a truncated listing through WithContinuationToken.
	NextToken string
}

ListResult is one page of a listing.

type MultipartUpload added in v1.2.12

type MultipartUpload struct {
	Bucket   string
	Key      string
	UploadID string
}

MultipartUpload identifies an upload in progress. CreateMultipartUpload returns one, and the other multipart calls take it back, so the bucket, key and upload ID cannot drift apart between calls.

type Object

type Object struct {
	ObjectInfo
	Body io.ReadCloser
}

Object is an object body together with its metadata. Body must be closed.

type ObjectInfo

type ObjectInfo struct {
	Key             string
	Size            int64
	ETag            string
	ContentType     string
	ContentEncoding string
	LastModified    time.Time

	// Metadata holds user metadata, with the x-amz-meta- prefix removed.
	Metadata map[string]string
}

ObjectInfo is the metadata S3 reports for an object.

type Option

type Option func(*config)

Option configures a Client.

func WithCredentials

func WithCredentials(creds Credentials) Option

WithCredentials sets static credentials.

func WithCredentialsFromEnv

func WithCredentialsFromEnv() Option

WithCredentialsFromEnv reads credentials from the environment. New does this already when no credentials option is given; the option states it explicitly.

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint overrides the endpoint URL, for S3-compatible servers such as RustFS or MinIO. It defaults to AWS_ENDPOINT_URL_S3, AWS_ENDPOINT_URL, or the regional AWS endpoint.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient supplies the http.Client to use.

Its CheckRedirect must return http.ErrUseLastResponse on standard Go builds: a redirected request has to be signed again for its new host, which this package does itself.

func WithPathStyle

func WithPathStyle(pathStyle bool) Option

WithPathStyle selects between https://endpoint/bucket/key and https://bucket.endpoint/key addressing.

The default is virtual-host addressing for amazonaws.com endpoints and path addressing everywhere else, which is what S3-compatible servers expect.

func WithRegion

func WithRegion(region string) Option

WithRegion sets the signing region. It defaults to AWS_REGION, then AWS_DEFAULT_REGION.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout bounds one logical operation, including redirects and reading the response body. Zero means DefaultTimeout.

func WithUnsignedPayload

func WithUnsignedPayload(unsigned bool) Option

WithUnsignedPayload signs requests with UNSIGNED-PAYLOAD instead of the body hash, so Put streams a body it cannot rewind instead of buffering it.

The request headers stay signed. Use it only over https, where TLS protects the body that the signature no longer covers.

type PresignOptions added in v1.2.12

type PresignOptions struct {
	// Method is the HTTP method the URL authorizes: GET, PUT, HEAD or
	// DELETE. Empty means GET.
	Method string

	// Expires bounds the URL's life, written as X-Amz-Expires in whole seconds
	// and rounded up. Zero means DefaultPresignExpiry; more than
	// MaxPresignExpiry, which S3 would reject, is ErrPresignExpiry.
	Expires time.Duration

	// ContentType, when set, is signed, so the sender must send exactly it.
	ContentType string

	// Headers are further request headers to sign. Each is a header the
	// sender must reproduce exactly, so a PUT that stores Content-Disposition
	// or x-amz-meta-* metadata names them here, and a plain GET names none: a
	// link in a page cannot add a header.
	Headers map[string]string

	// Query adds parameters to the URL, signed with it. On a GET,
	// response-content-disposition and response-content-type shape the reply
	// without asking the browser for a header; on a part upload, uploadId and
	// partNumber address the part.
	Query map[string]string
}

PresignOptions describe the request a presigned URL authorizes.

type PutOption

type PutOption func(*putConfig)

PutOption configures a single Put.

func WithContentEncoding

func WithContentEncoding(encoding string) PutOption

WithContentEncoding sets the object's Content-Encoding, for a body that is already compressed.

func WithContentLength

func WithContentLength(length int64) PutOption

WithContentLength states the body size for a stream the package cannot measure, which matters together with WithUnsignedPayload: without a length, the body goes out chunked, and AWS rejects a chunked PutObject.

func WithContentType

func WithContentType(contentType string) PutOption

WithContentType sets the object's Content-Type.

func WithMetadata

func WithMetadata(metadata map[string]string) PutOption

WithMetadata attaches user metadata, sent as x-amz-meta-* headers.

type PutResult

type PutResult struct {
	ETag      string
	VersionID string
}

PutResult reports what the endpoint stored.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL