Visitar URL original
s2 package - github.com/mojatter/s2 - Go Packages

s2

package module
v0.31.1 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 15 Imported by: 0

README

S2 — Simple Storage

PkgGoDev Go Report Card

S2 is a lightweight object storage library and S3-compatible server written in Go.

Why S2?

MinIO was the go-to S3-compatible server for local development, but it entered maintenance mode in December 2025 and was archived in February 2026. S2 fills this gap with a different philosophy:

  • Drop-in MinIO replacement — Same role in docker-compose for local development.
  • Unified Storage interface — Stop reinventing the S3-vs-local-filesystem swap in every project.

Features

  • S3-Compatible Server — Enough for local development, with osfs and memfs out of the box
  • Web Console — Built-in browser interface for managing buckets and objects
  • Lightweight — Minimal dependencies, single binary, go install ready
  • Unified Storage Interface — One API for local filesystem, in-memory, AWS S3, Google Cloud Storage, and Azure Blob Storage
  • Pluggable Backends — Register storage implementations with a blank import

S2 Web Console

Migrating from MinIO

For most local-development use cases, replacing MinIO with S2 is a one-line change in docker-compose.yml. S2 serves the S3 API at the root path on :9000 — the same endpoint layout MinIO uses — so existing S3 clients need no changes. The Web Console runs on a dedicated port (:9001 by default) so that the S3 API owns the root path cleanly.

docker-compose.yml

services:
  s2:
    image: mojatter/s2-server
    ports:
      - "9000:9000" # S3 API
      - "9001:9001" # Web Console
    environment:
      S2_SERVER_USER: myuser
      S2_SERVER_PASSWORD: mypassword
      S2_SERVER_BUCKETS: assets,uploads
    volumes:
      - s2-data:/var/lib/s2

volumes:
  s2-data:

Environment variable mapping

MinIO S2
MINIO_ROOT_USER S2_SERVER_USER
MINIO_ROOT_PASSWORD S2_SERVER_PASSWORD
MINIO_VOLUMES S2_SERVER_ROOT (default /var/lib/s2)
MINIO_DEFAULT_BUCKETS S2_SERVER_BUCKETS
MINIO_BROWSER / MINIO_CONSOLE_ADDRESS S2_SERVER_CONSOLE_LISTEN (default :9001; empty disables)

Migrating existing data — use any S3 client to copy data from MinIO to S2:

# Mirror an existing MinIO instance into a fresh S2 instance
aws --endpoint-url http://old-minio:9000 s3 sync s3://my-bucket /tmp/dump
aws --endpoint-url http://localhost:9000 s3 sync /tmp/dump s3://my-bucket

Or use mc mirror directly between the two endpoints.

Anatomy

S2 has two parts: S2 Server, a standalone S3-compatible daemon, and a Go library you import into your code. S2 Server embeds the library, so both share the same pluggable backends.

                   ┌───────────────────────────────┐
                   │              S2               │
                   └───────────────────────────────┘
                      │                          │
         ┌────────────┴─────────┐   ┌────────────┴─────────┐
         │      S2 Server       ├──►│      S2 library      │
         │                      │   │                      │
         │  run as S3 endpoint  │   │  import from Go      │
         │  (drop-in for MinIO) │   │  (s2 package)        │
         └──────────────────────┘   └───────┬──────────────┘
                                            │
                                            │ implemented by
                                            ▼
                    ┌──────────────────────────────────────┐
                    │   Backends                           │
                    │   osfs · memfs · s3 · gcs · azblob   │
                    └──────────────────────────────────────┘

S2 Server Storage Layout

The osfs backend stores objects as plain files on disk under the configured root directory (default /var/lib/s2 in Docker, ./data otherwise). Each bucket is a top-level directory, and object keys map directly to file paths:

$S2_SERVER_ROOT/
├── .buckets/               # per-bucket state: creation time, multipart generation (internal, not a bucket)
├── .multipart/             # in-progress multipart uploads (internal, not a bucket)
├── assets/                 # each top-level dir is a bucket
│   ├── .keep               # bucket marker
│   ├── .meta/
│   │   └── logo.png        # JSON metadata file for logo.png (etag, content_type, metadata)
│   ├── logo.png
│   └── css/
│       ├── .keep
│       ├── .meta/
│       │   └── style.css   # metadata file for css/style.css, beside it
│       └── style.css
└── uploads/
    ├── .keep
    └── photo.jpg

Before v0.18.2 the per-bucket state directory was .meta/. A root written by an older s2-server keeps its multipart generations if that directory is renamed to .buckets/ before the new version starts for the first time. docs/upgrading.md collects the steps an s2-server upgrade needs, version by version, from v0.14.0 on.

You can seed a bucket by bind-mounting a host directory — handy for serving git-tracked static assets without uploading them manually.

services:
  s2:
    image: mojatter/s2-server
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      S2_SERVER_USER: myuser
      S2_SERVER_PASSWORD: mypassword
    volumes:
      - ./public/assets:/var/lib/s2/assets   # host dir → bucket "assets"
      - s2-data:/var/lib/s2

volumes:
  s2-data:

The bind-mounted directory is auto-recognized as a bucket — no S2_SERVER_BUCKETS entry needed.

Note: s2 records a bucket's creation time under .buckets/ when it creates the bucket. A bind-mounted directory, or a bucket created by an older s2, uses its .keep file's modification time instead, and the current time without one (cosmetic only).

Install

Install server
go install github.com/mojatter/s2/cmd/s2-server@latest
Install library
go get github.com/mojatter/s2

Cloud backends live in separate modules — install only what you need:

go get github.com/mojatter/s2/s3     # AWS S3
go get github.com/mojatter/s2/gcs    # Google Cloud Storage
go get github.com/mojatter/s2/azblob # Azure Blob Storage

Note: If your code already imports a backend package (e.g. _ "github.com/mojatter/s2/s3"), go mod tidy will add the required module automatically.

Quick Start

As a local S3 server

Start the server:

# via go install
s2-server

# via Docker
docker run --rm -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/var/lib/s2 mojatter/s2-server

Then access it with any S3 client:

package main

import (
	"context"
	"fmt"

	"github.com/mojatter/s2"
	_ "github.com/mojatter/s2/s3" // registers s3 backend
)

func main() {
	ctx := context.Background()
	strg, err := s2.NewStorage(ctx, s2.Config{
		Type: s2.TypeS3,
		Root: "my-bucket",
		S3: &s2.S3Config{
			EndpointURL: "http://localhost:9000",
		},
	})
	if err != nil {
		panic(err)
	}
	res, err := strg.List(ctx, s2.ListOptions{Limit: 1000})
	if err != nil {
		panic(err)
	}
	fmt.Printf("%v\n", res.Objects)
}

Or use the AWS CLI:

aws --endpoint-url http://localhost:9000 s3 ls
aws --endpoint-url http://localhost:9000 s3 cp ./file.txt s3://my-bucket/file.txt
As a library

Define your storages in a JSON file (s2.json):

{
  "assets": { "type": "osfs", "root": "/var/data/assets" },
  "backups": { "type": "s3", "root": "my-backup-bucket" }
}

Then load and use them with s2env (go get github.com/mojatter/s2/s2env):

package main

import (
	"context"

	"github.com/mojatter/s2"
	"github.com/mojatter/s2/s2env"
)

func main() {
	ctx := context.Background()
	storages, err := s2env.Load(ctx, "s2.json")
	if err != nil {
		panic(err)
	}
	assets := storages["assets"]

	obj := s2.NewObjectBytes("hello.txt", []byte("Hello, S2!"))
	if err := assets.Put(ctx, obj); err != nil {
		panic(err)
	}
}

For per-backend configuration fields, multi-backend examples, and an explicit-import alternative (s2.LoadConfigsFile), see docs/backends.md.

In tests

For tests, swap any backend for memfs to get an isolated, in-process storage with no Docker, no temp directories, and no cleanup. The same s2.Storage interface is used in production and tests.

package mypkg_test

import (
	"context"
	"testing"

	"github.com/mojatter/s2"
	_ "github.com/mojatter/s2/fs" // registers osfs + memfs
)

func TestUploadAvatar(t *testing.T) {
	ctx := context.Background()
	strg, err := s2.NewStorage(ctx, s2.Config{Type: s2.TypeMemFS})
	if err != nil {
		t.Fatal(err)
	}

	if err := UploadAvatar(ctx, strg, "user-1", []byte("...")); err != nil {
		t.Fatal(err)
	}
	// assert via strg.Get / strg.List ...
}

s2test.StorageDelegator is a test double for s2.Storage where each method delegates to a *Func field — useful for injecting errors or capturing calls without writing a full mock.

Storage Backends

Each cloud backend is a separate Go module with its own dependencies, so go get github.com/mojatter/s2 alone does not pull in any cloud SDK.

Type Import Description
osfs github.com/mojatter/s2/fs Local filesystem storage
memfs github.com/mojatter/s2/fs In-memory filesystem (great for testing)
s3 github.com/mojatter/s2/s3 AWS S3 (and any S3-compatible service)
gcs github.com/mojatter/s2/gcs Google Cloud Storage
azblob github.com/mojatter/s2/azblob Azure Blob Storage

Backends are registered via blank imports. Import only what you need:

import (
	_ "github.com/mojatter/s2/fs"     // osfs + memfs
	_ "github.com/mojatter/s2/s3"     // AWS S3
	_ "github.com/mojatter/s2/gcs"    // Google Cloud Storage
	_ "github.com/mojatter/s2/azblob" // Azure Blob Storage
)

Backend Configuration

Each backend has its own configuration and authentication options. See docs/backends.md for details:

Storage Interface

type Storage interface {
	Type() Type
	Sub(ctx context.Context, prefix string) (Storage, error)
	List(ctx context.Context, opts ListOptions) (ListResult, error)
	Get(ctx context.Context, name string) (Object, error)
	Exists(ctx context.Context, name string) (bool, error)
	Put(ctx context.Context, obj Object) error
	PutMetadata(ctx context.Context, name string, metadata Metadata) error
	Copy(ctx context.Context, src, dst string) error
	Delete(ctx context.Context, name string) error
	DeleteRecursive(ctx context.Context, prefix string) error
	SignedURL(ctx context.Context, opts SignedURLOptions) (string, error)
}

// One List method covers flat and recursive listings, with explicit
// pagination via continuation token.
type ListOptions struct {
	Prefix     string
	After      string // opaque continuation token; empty = first page
	StartAfter string // caller-chosen key to resume after; ignored when After is set
	Limit      int    // 0 = backend default
	Recursive  bool
}

type ListResult struct {
	Objects        []Object
	CommonPrefixes []string // each ends in "/"; empty when Recursive == true
	NextAfter      string   // empty when exhausted
}

// SignedURL is method-aware so backends can issue both download and upload URLs.
type SignedURLOptions struct {
	Name   string
	Method SignedURLMethod // SignedURLGet (default) or SignedURLPut
	TTL    time.Duration
}

An object carries its content type and entity tag as attributes rather than as metadata keys, so a backend can answer them from whatever it stores natively:

type Object interface {
	Name() string
	Open() (io.ReadCloser, error)
	OpenRange(offset, length uint64) (io.ReadCloser, error)
	Length() uint64
	LastModified() time.Time
	Metadata() Metadata     // user metadata only
	ContentType() string    // "" when unknown
	ETag() string           // quoted, "" when unknown
}

Metadata holds only what the caller put there. What each backend stores and reports — including the one name s2-server still reserves on s3 and gcs roots until v1.0.0 — is in docs/backends.md.

Move is a free function rather than a method so backends do not have to implement two near-identical operations. Backends that can do better than Copy + Delete (e.g. osfs via filesystem rename) satisfy the optional s2.Mover interface, which s2.Move discovers via type assertion:

err := s2.Move(ctx, strg, "src.txt", "dst.txt")

Put reports nothing about what it stored, so a caller that needs the ETag would have to read the object back. s2.Upload avoids that: backends that can report it — all four bundled ones do — satisfy the optional s2.Uploader interface, and the free function falls back to Put followed by Get for those that do not.

res, err := s2.Upload(ctx, strg, obj, s2.UploadOptions{})
// res.ETag is what a later Get reports.

s2.Count counts the objects whose names begin with a prefix, at any depth, as a recursive List would. osfs and memfs satisfy the optional s2.Counter interface and count in one pruned walk; elsewhere the free function pages through List, which on s3, gcs and azblob costs one request per 1000 objects. UpTo stops the count once it is passed:

res, err := s2.Count(ctx, strg, s2.CountOptions{Prefix: "images/image-20260929", UpTo: 1000})
// res.Objects is exact, or 1000 with res.Truncated set when more exist.

Errors that report a missing object wrap s2.ErrNotExist; detect them with errors.Is:

if _, err := strg.Get(ctx, "missing.txt"); errors.Is(err, s2.ErrNotExist) {
	// handle not found
}

Server Configuration

Environment variables
Variable Default Description
S2_SERVER_CONFIG — Path to JSON config file
S2_SERVER_LISTEN :9000 S3 API listen address
S2_SERVER_CONSOLE_LISTEN :9001 Web Console listen address (set empty to disable the console listener)
S2_SERVER_HEALTH_PATH /healthz Health check path served on the S3 listener (set empty to disable)
S2_SERVER_TYPE osfs Storage backend type
S2_SERVER_ROOT /var/lib/s2 Root directory for bucket data
S2_SERVER_USER — Username for authentication (disables auth if empty)
S2_SERVER_PASSWORD — Password for authentication
S2_SERVER_BUCKETS — Comma-separated list of buckets to create on startup
S2_SERVER_MAX_UPLOAD_SIZE 5368709120 (16777216 on memfs) Largest object in bytes accepted by PutObject, UploadPart (and the range an UploadPartCopy copies) and a console upload
S2_SERVER_MAX_PREVIEW_SIZE 10485760 Largest text file in bytes the console previews (0 or negative uses the default)
S2_SERVER_MULTIPART_MAX_AGE 86400 Seconds an unfinished multipart upload stays usable; past it the upload is refused and its parts are reclaimed (negative keeps uploads forever)
S2_SERVER_LOG_LEVEL info Lowest level logged: debug, info, warn or error; debug adds one line per S3 API request and the detail of 4xx error responses

Environment variables take precedence over the config file.

Build tags

The server supports osfs and memfs backends by default. The official release binaries and Docker images include these two only. To enable cloud backends, install with the corresponding tags:

# Single backend
go install -tags server_gcs github.com/mojatter/s2/cmd/s2-server@latest

# All cloud backends
go install -tags server_s3,server_gcs,server_azblob github.com/mojatter/s2/cmd/s2-server@latest

The available tags are server_s3, server_gcs, server_azblob. Combine them with comma to enable multiple backends in one binary.

Authentication

Authentication is disabled by default (S2_SERVER_USER empty). When set, the server requires credentials on all routes:

  • Web Console — HTTP Basic Auth
  • S3 API — AWS Signature Version 4 (S2_SERVER_USER as the Access Key ID, S2_SERVER_PASSWORD as the Secret Access Key)
S2_SERVER_USER=myuser S2_SERVER_PASSWORD=mypassword s2-server

Using the AWS CLI:

AWS_ACCESS_KEY_ID=myuser AWS_SECRET_ACCESS_KEY=mypassword \
  aws --endpoint-url http://localhost:9000 s3 ls

Or via a named profile in ~/.aws/config:

[profile s2]
endpoint_url = http://localhost:9000
aws_access_key_id = myuser
aws_secret_access_key = mypassword
aws --profile s2 s3 ls

Presigned URLs — S2 verifies AWS SigV4 signatures passed in the query string (X-Amz-Algorithm=AWS4-HMAC-SHA256, X-Amz-Signature, …), so URLs produced by s3.NewPresignClient (Go) or s3.getSignedUrl (JavaScript) work for GET and PUT. The body of a presigned PUT is treated as UNSIGNED-PAYLOAD.

Multiple principals with scoped, IAM-style policies — beyond the single user/password pair above, the config file can define a users list, each with its own credentials and an optional AWS-IAM-shaped Policy restricting which buckets/keys/actions it may use. See docs/users-policy.md.

Config file
{
  "listen": ":9000",
  "console_listen": ":9001",
  "health_path": "/healthz",
  "type": "osfs",
  "root": "/var/lib/s2",
  "user": "myuser",
  "password": "mypassword",
  "buckets": ["assets", "uploads"]
}
s2-server -f config.json

-f takes precedence over S2_SERVER_CONFIG.

S3 API endpoints
Method Path Operation
GET / ListBuckets
PUT /{bucket} CreateBucket
HEAD /{bucket} HeadBucket
DELETE /{bucket} DeleteBucket
GET /{bucket}?location GetBucketLocation
GET /{bucket} ListObjectsV2
GET /{bucket}/{key...} GetObject (Range supported)
HEAD /{bucket}/{key...} HeadObject
GET /{bucket}/{key...}?tagging GetObjectTagging (always an empty tag set; s2 keeps no tags)
PUT /{bucket}/{key...} PutObject / CopyObject
DELETE /{bucket}/{key...} DeleteObject
POST /{bucket}?delete DeleteObjects
POST /{bucket}/{key...}?uploads CreateMultipartUpload
PUT /{bucket}/{key...}?uploadId&partNumber UploadPart / UploadPartCopy (x-amz-copy-source-range supported)
POST /{bucket}/{key...}?uploadId CompleteMultipartUpload
DELETE /{bucket}/{key...}?uploadId AbortMultipartUpload
GET /{bucket}?uploads ListMultipartUploads
GET /{bucket}/{key...}?uploadId ListParts
GET, HEAD /healthz Health check (configurable via S2_SERVER_HEALTH_PATH)

DeleteBucket refuses a bucket that still holds objects with 409 BucketNotEmpty; .keep markers, which S3 listings hide (the bucket marker and console folders), do not count, and unfinished multipart uploads do not block it. The console's delete removes the contents.

Custom metadata is supported via x-amz-meta-* headers on PutObject/CopyObject and returned on GetObject/HeadObject. Content-Type and ETag are not metadata: they are attributes the backend owns, described in docs/backends.md.

Benchmarks

Two complementary benchmark harnesses ship with S2:

  • make bench runs Go-native testing.B benchmarks against the storage layer (fs package) and the HTTP handler (server/handlers/s3api package). Good for catching regressions in a unit-test-style loop — no external binary needed.
  • make bench-warp drives a fresh in-process s2-server end-to-end with minio/warp. Good for SDK-level throughput numbers that exercise the whole stack (connection pooling, SigV4 streaming, chunked bodies, real wire HTTP). Install warp first with go install github.com/minio/warp@latest.

Numbers below are local runs on an Apple M4 (darwin/arm64, single process). Treat them as sanity checks, not competitive rankings — the most useful number is the one you get on your own hardware.

Go-native microbenchmarks

1 KiB payload, osfs backend rooted at b.TempDir(), 2-second bench time. make bench runs the full set.

Benchmark ns/op B/op allocs/op
BenchmarkPutObject (osfs) 9,444,800 40,499 91
BenchmarkGetObject (osfs) 82,947 3,034 33
BenchmarkPutObjectMemFS 7,781 39,772 128
BenchmarkGetObjectMemFS 1,375 2,192 35
BenchmarkHTTPPutObject (osfs) 9,862,317 52,248 221
BenchmarkHTTPGetObject (osfs) 140,662 15,212 182
BenchmarkHTTPPutObjectMemFS 103,358 48,527 215
BenchmarkHTTPGetObjectMemFS 55,263 14,332 170

The osfs PUT path always fsyncs the object and its metadata file before renaming each into place — those two fsyncs are roughly 7 to 8 ms of the 9.4 ms per storage-layer PUT on this machine, where one fsync of a 1 KiB file takes 3.4 to 4 ms. For apples-to-apples comparisons against benchmarks from other S3-compatible servers, make sure they are running with fsync enabled as well. Many default to write-through-page-cache and will look proportionally faster until you flip the fsync switch on.

The memfs columns exist because S2 ships an in-memory backend specifically for tests. Running on memfs makes GetObject about 60x faster and PutObject over 1000x faster than osfs on the same hardware. That speedup is what makes memfs worth reaching for in unit tests that need an S3-compatible target without Docker or a temp directory.

Listing

Listing a 1000-object page of one-byte objects with Limit: 1000, 2-second bench time. The ETag rows also read every object's metadata file, one after another or through s2.PrefetchMetadata as ListObjectsV2 does. make bench runs these too.

Benchmark ns/op B/op allocs/op
BenchmarkList (osfs) 1,936,912 709,842 6,022
BenchmarkListETag (osfs) 47,032,704 2,483,269 26,031
BenchmarkListETagPrefetch (osfs) 14,723,591 2,512,974 26,045
BenchmarkListMemFS 86,924 343,512 2,023
BenchmarkListETagMemFS 1,171,072 1,890,304 23,027
End-to-end benchmark with warp

Captured with the default make bench-warp settings (1 MiB objects, 8 concurrent clients, 30 seconds, warp mixed), osfs backend.

Operation Throughput p50 latency p99 latency
PUT 133.53 MiB/s (133.53 obj/s) 53.6 ms 73.4 ms
GET 401.30 MiB/s (401.30 obj/s) 0.5 ms 7.7 ms
STAT 267.93 obj/s 0.2 ms 5.5 ms
DELETE 89.08 obj/s 4.3 ms 14.8 ms
Total 534.83 MiB/s, 891.83 obj/s — —

You can run any other warp workload by overriding the Makefile variables, e.g. larger objects:

make bench-warp BENCH_OBJSIZE=10MiB BENCH_OBJECTS=50 BENCH_CONC=16 BENCH_TIME=60s

S3 Compatibility

S2 Server is designed to drop-in replace MinIO for:

  • ✅ Local development against aws-sdk-go, boto3, @aws-sdk/client-s3, and other S3 SDKs
  • ✅ CI/test environments using S3 via testcontainers or docker-compose
  • ✅ Small-scale production for static assets, uploads, and backups
  • ✅ Presigned URL workflows (browser uploads/downloads)
  • ✅ Multipart uploads for large objects

It is not a replacement for AWS S3 in scenarios requiring versioning, server-side encryption, bucket-level ACLs, lifecycle management, or multi-node replication. See Limitations for details.

Limitations

S2 aims to cover the parts of the S3 API that matter for local development and lightweight production use. Some features are intentionally not implemented:

  • Object versioning — VersionId, version listing, and s3:GetObjectVersion are not supported. Buckets behave as if versioning is permanently disabled.
  • ListObjectsV2 only — The legacy ListObjects (V1) API is not implemented. Most modern SDKs use V2 by default; older clients may need configuration changes.
  • Server-side encryption (SSE-S3 / SSE-KMS / SSE-C) — Not implemented. Use full-disk encryption at the OS level if needed.
  • Bucket ACLs, S3 bucket-policy JSON on individual buckets — Not implemented. S2 does support multiple principals with IAM-style Policy documents scoped by bucket/key/action (see docs/users-policy.md), but there is no per-bucket ACL or bucket-attached policy document as AWS has; access control is entirely principal-side.
  • Replication, lifecycle rules, object lock — Not implemented.
  • Subresources — PUT and DELETE naming a subresource such as ?tagging, ?acl, ?policy or ?versioning, on a bucket or an object, answer 501 NotImplemented rather than touching it; so does DELETE with ?versionId, and an x-amz-copy-source naming a ?versionId other than null, since s2 keeps no versions.

If your use case needs any of the above, S2 is probably not the right tool — consider AWS S3, Ceph RGW, or SeaweedFS.

License

MIT

Credits

The header image was generated with Google Gemini. It includes the Go Gopher mascot, originally designed by Renée French and licensed under CC BY 3.0.

Documentation

Overview

Package s2 is a lightweight object storage abstraction with multiple backends and an embeddable S3-compatible server.

Overview

s2 provides a single Storage interface that all backends implement, and a small set of value types (Object, Metadata, Config) for moving data through it. The same interface drives a local-filesystem backend (osfs), an in-memory backend (memfs), and an AWS S3 / S3-compatible backend (s3). Backends register themselves via blank import:

import (
    _ "github.com/mojatter/s2/fs" // osfs + memfs
    _ "github.com/mojatter/s2/s3" // AWS S3
)

strg, err := s2.NewStorage(ctx, s2.Config{Type: s2.TypeOSFS, Root: "/var/data"})

Errors

Operations that report a missing object wrap the sentinel ErrNotExist. Detect them with errors.Is:

if _, err := strg.Get(ctx, "foo.txt"); errors.Is(err, s2.ErrNotExist) {
    // not found
}

NewStorage wraps ErrUnknownType when the requested backend has not been registered.

Concurrency

All Storage implementations shipped with s2 are safe for concurrent use by multiple goroutines. Methods that mutate state (Put, Delete, Copy, ...) are independently atomic per object: a single Put is either fully visible to subsequent reads or not visible at all. Multiple concurrent Puts to the same object resolve to one of the writes — there is no defined ordering — but no torn writes are exposed.

An Object's attribute accessors — Name, Length, LastModified, Metadata, ContentType and ETag — are safe for concurrent use by multiple goroutines, on an Object returned by Get or List as much as on one you built, and Put leaves them alone. A backend that resolves an attribute lazily on first call must synchronize that itself; the fs backend reads its sidecar under a sync.Once.

Open and OpenRange differ by how the Object was made. One from NewObjectReader or NewObjectBytes wraps a single reader and hands the same one to every caller, so its body can be read — and therefore stored with Put — only once. One from Get, List or NewObjectFromFile opens a new reader per call.

Writing to the map Object.Metadata returns changes the in-memory Object and never the stored object, and must not be done while the Object is shared between goroutines. WithMetadata keeps the caller's map by reference, so every Object built from one map shares it — and so does the caller; pass a Metadata.Clone to hand over an independent copy. Get returns a writable map even for an object that carries no metadata. A List result reports whatever the backend holds, which may be nil: reads are safe on a nil map, writes to one panic.

PutMetadata is NOT atomic with Put. Calling Put followed by PutMetadata leaves a window during which the object exists with whatever metadata Put itself wrote.

PutMetadata takes two requests on the s3 backend, so a Put between them loses its Content-Type; on gcs the write is one request, which fails when the object changed since it was read. See each backend's PutMetadata.

Atomicity per backend

The following table summarizes the atomicity guarantees of each operation across the bundled backends.

Operation             | osfs                  | memfs                 | s3
----------------------|-----------------------|-----------------------|-----------------------
Put                   | atomic (temp+rename)  | atomic                | atomic (server-side)
Copy                  | atomic per dst        | atomic per dst        | atomic (server-side)
Move (via [Move])     | atomic (rename)       | non-atomic            | non-atomic
Delete                | atomic                | atomic                | atomic
DeleteRecursive       | best-effort, partial  | best-effort, partial  | best-effort, partial

"Best-effort, partial" means the operation deletes objects one at a time (or in pages, for s3) and may leave some objects behind on error.

Stability

s2 follows semantic versioning. Until v1.0.0 the public API may change between minor versions; breaking changes are documented in the release notes. After v1.0.0, the package will only break compatibility on a major-version bump.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInvalidName = errors.New("s2: invalid name")

ErrInvalidName is returned for a name or prefix a storage refuses: one that would resolve to something other than it spells, as "../other" does, or one the backend keeps for its own state. Detect with errors.Is:

if errors.Is(err, s2.ErrInvalidName) {
    // reject the caller's name
}
View Source
var ErrModified = errors.New("s2: object modified since Get")

ErrModified is returned by Object.Open and OpenRange once the key holds another version than the one Get or List returned; Get the key again to read it.

View Source
var ErrNotExist = errors.New("s2: object not exist")

ErrNotExist is returned when an operation targets an object that does not exist. Backends wrap this with the missing object's name via fmt.Errorf, so callers should detect it with errors.Is rather than direct equality:

if errors.Is(err, s2.ErrNotExist) {
    // handle missing object
}
View Source
var ErrPreconditionFailed = errors.New("s2: precondition failed")

ErrPreconditionFailed is returned by Upload when a condition in UploadOptions does not hold; the key is left as it was.

View Source
var ErrRequiredConfigRoot = errors.New("s2: required config.root")

ErrRequiredConfigRoot is returned by NewStorage implementations when Config.Root is empty. Root identifies the bucket, container, or directory that the storage operates on and is always required.

View Source
var ErrUnknownETag = errors.New("s2: object stored but its etag is unknown")

ErrUnknownETag is returned by Upload when the object was stored but its ETag could not be learned, because the read-back that would have supplied it failed. The write happened; only UploadResult.ETag is missing. The read-back's own error is wrapped alongside, so errors.Is still finds ErrNotExist under it.

View Source
var ErrUnknownType = errors.New("s2: unknown storage type")

ErrUnknownType is returned by NewStorage when no plugin is registered for the requested Type. Detect with errors.Is:

if errors.Is(err, s2.ErrUnknownType) {
    // unknown backend
}

Functions

func Key added in v0.15.0

func Key(prefix, name string) string

Key joins a storage prefix and an object name.

func Move added in v0.2.0

func Move(ctx context.Context, s Storage, src, dst string) error

Move moves src to dst on s. If s implements Mover, its Move method is used (which may be atomic and is generally more efficient). Otherwise Move falls back to Copy followed by Delete; that fallback is NOT atomic: if Delete fails after a successful Copy, both objects exist.

Example

ExampleMove uses the free function s2.Move, which transparently picks the fastest path: backends that implement the optional s2.Mover interface (e.g. osfs via filesystem rename) get an atomic rename, others fall back to Copy + Delete.

package main

import (
	"context"
	"errors"
	"fmt"
	"io"

	"github.com/mojatter/s2"
	_ "github.com/mojatter/s2/fs"
)

func main() {
	ctx := context.Background()
	strg, _ := s2.NewStorage(ctx, s2.Config{Type: s2.TypeMemFS})
	_ = strg.Put(ctx, s2.NewObjectBytes("src.txt", []byte("hi")))

	if err := s2.Move(ctx, strg, "src.txt", "dst.txt"); err != nil {
		panic(err)
	}

	_, err := strg.Get(ctx, "src.txt")
	fmt.Println("src missing:", errors.Is(err, s2.ErrNotExist))

	got, _ := strg.Get(ctx, "dst.txt")
	rc, _ := got.Open()
	defer rc.Close()
	body, _ := io.ReadAll(rc)
	fmt.Println("dst body:", string(body))
}
Output:
src missing: true
dst body: hi

func MustInt64

func MustInt64(v uint64) int64

MustInt64 converts a uint64 to int64, panicking if the value exceeds math.MaxInt64.

func MustUint64

func MustUint64(v int64) uint64

MustUint64 converts an int64 to uint64, panicking if the value is negative.

func ParseRoot added in v0.5.0

func ParseRoot(root string) (name, prefix string)

ParseRoot splits a Root string like "bucket/some/prefix" into the top-level name (bucket, container, or directory) and an optional key prefix. Leading and trailing slashes are trimmed.

The prefix is cleaned, so the two ways a backend spells a full key -- joining it with the name, or concatenating to keep the name verbatim -- resolve to the same string, and one that resolves to the root is empty rather than ".".

func PrefetchMetadata added in v0.30.0

func PrefetchMetadata(objs []Object)

PrefetchMetadata reads the Metadata of objs, a List page whose ContentType, ETag or Metadata the caller will read, on up to 8 goroutines at once. Reads fail as the accessors do; the accessors' own errors are not reported.

func RegisterNewStorageFunc

func RegisterNewStorageFunc(t Type, fn NewStorageFunc)

RegisterNewStorageFunc registers a new storage function.

func RelName added in v0.15.0

func RelName(prefix, name string) string

RelName is the inverse of Key: it strips the storage prefix, leaving a name that does not carry it untouched.

func UnregisterNewStorageFunc

func UnregisterNewStorageFunc(t Type)

UnregisterNewStorageFunc unregisters a storage function. Primarily useful in tests that swap backends.

func ValidateName added in v0.18.1

func ValidateName(name string) error

ValidateName wraps ErrInvalidName unless name is already the name the storage will use: exactly fs.ValidPath. A name that only resolves after cleaning -- "../x", "/x", "./x", "a//b", "x/" -- is rejected rather than folded, so the name a caller authorizes is the name it reads.

func ValidatePrefix added in v0.18.1

func ValidatePrefix(prefix string) error

ValidatePrefix is ValidateName for a value that selects objects rather than naming one: "" selects everything, and one trailing "/" confines the selection to a directory. Listing cursors take it too, since they are compared against names rather than resolved to one.

Types

type AzblobConfig added in v0.5.0

type AzblobConfig struct {
	// AccountName is the Azure storage account name.
	AccountName string `json:"account_name,omitempty"`
	// AccountKey is the shared key for the storage account.
	AccountKey string `json:"account_key,omitempty"`
	// ConnectionString is a full Azure Storage connection string.
	ConnectionString string `json:"connection_string,omitempty"`
}

AzblobConfig holds Azure Blob Storage-specific configuration. Authentication priority: ConnectionString > AccountName+AccountKey > DefaultAzureCredential (environment, managed identity, Azure CLI, etc.).

type Config

type Config struct {
	// Type is the type of storage.
	Type Type `json:"type"`
	// Root is the root path of the storage.
	// If Type is TypeOSFS, Root is the root path of the file system.
	// If Type is TypeMemFS, Root is not used.
	// If Type is TypeS3, Root is the S3 bucket name. The string following / in the bucket name is treated as a prefix.
	// If Type is TypeGCS, Root is the GCS bucket name. The string following / is treated as a prefix.
	Root string `json:"root"`
	// SignedURL is the presign URL of the storage. This is used for TypeOSFS and TypeMemFS.
	SignedURL string `json:"signed_url,omitempty"`
	// OSFS holds OSFS-specific settings. Only used when Type is TypeOSFS.
	OSFS *OSFSConfig `json:"osfs,omitempty"`
	// S3 holds S3-specific settings. Only used when Type is TypeS3.
	S3 *S3Config `json:"s3,omitempty"`
	// GCS holds GCS-specific settings. Only used when Type is TypeGCS.
	GCS *GCSConfig `json:"gcs,omitempty"`
	// Azblob holds Azure Blob Storage-specific settings. Only used when Type is TypeAzblob.
	Azblob *AzblobConfig `json:"azblob,omitempty"`
}

Config is a configuration for a storage.

type Configs added in v0.5.1

type Configs map[string]Config

Configs is a map of logical names to Config objects.

func LoadConfigs added in v0.5.1

func LoadConfigs(r io.Reader) (Configs, error)

LoadConfigs parses JSON formatted data from the specified io.Reader into Configs.

func LoadConfigsFile added in v0.5.1

func LoadConfigsFile(filename string) (Configs, error)

LoadConfigsFile reads the file at the given path and parses it as a JSON format into Configs.

func (Configs) Storages added in v0.5.1

func (c Configs) Storages(ctx context.Context) (Storages, error)

Storages iterates over the parsed configs, invokes NewStorage for each, and returns a new Storages map representing the initialized storage environments.

type CountOptions added in v0.23.0

type CountOptions struct {
	// Prefix selects the names beginning with it, at any depth, as a recursive List does.
	Prefix string
	// UpTo stops the count once more than UpTo objects are seen; zero counts all.
	UpTo int64
}

CountOptions controls a Count call.

type CountResult added in v0.23.0

type CountResult struct {
	// Objects is the exact count, or UpTo when Truncated.
	Objects int64
	// Truncated reports that more than UpTo objects exist.
	Truncated bool
}

CountResult reports how many objects a Count call saw.

func Count added in v0.23.0

func Count(ctx context.Context, s Storage, opts CountOptions) (CountResult, error)

Count counts the objects on s matching opts, via Counter when s has it, else by paging a recursive List; a failure reports no partial count.

type Counter added in v0.23.0

type Counter interface {
	Count(ctx context.Context, opts CountOptions) (CountResult, error)
}

Counter is an optional interface a Storage may satisfy to count faster than paging List; it must report what a recursive List of an unchanging, readable tree reports and may ignore UpTo, which Count caps.

type GCSConfig added in v0.5.0

type GCSConfig struct {
	// CredentialsFile is a path to a service account JSON key file.
	CredentialsFile string `json:"credentials_file,omitempty"`
}

GCSConfig holds GCS-specific configuration. When fields are empty, the GCS SDK defaults (environment variables, Application Default Credentials, etc.) are used.

type ListOptions added in v0.2.0

type ListOptions struct {
	// Prefix restricts the listing to objects whose names begin with Prefix
	// when Recursive is true. A non-recursive listing treats it as a
	// directory, so every backend reads it as if it ended in "/"; one naming
	// an object, and no directory, returns an empty page.
	Prefix string
	// After is an opaque continuation token returned by a previous call as
	// ListResult.NextAfter; pass it to fetch the next page. Empty for the
	// first page. Its encoding is backend-specific: never construct one or
	// carry one across storages. After wins over StartAfter.
	After string
	// StartAfter is a key name picked by the caller, existing or not; the
	// listing resumes at the first entry, object or common prefix, sorting
	// after it, as S3's start-after does. Ignored when After is set.
	StartAfter string
	// Limit caps the number of returned entries, CommonPrefixes included, as
	// S3's max-keys does. Zero means the backend's default page size; follow
	// NextAfter for the rest.
	Limit int
	// Recursive, when true, walks subdirectories and returns no
	// CommonPrefixes; when false, the listing stops at the first "/" past
	// Prefix and "directory-like" entries are surfaced via CommonPrefixes.
	Recursive bool
}

ListOptions controls a Storage.List call.

All fields are optional. The zero value lists the first page of the storage's flat namespace.

type ListResult added in v0.2.0

type ListResult struct {
	// Objects are the objects matching the request, in lexicographic order.
	// Their metadata may be unset depending on the backend, and Metadata()
	// may report nil; use Storage.Get for full metadata and a writable map.
	Objects []Object
	// CommonPrefixes are the directory-like grouping prefixes (only populated
	// when ListOptions.Recursive is false). Each ends in "/", as S3 returns it,
	// and is listed once across the pages of a listing.
	CommonPrefixes []string
	// NextAfter is an opaque continuation token, set only when an entry
	// followed the page as it was read. When empty, the listing is exhausted.
	// On s3, gcs and azblob, hidden directory markers ending a listing can leave the page after it empty.
	NextAfter string
}

ListResult is the response from Storage.List.

type Metadata

type Metadata map[string]string

Metadata holds object metadata as case-sensitive key/value pairs.

Metadata mirrors the http.Header / url.Values pattern: it is a named map type with helper methods, so callers can use the standard map operations (range, len, indexing, literal initialization) directly when convenient and the methods when ergonomics matter.

The zero value is nil and is read-safe; writes to a nil Metadata panic. Use make(s2.Metadata) or a literal s2.Metadata{...} to obtain a writable instance.

func (Metadata) Clone added in v0.2.0

func (m Metadata) Clone() Metadata

Clone returns a deep copy of the metadata. The result is independent of the receiver: mutating one does not affect the other. Cloning a nil Metadata returns nil.

func (Metadata) Delete added in v0.2.0

func (m Metadata) Delete(key string)

Delete removes the entry for key. Calling Delete on a missing key is a no-op.

func (Metadata) Get

func (m Metadata) Get(key string) (string, bool)

Get returns the value associated with key. The boolean reports whether the key is present, distinguishing a missing entry from an empty value.

func (Metadata) Set added in v0.2.0

func (m Metadata) Set(key, value string)

Set assigns value to key, overwriting any existing entry.

type Mover added in v0.2.0

type Mover interface {
	Move(ctx context.Context, src, dst string) error
}

Mover is an optional interface that a Storage implementation may satisfy to provide a move operation that is more efficient — and possibly atomic — than the default Copy + Delete fallback. The osfs backend implements Mover via a filesystem rename, for example.

Storage implementations are not required to satisfy Mover; the free function Move falls back to Copy + Delete when they do not.

Move(k, k) must keep k, and Move of a missing key returns ErrNotExist; s2test.TestStorageCopyMove checks both.

type NewStorageFunc

type NewStorageFunc func(ctx context.Context, cfg Config) (Storage, error)

NewStorageFunc is a function that creates a new storage.

type OSFSConfig added in v0.31.0

type OSFSConfig struct {
	// ConfinedRoot keeps the storage inside Root: a symlink leading outside it, or an absolute one, leads nowhere.
	ConfinedRoot bool `json:"confined_root,omitempty"`
}

OSFSConfig holds OSFS-specific settings.

type Object

type Object interface {
	// Name returns the name of the object.
	Name() string
	// Open opens the object for reading and returns the reader stream.
	// The caller is responsible for closing the returned io.ReadCloser.
	// From a Storage, it fails with ErrModified once the key holds another version than Get or List saw, and with ErrNotExist once the key is gone.
	Open() (io.ReadCloser, error)
	// OpenRange opens the object for reading the specified range and returns the reader stream.
	// The caller is responsible for closing the returned io.ReadCloser.
	// It fails as Open does.
	OpenRange(offset, length uint64) (io.ReadCloser, error)
	// Length returns the length of the object in bytes.
	Length() uint64
	// LastModified returns the last modified time of the object.
	LastModified() time.Time
	// Metadata returns the object's user metadata. Writing to the returned
	// map changes the in-memory Object, never the stored one.
	//
	// Storage.Get returns a writable map even when the object has none. A
	// List result reports whatever the backend holds, which may be nil: reads
	// are safe on a nil map, writes to one panic. On some backends (e.g. S3)
	// a listed object carries no metadata at all; use Storage.Get for it.
	Metadata() Metadata
	// ContentType returns the object's MIME type, or "" when unknown.
	// Objects returned by List may report "" (e.g., S3); use Storage.Get.
	ContentType() string
	// ETag returns the object's entity tag in quoted form, or "" when unknown.
	// Objects returned by List carry it without extra network round trips.
	ETag() string
}

Object is an interface that represents an object in a storage.

func NewObjectBytes

func NewObjectBytes(name string, body []byte, opts ...ObjectOption) Object

NewObjectBytes creates new object from byte slice.

func NewObjectFromFile added in v0.2.0

func NewObjectFromFile(ctx context.Context, name string, opts ...ObjectOption) (Object, error)

NewObjectFromFile creates a new Object backed by a file on the local filesystem. The file at name must exist and not be a directory; otherwise the returned error wraps ErrNotExist. The Object's Length and LastModified are populated from os.Stat; metadata can be supplied via WithMetadata.

Reads via Open are performed lazily by re-opening the underlying file. The supplied ctx is currently unused but reserved for future cancellation.

func NewObjectReader

func NewObjectReader(name string, body io.ReadCloser, length uint64, opts ...ObjectOption) Object

NewObjectReader creates new object from io.ReadCloser.

type ObjectOption

type ObjectOption func(*object)

ObjectOption is a functional option for configuring objects created by NewObject, NewObjectReader, and NewObjectBytes.

func WithContentType added in v0.18.0

func WithContentType(contentType string) ObjectOption

WithContentType sets the content type on the object.

func WithLastModified

func WithLastModified(t time.Time) ObjectOption

WithLastModified sets the last modified time on the object.

func WithMetadata

func WithMetadata(md Metadata) ObjectOption

WithMetadata sets the metadata on the object, keeping md by reference.

type S3Config

type S3Config struct {
	// EndpointURL is a custom S3-compatible endpoint (e.g. "http://localhost:9000").
	EndpointURL string `json:"endpoint_url,omitempty"`
	// Region is the AWS region (e.g. "ap-northeast-1").
	Region string `json:"region,omitempty"`
	// AccessKeyID is the AWS access key ID.
	AccessKeyID string `json:"access_key_id,omitempty"`
	// SecretAccessKey is the AWS secret access key.
	SecretAccessKey string `json:"secret_access_key,omitempty"`
}

S3Config holds S3-specific configuration. When fields are empty, the AWS SDK defaults (environment variables, shared credentials, IAM role, etc.) are used.

type SignedURLMethod added in v0.2.0

type SignedURLMethod string

SignedURLMethod is the HTTP method that a presigned URL is authorized for.

const (
	// SignedURLGet authorizes a GET request (download).
	SignedURLGet SignedURLMethod = "GET"
	// SignedURLPut authorizes a PUT request (upload).
	SignedURLPut SignedURLMethod = "PUT"
)

type SignedURLOptions added in v0.2.0

type SignedURLOptions struct {
	// Name is the object name to sign.
	Name string
	// Method is the HTTP method to authorize. Defaults to GET when empty.
	Method SignedURLMethod
	// TTL is how long the URL remains valid.
	TTL time.Duration
}

SignedURLOptions controls a Storage.SignedURL call.

type Storage

type Storage interface {
	// Type returns the type of the storage.
	Type() Type
	// Sub returns a view of the given prefix that shares the parent's lifetime:
	// an object written through either is the same object, with the same ETag,
	// Content-Type and metadata, through the other.
	Sub(ctx context.Context, prefix string) (Storage, error)
	// List returns the objects (and, when non-recursive, common prefixes)
	// matching opts.
	List(ctx context.Context, opts ListOptions) (ListResult, error)
	// Get returns the object identified by name, including its metadata.
	// The returned Object's Metadata map is non-nil and writable even when
	// the object carries none; a List result may report nil instead.
	// If no object exists at name, the returned error wraps ErrNotExist.
	Get(ctx context.Context, name string) (Object, error)
	// Exists reports whether anything is present at name. Backends that
	// expose a directory hierarchy (osfs, memfs) treat both regular
	// files and directories as "present"; flat key-value backends (s3)
	// only resolve to leaf objects since they have no directory
	// primitive of their own.
	Exists(ctx context.Context, name string) (bool, error)
	// Put writes obj to the storage atomically per object. Any metadata on
	// obj is persisted as part of the same call.
	Put(ctx context.Context, obj Object) error
	// PutMetadata replaces the metadata of an existing object without
	// rewriting its body. It is intended for hash- or ETag-style metadata
	// that can only be computed after the body is written. Note: PutMetadata
	// is NOT atomic with Put; a crash between the two leaves the object on
	// disk with whatever metadata Put itself wrote. Replaces (does not merge)
	// any existing metadata. A backend may fail the call when the object
	// changes concurrently; that error is the provider's own.
	PutMetadata(ctx context.Context, name string, metadata Metadata) error
	// Copy duplicates src to dst. The semantics are backend-defined: the s3
	// backend uses server-side copy, while file-backed backends stream the
	// body.
	Copy(ctx context.Context, src, dst string) error
	// Delete removes the object at name. Deleting a non-existent object is
	// a no-op and does not return an error.
	Delete(ctx context.Context, name string) error
	// DeleteRecursive removes every object whose name begins with prefix.
	// A prefix ending in "/" covers only that directory, not names that merely share it.
	// The operation is best-effort and not atomic across objects.
	DeleteRecursive(ctx context.Context, prefix string) error
	// SignedURL returns a presigned URL for the object identified by opts.
	// Backends that do not support presigning return an error.
	SignedURL(ctx context.Context, opts SignedURLOptions) (string, error)
}

Storage is a simple object storage abstraction. Implementations are expected to be safe for concurrent use by multiple goroutines.

Errors that report a missing object wrap ErrNotExist; detect them with errors.Is(err, s2.ErrNotExist).

Every method takes names exactly as stored -- see ValidateName and ValidatePrefix -- and wraps ErrInvalidName for anything else.

func NewStorage

func NewStorage(ctx context.Context, cfg Config) (Storage, error)

NewStorage creates a new storage from the given configuration. If no plugin is registered for cfg.Type, the returned error wraps ErrUnknownType.

Example

ExampleNewStorage shows how to construct an in-memory Storage. The blank import of github.com/mojatter/s2/fs registers both osfs and memfs.

package main

import (
	"context"
	"fmt"

	"github.com/mojatter/s2"
	_ "github.com/mojatter/s2/fs"
)

func main() {
	ctx := context.Background()

	strg, err := s2.NewStorage(ctx, s2.Config{Type: s2.TypeMemFS})
	if err != nil {
		panic(err)
	}
	fmt.Println(strg.Type())
}
Output:
memfs

type Storages added in v0.5.1

type Storages map[string]Storage

Storages is a map of logical names to Storage instances.

func Load added in v0.5.1

func Load(ctx context.Context, filename string) (Storages, error)

Load helps to simplify loading the Configs from a specific JSON file and fully initializing all nested Storage instances into the resulting Storages structure.

type Type

type Type string
const (
	TypeOSFS   Type = "osfs"
	TypeMemFS  Type = "memfs"
	TypeS3     Type = "s3"
	TypeGCS    Type = "gcs"
	TypeAzblob Type = "azblob"
)

func KnownTypes added in v0.2.0

func KnownTypes() []Type

KnownTypes returns the list of storage Types that are known to s2. The returned slice is a fresh copy; mutating it does not affect future calls. Note that this only enumerates compiled-in types; whether a given type is *registered* depends on whether the corresponding backend package has been imported (e.g. _ "github.com/mojatter/s2/fs").

type UploadOptions added in v0.19.0

type UploadOptions struct {
	// IfMatch, when set, writes only while the key holds an object with this ETag, in the quoted form Object.ETag reports; "*" is unsupported.
	IfMatch string
	// IfNoneMatch, when "*", writes only while the key holds no object; any other value is unsupported.
	IfNoneMatch string
}

UploadOptions controls an Upload call. At most one condition is set, as S3 takes them; see Validate.

func (UploadOptions) Check added in v0.27.0

func (o UploadOptions) Check(name, etag string, exists bool) error

Check judges opts against the object at name, whose ETag is etag when exists. It wraps ErrNotExist when IfMatch finds no object, ErrPreconditionFailed when a condition fails, and errors.ErrUnsupported for what Validate refuses.

func (UploadOptions) Conditional added in v0.27.0

func (o UploadOptions) Conditional() bool

Conditional reports whether opts carries a condition.

func (UploadOptions) Validate added in v0.27.0

func (o UploadOptions) Validate() error

Validate refuses with errors.ErrUnsupported, without reading the key, what S3 answers 501 to: IfMatch "*", both conditions, an IfNoneMatch other than "*".

type UploadResult added in v0.19.0

type UploadResult struct {
	// ETag is the stored object's entity tag in quoted form, the same value a
	// following Get reports — including "" for a backend that has none, since
	// [Object.ETag] is itself "" when unknown.
	ETag string
}

UploadResult reports what the backend stored. A field the backend does not know is left at its zero value.

func Upload added in v0.19.0

func Upload(ctx context.Context, s Storage, obj Object, opts UploadOptions) (UploadResult, error)

Upload stores obj on s and reports what was stored. If s implements Uploader, its Upload method is used and no read-back happens. Otherwise Upload falls back to Put followed by Get, which costs one more round trip and answers the same ETag. A capability that reports no ETag is read back the same way, so the result carries exactly what a following Get answers — which is "" for a backend that reports no ETag at all.

A read-back failure is returned as the call's error even though the object was stored; it wraps ErrUnknownETag over the error Get gave. A caller mapping errors to a status must not turn that into "no such object" — the write succeeded — and can tell the case apart with errors.Is.

The fallback is not atomic: a concurrent write to the same name can land between the Put and the read-back, and the ETag then describes that write rather than obj. Only a backend implementing Uploader is free of this. For the same reason the fallback fails a conditional call with errors.ErrUnsupported.

type Uploader added in v0.19.0

type Uploader interface {
	Upload(ctx context.Context, obj Object, opts UploadOptions) (UploadResult, error)
}

Uploader is an optional interface that a Storage implementation may satisfy to report what it stored. Put answers nothing, so a caller that needs the stored ETag otherwise has to read the object back; a backend already has the value in its own write response.

Storage implementations are not required to satisfy Uploader; the free function Upload falls back to Put followed by Get when they do not.

Upload must hold opts' conditions until the write lands, as UploadOptions.Check judges them, or fail a conditional call with an error wrapping errors.ErrUnsupported.

Directories

Path Synopsis
azblob module
cmd
s2-server module
gcs module
s2env module
Package s2test is a conformance suite for s2.Storage implementations.
Package s2test is a conformance suite for s2.Storage implementations.
s3 module
server module

Jump to

Keyboard shortcuts

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