Documentation
¶
Overview ¶
Package s3pe is a small, dependable client for S3-compatible object stores (AWS S3, Garage, MinIO, Ceph, ...). Standard library only, no cgo.
Nine verbs, one client, nothing else to learn:
ctx := context.Background() manifest, err := s3pe.ReadS3(ctx, "s3://my-bucket/data/manifest.json") _, err = s3pe.WriteS3(ctx, "s3://my-bucket/reports/figure1.png", png, "") objects, err := s3pe.ListS3(ctx, "s3://my-bucket/data/") _, err = s3pe.UploadS3(ctx, "sales.parquet", "s3://my-bucket/etl/", "") _, err = s3pe.DownloadS3(ctx, "s3://my-bucket/etl/sales.parquet", "sales.parquet") ok, err := s3pe.ExistsS3(ctx, "s3://my-bucket/etl/manifest.json") meta, err := s3pe.HeadS3(ctx, "s3://my-bucket/etl/manifest.json") err = s3pe.DeleteS3(ctx, "s3://my-bucket/etl/old.parquet") buckets, err := s3pe.BucketsS3(ctx)
And, since 0.3.0, a few more: CopyS3 (server-side copy), DeleteManyS3 (1000 keys per request), ListDirS3 (one level: objects and "folders"), PresignS3 (a URL that works without credentials for a while), SyncUpS3 and SyncDownS3 (a directory and a prefix kept equal by ETag, a manifest last). Client.WriteWith and Client.UploadWith take PutOptions: user metadata (x-amz-meta-*) and any other header, so Cache-Control, storage classes and server-side encryption headers reach stores that support them. Bodies above 64 MiB go up as a multipart upload, 16 MiB parts, four at a time, aborted on failure, with the final ETag checked (configurable in Config).
The package-level verbs share a client built from the environment. An explicit Client takes the same settings in a Config; a default bucket makes keys short:
s3, err := s3pe.New(s3pe.Config{Endpoint: "https://s3.example.org", Bucket: "my-bucket"})
data, err := s3.Read(ctx, "data/manifest.json")
Configuration ¶
Anything not set in Config comes from the environment, then from the defaults:
endpoint S3PE_ENDPOINT, AWS_ENDPOINT_URL required region AWS_REGION, AWS_DEFAULT_REGION us-east-1 credentials AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN required bucket S3PE_BUCKET none: use s3://bucket/key multipart S3PE_MULTIPART_THRESHOLD, S3PE_PART_SIZE (bytes), S3PE_PARALLEL 64 MiB, 16 MiB, 4 quiet S3PE_QUIET off
URLs are always path-style (https://host/bucket/key); a key may be an s3://bucket/key URL, which names the bucket for that call.
What "dependable" means here ¶
Requests are signed with AWS Signature V4 over the real SHA-256 of the payload (no aws-chunked, which some stores, Garage among them, reject); every x-amz-* header is part of the signature. Connection failures and HTTP 429/500/502/503/504 are retried twice, with a 1 s then 2 s pause; other errors are returned at once, typed: errors.Is(err, ErrNotFound), ErrAccessDenied, ErrConfig, ErrIntegrity, ErrTransport, ErrHTTP, with the details in an *Error. After an upload the server's ETag is compared with the local MD5 (part by part, and as a whole, for multipart uploads); after a download the byte count is compared with Content-Length and, when the ETag is an MD5, with the MD5 of what arrived. Downloads go to a .s3pe-part file renamed when complete. One line per transfer goes to the logger (standard error by default):
↑ s3://my-bucket/etl/sales.parquet (12.3 MB, 0.8 s)
Out of scope, on purpose: ACLs (manage access with keys and policies) and object versioning.
This is the Go edition of s3pe; the same package exists as a Rust crate with a command line (cargo install s3pe), for Python (pip install s3pe) and for R (install.packages("s3pe")). All four follow one specification and reproduce the same signatures.
Example ¶
Endpoint and credentials come from the environment; keys are s3:// URLs.
package main
import (
"context"
"fmt"
"log"
"github.com/StrategicProjects/s3pe"
)
func main() {
ctx := context.Background()
manifest, err := s3pe.ReadS3(ctx, "s3://my-bucket/data/manifest.json")
if err != nil {
log.Fatal(err)
}
fmt.Println(len(manifest))
if _, err := s3pe.WriteS3(ctx, "s3://my-bucket/reports/table1.csv", []byte("a,b\n1,2\n"), ""); err != nil {
log.Fatal(err)
}
objects, err := s3pe.ListS3(ctx, "s3://my-bucket/data/")
if err != nil {
log.Fatal(err)
}
for _, o := range objects {
fmt.Println(o.Size, o.Key)
}
}
Output:
Index ¶
- Constants
- Variables
- func ContentTypeFor(key string) string
- func DeleteManyS3(ctx context.Context, keys []string) error
- func DeleteS3(ctx context.Context, key string) error
- func ExistsS3(ctx context.Context, key string) (bool, error)
- func ExpectedETag(path string, threshold, partSize int64) (string, error)
- func FormatSize(n int64) string
- func MultipartETag(partMD5sHex []string) string
- func PresignS3(key, method string, expires time.Duration) (string, error)
- func ReadS3(ctx context.Context, key string) ([]byte, error)
- type Bucket
- type Client
- func (c *Client) Bucket() string
- func (c *Client) Buckets(ctx context.Context) ([]Bucket, error)
- func (c *Client) Copy(ctx context.Context, from, to string) (Transfer, error)
- func (c *Client) Delete(ctx context.Context, key string) error
- func (c *Client) DeleteMany(ctx context.Context, keys []string) error
- func (c *Client) Download(ctx context.Context, key, path string) (Transfer, error)
- func (c *Client) Endpoint() string
- func (c *Client) Exists(ctx context.Context, key string) (bool, error)
- func (c *Client) Head(ctx context.Context, key string) (*Object, error)
- func (c *Client) List(ctx context.Context, prefix string) ([]Object, error)
- func (c *Client) ListDir(ctx context.Context, prefix string) (Listing, error)
- func (c *Client) ListWith(ctx context.Context, prefix, delimiter string) (Listing, error)
- func (c *Client) Presign(key, method string, expires time.Duration) (string, error)
- func (c *Client) Read(ctx context.Context, key string) ([]byte, error)
- func (c *Client) Resolve(key string) (bucket, objectKey string, err error)
- func (c *Client) SyncDown(ctx context.Context, prefix, dir string, opts SyncOptions) (SyncReport, error)
- func (c *Client) SyncUp(ctx context.Context, dir, prefix string, opts SyncOptions) (SyncReport, error)
- func (c *Client) Upload(ctx context.Context, path, key, contentType string) (Transfer, error)
- func (c *Client) UploadWith(ctx context.Context, path, key string, opts PutOptions) (Transfer, error)
- func (c *Client) Write(ctx context.Context, key string, data []byte, contentType string) (Transfer, error)
- func (c *Client) WriteWith(ctx context.Context, key string, data []byte, opts PutOptions) (Transfer, error)
- type Config
- type Error
- type Listing
- type Object
- type PutOptions
- type SyncOptions
- type SyncReport
- type Transfer
- func CopyS3(ctx context.Context, from, to string) (Transfer, error)
- func DownloadS3(ctx context.Context, key, path string) (Transfer, error)
- func UploadS3(ctx context.Context, path, key, contentType string) (Transfer, error)
- func WriteS3(ctx context.Context, key string, data []byte, contentType string) (Transfer, error)
Examples ¶
Constants ¶
const ( // DefaultRegion is the signing region used when none is given. DefaultRegion = "us-east-1" // DefaultTimeout is the time one call may take before it is abandoned. DefaultTimeout = 300 * time.Second // DefaultMaxTries is the number of attempts per call: the first and two retries. DefaultMaxTries = 3 // DefaultMultipartThreshold: bodies larger than this go up as a multipart upload (D15). DefaultMultipartThreshold int64 = 64 * 1024 * 1024 // DefaultPartSize is the size of each part of a multipart upload (D15). DefaultPartSize int64 = 16 * 1024 * 1024 // DefaultParallel is the number of parts sent at the same time (D15). DefaultParallel = 4 // MinPartSize is the smallest part S3 accepts, except for the last one. MinPartSize int64 = 5 * 1024 * 1024 // MaxPresignSecs is the longest validity of a pre-signed URL (seven days). MaxPresignSecs int64 = 7 * 24 * 3600 )
Variables ¶
var ( // ErrConfig: missing endpoint, credentials or bucket, bad endpoint. ErrConfig = errors.New("configuration") // ErrTransport: the server could not be reached, or the connection broke (retried first). ErrTransport = errors.New("transport") // ErrNotFound: the object or bucket does not exist (HTTP 404). ErrNotFound = errors.New("not found") // ErrAccessDenied: the credentials do not allow this (HTTP 403). ErrAccessDenied = errors.New("access denied") // ErrHTTP: any other HTTP error; the *Error carries status, code and message. ErrHTTP = errors.New("http") // ErrIntegrity: the bytes that arrived are not the bytes that were sent. ErrIntegrity = errors.New("integrity") )
Sentinel errors, to be tested with errors.Is.
Functions ¶
func ContentTypeFor ¶
ContentTypeFor is the content type a key's extension suggests (application/octet-stream when unknown). Every edition of s3pe uses the same table.
func DeleteManyS3 ¶ added in v0.3.0
DeleteManyS3 deletes several objects in batches, with a client built from the environment.
func ExistsS3 ¶
ExistsS3 reports whether an object exists, with a client built from the environment.
func ExpectedETag ¶ added in v0.3.0
ExpectedETag is the ETag the store would give a local file uploaded by this client: its MD5, or the multipart ETag (D15) when the file is larger than threshold, with parts of partSize.
func FormatSize ¶
FormatSize renders a byte count with SI units and one decimal: "12.3 MB", "1.2 kB", "512 B".
func MultipartETag ¶ added in v0.3.0
MultipartETag is the ETag S3 gives a multipart upload: md5(concat(part MD5 bytes))-N, from the hex MD5s of the parts.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a configured connection to one S3 endpoint. Every method takes a key that is either relative to the client's bucket or a full s3://bucket/key URL. A Client is safe for concurrent use.
func New ¶
New builds a client from cfg, the environment and the defaults. It fails without an endpoint, without credentials or with an endpoint that is not scheme://host[:port].
Example ¶
An explicit client, with a default bucket, keeps the keys short.
package main
import (
"context"
"log"
"github.com/StrategicProjects/s3pe"
)
func main() {
s3, err := s3pe.New(s3pe.Config{
Endpoint: "https://s3.example.org",
AccessKey: "AKIA…",
SecretKey: "…",
Bucket: "my-bucket",
})
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
if _, err := s3.Upload(ctx, "sales.parquet", "etl/sales.parquet", ""); err != nil {
log.Fatal(err)
}
if _, err := s3.Download(ctx, "etl/sales.parquet", "copy.parquet"); err != nil {
log.Fatal(err)
}
}
Output:
func (*Client) Copy ¶ added in v0.3.0
Copy copies an object inside the store (CopyObject, D17). Both keys may be s3:// URLs; the copy keeps the source's metadata.
Example ¶
A server-side copy keeps the source's metadata.
package main
import (
"context"
"fmt"
"log"
"github.com/StrategicProjects/s3pe"
)
func main() {
s3, err := s3pe.FromEnv()
if err != nil {
log.Fatal(err)
}
t, err := s3.Copy(context.Background(), "s3://my-bucket/etl/sales.parquet", "s3://my-bucket/archive/sales-2026.parquet")
if err != nil {
log.Fatal(err)
}
fmt.Println(t.URL())
}
Output:
func (*Client) DeleteMany ¶ added in v0.3.0
DeleteMany deletes several objects with DeleteObjects, 1000 per request (D17). Keys may name different buckets. Any object the store could not delete is reported in one error of kind ErrHTTP with code DeleteErrors.
func (*Client) Download ¶
Download streams an object to a file. The bytes go to path + ".s3pe-part", renamed when complete, so a failure leaves nothing half-written at path. Missing parent directories are created.
func (*Client) List ¶
List lists every object under a prefix (all pages). prefix may be an s3://bucket/prefix URL, a bare prefix, or "" for the whole bucket.
func (*Client) ListDir ¶ added in v0.3.0
ListDir lists one level: the objects directly under the prefix and the common prefixes ("folders") below it, with the delimiter "/" (D17).
func (*Client) Presign ¶ added in v0.3.0
Presign returns a URL that reads (GET) or writes (PUT) the object without credentials for expires, at most seven days (D16).
Example ¶
A pre-signed URL lets someone without credentials read the object for a while.
package main
import (
"fmt"
"log"
"time"
"github.com/StrategicProjects/s3pe"
)
func main() {
s3, err := s3pe.FromEnv()
if err != nil {
log.Fatal(err)
}
u, err := s3.Presign("s3://my-bucket/reports/report.pdf", "GET", time.Hour)
if err != nil {
log.Fatal(err)
}
fmt.Println(u)
}
Output:
func (*Client) Read ¶
Read reads an object into memory.
Example ¶
A missing object is a typed error.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/StrategicProjects/s3pe"
)
func main() {
s3, err := s3pe.FromEnv()
if err != nil {
log.Fatal(err)
}
_, err = s3.Read(context.Background(), "s3://my-bucket/does-not-exist.json")
fmt.Println(errors.Is(err, s3pe.ErrNotFound))
}
Output:
func (*Client) Resolve ¶
Resolve splits s3://bucket/key or applies the default bucket to a bare key.
func (*Client) SyncDown ¶ added in v0.3.0
func (c *Client) SyncDown(ctx context.Context, prefix, dir string, opts SyncOptions) (SyncReport, error)
SyncDown downloads every object under prefix whose local copy is missing or differs; with Delete, removes local files that are not remote (D18).
func (*Client) SyncUp ¶ added in v0.3.0
func (c *Client) SyncUp(ctx context.Context, dir, prefix string, opts SyncOptions) (SyncReport, error)
SyncUp uploads every file under dir whose remote ETag differs from the local one or which is missing remotely; see SyncOptions (D18).
Example ¶
SyncUp uploads what changed, the manifest last.
package main
import (
"context"
"fmt"
"log"
"github.com/StrategicProjects/s3pe"
)
func main() {
s3, err := s3pe.FromEnv()
if err != nil {
log.Fatal(err)
}
r, err := s3.SyncUp(context.Background(), "./out", "s3://my-bucket/reports/", s3pe.SyncOptions{Last: "manifest.json"})
if err != nil {
log.Fatal(err)
}
fmt.Println(len(r.Transferred), "uploaded,", len(r.Skipped), "unchanged")
}
Output:
func (*Client) Upload ¶
Upload streams a file to an object. A key that is empty or ends in "/" gets the file's base name appended. An empty contentType is guessed from the key. Files above the multipart threshold go up in parts.
func (*Client) UploadWith ¶ added in v0.3.0
func (c *Client) UploadWith(ctx context.Context, path, key string, opts PutOptions) (Transfer, error)
UploadWith streams a file to an object with metadata and headers (D17).
func (*Client) Write ¶
func (c *Client) Write(ctx context.Context, key string, data []byte, contentType string) (Transfer, error)
Write writes bytes to an object. An empty contentType is guessed from the key's extension. Bodies above the multipart threshold go up in parts.
func (*Client) WriteWith ¶ added in v0.3.0
func (c *Client) WriteWith(ctx context.Context, key string, data []byte, opts PutOptions) (Transfer, error)
WriteWith writes bytes to an object with metadata and headers (D17).
Example ¶
Metadata and extra headers travel with the object; large bodies go up in parts.
package main
import (
"context"
"log"
"github.com/StrategicProjects/s3pe"
)
func main() {
s3, err := s3pe.FromEnv()
if err != nil {
log.Fatal(err)
}
_, err = s3.WriteWith(context.Background(), "s3://my-bucket/reports/report.pdf", []byte("%PDF"), s3pe.PutOptions{
Metadata: map[string]string{"author": "ana"},
Headers: map[string]string{"Cache-Control": "max-age=3600"},
})
if err != nil {
log.Fatal(err)
}
}
Output:
type Config ¶
type Config struct {
// Endpoint is scheme://host[:port], no path. Required.
Endpoint string
// Region is the signing region (default us-east-1; on AWS, the bucket's region).
Region string
// AccessKey and SecretKey are required, here or in the environment.
AccessKey, SecretKey string
// SessionToken is for temporary credentials.
SessionToken string
// Bucket is used when a key is not an s3:// URL.
Bucket string
// Timeout limits one call (default 300 s).
Timeout time.Duration
// MaxTries is the number of attempts per call (default 3).
MaxTries int
// MultipartThreshold: bodies above it go up in parts (default 64 MiB).
MultipartThreshold int64
// PartSize of a multipart upload, at least 5 MiB (default 16 MiB).
PartSize int64
// Parallel is the number of parts sent at the same time (default 4,
// or S3PE_PARALLEL).
Parallel int
// HTTPClient replaces the default one (which follows no redirects).
HTTPClient *http.Client
// Logger receives one line per transfer (default: standard error).
Logger *log.Logger
// Quiet silences the transfer lines; warnings about retries still go out.
Quiet bool
}
Config configures a Client. Zero fields are taken from the environment (S3PE_ENDPOINT or AWS_ENDPOINT_URL, AWS_REGION or AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, S3PE_BUCKET, S3PE_MULTIPART_THRESHOLD, S3PE_PART_SIZE, S3PE_PARALLEL, S3PE_QUIET) and then from the defaults.
type Error ¶
type Error struct {
// Status is the HTTP status, 0 when no response arrived.
Status int
// Code is the S3 error code (NoSuchKey, AccessDenied, …), empty if none.
Code string
// Message is the server's or the client's explanation.
Message string
// Bucket and Key name what was asked for (empty for bucket-level calls).
Bucket, Key string
// contains filtered or unexported fields
}
Error is what every failed call returns. errors.Is(err, ErrNotFound) and the other sentinels tell the kind; errors.As gives the details.
type Listing ¶ added in v0.3.0
type Listing struct {
// Objects directly under the prefix.
Objects []Object
// Prefixes are the common prefixes, each ending with the delimiter.
Prefixes []string
}
Listing is one level of a bucket: the objects directly under a prefix and the common prefixes ("folders") below it (D17).
type Object ¶
type Object struct {
// Key inside the bucket.
Key string
// Size in bytes.
Size int64
// LastModified is zero when the server's date could not be parsed.
LastModified time.Time
// ETag without the surrounding quotes.
ETag string
// ContentType is only known after Head.
ContentType string
// Metadata is the user metadata (x-amz-meta-*, names without the
// prefix, lower case), only known after Head.
Metadata map[string]string
}
Object is one object as listed or described by the store.
type PutOptions ¶ added in v0.3.0
type PutOptions struct {
// ContentType; guessed from the key when empty.
ContentType string
// Metadata is user metadata, sent as x-amz-meta-<name>.
Metadata map[string]string
// Headers are any other headers (Cache-Control, Content-Disposition,
// x-amz-storage-class, x-amz-server-side-encryption, …). x-amz-* headers
// are signed, the rest are not.
Headers map[string]string
}
PutOptions are the options of WriteWith and UploadWith (D17).
type SyncOptions ¶ added in v0.3.0
type SyncOptions struct {
// Delete removes what exists on the destination but not on the source.
Delete bool
// Last is the relative path of the file to transfer last (a manifest).
Last string
}
SyncOptions are the options of SyncUp and SyncDown (D18).
type SyncReport ¶ added in v0.3.0
type SyncReport struct {
// Transferred are the relative paths uploaded or downloaded.
Transferred []string
// Skipped are the relative paths already identical on both sides.
Skipped []string
// Deleted are the relative paths removed from the destination.
Deleted []string
// Elapsed is the wall time.
Elapsed time.Duration
}
SyncReport is what a sync did.
func SyncDownS3 ¶ added in v0.3.0
func SyncDownS3(ctx context.Context, prefix, dir string, opts SyncOptions) (SyncReport, error)
SyncDownS3 downloads a prefix to a directory, with a client built from the environment.
func SyncUpS3 ¶ added in v0.3.0
func SyncUpS3(ctx context.Context, dir, prefix string, opts SyncOptions) (SyncReport, error)
SyncUpS3 uploads a directory to a prefix, with a client built from the environment.
type Transfer ¶
type Transfer struct {
Bucket, Key string
// Size is the number of bytes moved.
Size int64
// ETag reported by the server, without quotes.
ETag string
// Elapsed is the wall time of the whole call, retries included.
Elapsed time.Duration
}
Transfer is what a completed upload, download or copy reports.
func CopyS3 ¶ added in v0.3.0
CopyS3 copies an object inside the store, with a client built from the environment.
func DownloadS3 ¶
DownloadS3 downloads an object to a file, with a client built from the environment.