s3
Store and fetch objects in Amazon S3 and S3-compatible services such as R2 and MinIO, and hand out time-limited signed links.
import "std:s3"S3 is object storage: a bucket holds objects, each found by a key. Amazon S3 started it, and Cloudflare R2, MinIO, Backblaze and others speak the same protocol. std:s3 uploads, downloads and deletes objects, and makes presigned URLs that let someone upload or download one object without your credentials.
A client is made without touching the network, so creating one and signing a link runs here. Calls that move data are norun.
Make a client#
s3.new(options) returns a client for one bucket. endpoint and bucket are required. Credentials, region and a public URL are optional:
import "std:s3"
auto bucket = s3.new({
endpoint: "https://s3.example.com",
bucket: "photos",
region: "eu-west-1",
accessKeyId: "AKIAEXAMPLE",
secretAccessKey: "secret"
})
log bucket.bucket()
log bucket.endpoint()
log bucket.region()Leave out endpoint and run it: s3.new throws a ValueError that names the missing option, because a client that can't go anywhere is a mistake in your code. Read keys from env.
Public URLs#
If the bucket is served from a CDN or a public domain, publicURL lets client.public(key) build the link people can open, escaping the key:
import "std:s3"
auto bucket = s3.new({
endpoint: "https://s3.example.com",
bucket: "photos",
publicURL: "https://cdn.example.com"
})
log bucket.public("holiday/beach day.png")Upload and download#
upload(key, data) stores a string or bytes, uploadFile(key, path) stores a file, and download(key) returns bytes. downloadFile(key, path) saves one to disk. A missing object is a NotFoundError:
import "std:s3"
import "std:env"
auto bucket = s3.new({
endpoint: "https://s3.example.com",
bucket: "photos",
accessKeyId: env.get("S3_KEY"),
secretAccessKey: env.get("S3_SECRET")
})
bucket.upload("notes/hello.txt", "hello from OSL", {contentType: "text/plain"}).unwrap()
byte[] data = bucket.download("notes/hello.txt").unwrap()
log data.len
switch bucket.download("notes/missing.txt") (
case ok(bytes)
log "found"
case err(e)
log "no such object:" e.kind
)contentType is stored with the object, so browsers treat it right. mime can pick one from a file name.
Delete#
delete(key) removes an object. Deleting a key that doesn't exist succeeds, as it does in S3, so you don't need to check first:
import "std:s3"
auto bucket = s3.new({
endpoint: "https://s3.example.com",
bucket: "photos",
accessKeyId: "...",
secretAccessKey: "..."
})
bucket.delete("notes/hello.txt").unwrap()
bucket.delete("notes/hello.txt").unwrap()Presigned URLs#
A presigned URL gives whoever holds it one kind of access to one object for a limited time, with no credentials shared. Hand a browser a URL to upload straight to the bucket, so a big file never passes through your server. expires is in milliseconds:
import "std:s3"
auto bucket = s3.new({
endpoint: "https://s3.example.com",
bucket: "photos",
accessKeyId: "AKIAEXAMPLE",
secretAccessKey: "secret"
})
string download = bucket.presignURL("a/b.png", {expires: 600000})
log download.startsWith("https://s3.example.com/photos/a/b.png?")
log download.contains("X-Amz-Signature=")
log download.contains("X-Amz-Expires=600")
string upload = bucket.presignURL("a/new.png", {method: "PUT", expires: 60000})
log upload.contains("X-Amz-Expires=60")The signature depends on the time, so the URL itself differs on every run. Try a different expires and see the number in the link change. The longest a link can live is seven days.
Errors#
Failures name their kind: PermissionError for bad credentials, NetworkError and TimeoutError for connection trouble, NotFoundError for a missing object. A service that is down shows up like this:
import "std:s3"
auto bucket = s3.new({
endpoint: "http://127.0.0.1:1",
bucket: "b",
accessKeyId: "x",
secretAccessKey: "y",
timeout: 1000
})
log bucket.download("anything").unwrapErr().kindWhy use a presigned upload URL instead of uploading through your server?
A link that lasts a minute
Make a client for bucket reports at https://s3.example.com with credentials, and print whether a presigned download link for q3.pdf that expires in 60 seconds mentions X-Amz-Expires=60.
true
API reference#
15 members, generated from the std:s3 source of OSL 9.3.0.
Functions
s3.new(object? options) *s3.Clientnew returns a client for one bucket. Options: endpoint (required, the service URL such as "https://s3.us-east-1.amazonaws.com" or "https://<account>.r2.cloudflarestorage.com"), bucket (required), region (default "us-east-1"; R2 uses "auto"), accessKeyId, secretAccessKey, sessionToken (for temporary credentials), publicURL (base URL that client.public builds on) and timeout (milliseconds per request, default 30000). Objects are addressed path-style, as endpoint/bucket/key. Throws ValueError when endpoint or bucket is missing or endpoint isn't an http or https URL.
*s3.Client
client.bucket() stringbucket returns the bucket name.
client.delete(string key) result<unit, error>delete removes the object key. Deleting a key that doesn't exist succeeds, as it does in S3. Fails with PermissionError, NetworkError, TimeoutError or RuntimeError like download.
client.download(string key) result<byte[], error>download returns the contents of the object key. Fails with NotFoundError when there is no such object, PermissionError when access is denied, NetworkError or TimeoutError when the service can't be reached, and RuntimeError for any other error response. Throws ValueError for an empty key.
client.downloadFile(string key, string path) result<unit, error>downloadFile saves the object key to path, replacing the file. Fails like download, and with an IOError or PermissionError when path can't be written; a partly written file is removed.
client.endpoint() stringendpoint returns the service URL the client was created with.
client.get(object input) objectDeprecated. Deprecated: use client.download(key) instead, which fails with NotFoundError for a missing object.
get fetches input.key and returns {ok, status, body, headers}, or {ok: false, error} when the request couldn't be made.
client.presign(object input) objectDeprecated. Deprecated: use client.presignURL(key, options?) instead, which takes expires in milliseconds.
presign returns {ok: true, url} for input.key, with input.expires in seconds, input.method and input.content_length, or {ok: false, error}.
client.presignURL(string key, object? options) stringpresignURL returns a URL that grants whoever holds it one kind of access to key without credentials. Options: method (default "GET"; "PUT" lets the holder upload), expires (milliseconds the URL stays valid, default 3600000, whole seconds from 1 second to 7 days) and contentLength (bytes; the upload must then be exactly that size). Throws ValueError for an empty key or an out-of-range option.
client.public(string key) stringpublic returns the public URL of key: publicURL/key when the client has a publicURL option, otherwise endpoint/bucket/key. The URL is only useful when the bucket allows public reads; use presignURL for private objects.
client.put(object input) objectDeprecated. Deprecated: use client.upload(key, data, options?) instead, which fails with a typed error.
put uploads input.body as input.key and returns {ok, status, body, headers}, or {ok: false, error} when the request couldn't be made.
client.region() stringregion returns the signing region, "us-east-1" unless set.
client.remove(object input) objectDeprecated. Deprecated: use client.delete(key) instead.
remove deletes input.key and returns {ok, status, body, headers}, or {ok: false, error} when the request couldn't be made.
client.upload(string key, any data, object? options) result<unit, error>upload stores data, a string or byte[], as the object key, replacing any object already there. Options: contentType (default "application/octet-stream"), contentEncoding (such as "gzip") and cacheControl. Fails with PermissionError when the credentials are refused, NetworkError or TimeoutError when the service can't be reached, and RuntimeError for any other error response. Throws ValueError for an empty key and TypeError for other data types.
client.uploadFile(string key, string path, object? options) result<unit, error>uploadFile stores the file at path as the object key. It takes the same options as upload, except that contentType defaults to the type its extension suggests. Fails like upload, and with NotFoundError or PermissionError when the local file can't be read.