Skip to main content

Core Operations

Every operation below works identically regardless of which backend store is — the differences are all in construction, covered per-backend under Backends.

Upload​

// From io.Reader
file, _ := os.Open("photo.jpg")
info, err := store.Put(ctx, "images/photo.jpg", file)

// With options
info, err := store.Put(ctx, "images/photo.jpg", file,
objstore.WithContentType("image/jpeg"),
objstore.WithMetadata(map[string]string{"author": "john"}),
objstore.WithCacheControl("max-age=31536000"),
objstore.WithACL("public-read"),
)

// Prevent overwrite
info, err := store.Put(ctx, "images/photo.jpg", file,
objstore.WithOverwrite(false),
)
if errors.Is(err, objstore.ErrAlreadyExists) {
// File already exists
}

// Helper functions
objstore.PutBytes(ctx, store, "data.bin", []byte{1, 2, 3})
objstore.PutString(ctx, store, "hello.txt", "Hello, World!")
objstore.PutDataURI(ctx, store, "image.png", "data:image/png;base64,...")

Conditional Writes​

WithOverwrite(false) is a create-only write, and every backend checks it atomically: of several concurrent writers, exactly one succeeds.

WithIfMatch makes a write depend on the object being unchanged. Pass the ETag from an earlier Put, Stat, or List. If anyone wrote or deleted the object since then, the write is rejected with ErrPreconditionFailed. That gives you optimistic concurrency for read-modify-write updates:

for {
info, err := store.Stat(ctx, "config.json")
if err != nil {
return err
}
current, _ := objstore.GetBytes(ctx, store, "config.json")
updated := modify(current)

_, err = store.Put(ctx, "config.json", bytes.NewReader(updated),
objstore.WithIfMatch(info.ETag),
)
if errors.Is(err, objstore.ErrPreconditionFailed) {
continue // someone else won the race; re-read and retry
}
return err
}

If the object doesn't exist, the error also matches ErrNotFound. WithOverwrite is ignored when WithIfMatch is set.

Treat ETag as an opaque string. Only pass it back to the same backend it came from, because each backend produces its own format.

BackendHow it is enforced
S3Native If-Match / If-None-Match, including multipart uploads
GCSGeneration preconditions; the ETag is resolved to the generation that has it
AzureNative If-Match / If-None-Match access conditions
MemoryUnder the storage lock
LocalO_EXCL for create-only, which also holds across processes. WithIfMatch is atomic only against other Puts on the same LocalStorage, not against other processes or Copy/Move/Delete

Some S3-compatible services don't support conditional writes yet. Check your provider before relying on them.

Download​

// As io.ReadCloser
reader, err := store.Get(ctx, "docs/file.pdf")
if errors.Is(err, objstore.ErrNotFound) {
// File doesn't exist
}
defer reader.Close()
io.Copy(dst, reader)

// Helper functions
data, _ := objstore.GetBytes(ctx, store, "data.bin")
text, _ := objstore.GetString(ctx, store, "hello.txt")

// Partial (ranged) download — e.g. resuming a download or seeking into a video
reader, err := store.Get(ctx, "videos/movie.mp4", objstore.WithRange(1024, 4096)) // bytes [1024, 5120)
reader, err := store.Get(ctx, "videos/movie.mp4", objstore.WithRange(1024, 0)) // from byte 1024 to EOF
if errors.Is(err, objstore.ErrInvalidRange) {
// offset is negative or at/beyond the end of the file
}

Delete​

err := store.Delete(ctx, "images/photo.jpg")

// Delete with prefix (all files in directory)
objstore.DeletePrefix(ctx, store, "images/user-123/")

// S3: Batch delete multiple files
s3Store.DeleteMultiple(ctx, []string{"file1.txt", "file2.txt"})

Check Existence​

exists, err := store.Exists(ctx, "images/photo.jpg")

File Information​

info, err := store.Stat(ctx, "images/photo.jpg")

fmt.Println(info.Path) // "images/photo.jpg"
fmt.Println(info.Name) // "photo.jpg"
fmt.Println(info.Size) // 12345
fmt.Println(info.ContentType) // "image/jpeg"
fmt.Println(info.LastModified) // 2026-03-16 10:30:00
fmt.Println(info.ETag) // "abc123"
fmt.Println(info.Metadata) // map[author:john]

List Files​

// List files in directory
result, err := store.List(ctx, "images/")

for _, file := range result.Files {
fmt.Println(file.Path, file.Size)
}

// List subdirectories
for _, prefix := range result.Prefixes {
fmt.Println("Directory:", prefix)
}

// With options
result, err := store.List(ctx, "images/",
objstore.WithMaxKeys(100),
objstore.WithDelimiter("/"),
objstore.WithRecursive(true),
)

// Pagination
nextToken := ""
for {
result, err := store.List(ctx, "images/",
objstore.WithMaxKeys(100),
objstore.WithToken(nextToken),
)
if err != nil {
log.Fatal(err)
}

// Process files...

if !result.IsTruncated {
break
}
nextToken = result.NextToken
}

Copy and Move​

// Copy
err := store.Copy(ctx, "images/original.jpg", "images/backup.jpg")

// Move (rename)
err := store.Move(ctx, "temp/upload.jpg", "images/photo.jpg")

// Copy between storages
objstore.CopyTo(ctx, srcStore, "file.txt", dstStore, "file.txt")

// Move between storages
objstore.MoveTo(ctx, srcStore, "file.txt", dstStore, "file.txt")

URLs​

// Public URL
url, err := store.URL(ctx, "images/photo.jpg")
// "https://cdn.example.com/images/photo.jpg"

// Signed URL (temporary access)
url, err := store.SignedURL(ctx, "images/photo.jpg",
objstore.WithExpires(15 * time.Minute),
)

// Signed URL for upload
url, err := store.SignedURL(ctx, "uploads/new-file.jpg",
objstore.WithMethod("PUT"),
objstore.WithExpires(5 * time.Minute),
objstore.WithSignedContentType("image/jpeg"),
)

See Signed URLs for how signing differs between cloud backends and the local/memory backends.

ACL Values​

Common ACL values for S3 and GCS:

ACLDescription
privateOwner-only access (default)
public-readPublic read access
public-read-writePublic read/write access
authenticated-readAuthenticated users can read
bucket-owner-full-controlBucket owner has full control