S3 API
S3-compatible object storage helpers are available under ptool.s3 and p.s3.
ptool.s3.connect
v0.10.0- Introduced.
ptool.s3.connect(options) opens an S3-compatible object storage connection
and returns a Connection object.
options fields:
bucket(string, required): The bucket name.region(string, optional): The AWS region or provider region.endpoint(string, optional): A custom S3-compatible endpoint URL such as MinIO, R2, or another object storage service.access_key_id(string, optional): The access key ID.secret_access_key(string, optional): The secret access key.session_token(string, optional): The session token.root(string, optional): A root prefix applied to all object operations.allow_anonymous(boolean, optional): Whentrue, allow unsigned requests if credentials are not configured. Defaults tofalse.
Environment fallback:
- Explicit
optionsvalues win. - Missing
region,endpoint,access_key_id,secret_access_key, andsession_tokenvalues fall back to:AWS_REGIONAWS_ENDPOINT,AWS_ENDPOINT_URL, orAWS_S3_ENDPOINTAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN
- Environment fallback uses
ptool's runtime environment view, so values set throughp.os.setenv(...)are also visible toptool.s3.connect(...).
Example:
local s3 = ptool.s3.connect({
bucket = "artifacts",
region = "auto",
endpoint = "https://<account>.r2.cloudflarestorage.com",
access_key_id = p.os.getenv("AWS_ACCESS_KEY_ID"),
secret_access_key = p.os.getenv("AWS_SECRET_ACCESS_KEY"),
root = "builds/",
})
Connection
v0.10.0- Introduced.
Connection represents an open object storage connection returned by
ptool.s3.connect().
It is implemented as a Lua userdata.
Methods:
conn:read(path[, options])->stringconn:write(path, content[, options])->tableconn:delete(path)->nilconn:exists(path)->booleanconn:list([prefix])->tableconn:stat(path)->tableconn:put_bucket_acl(options)->nilconn:put_object_acl(path, options)->nil
Path rules:
- Object paths must be non-empty strings unless otherwise noted.
- Leading
/is ignored, so/foo/bar.txtandfoo/bar.txttarget the same object. - Paths are relative to
rootwhenrootis configured on the connection.
Entry table shape:
path(string): The object path relative to the connection root.size(integer): The object size in bytes.etag(string | nil): The object ETag when available.last_modified(string | nil): The last-modified timestamp when available.content_type(string | nil): The object content type when available.version(string | nil): The object version when available.metadata(table | nil): User-defined object metadata when available.is_file(boolean): Whether the entry is a file.is_dir(boolean): Whether the entry is a directory.mode(string): One of"file","dir", or"unknown".
read
v0.10.0- Introduced. Unreleased - Changed.
Canonical API name: ptool.s3.Connection:read.
conn:read(path[, options]) reads an object as raw bytes and returns a Lua string.
path(string, required): The object path.options(table, optional): Read options.- Returns:
string.
options fields:
range(table, optional): Reads a byte range using half-open bounds[start, end).start(integer, optional): The first byte offset to include.end(integer, optional): The first byte offset to exclude.
Behavior:
- Omitting
rangereads the full object. { start = N }reads from byteNto the end.{ end = N }reads from the beginning up to, but not including, byteN.{ start = A, end = B }reads bytesAthroughB - 1.
Example:
local s3 = ptool.s3.connect({ bucket = "artifacts" })
local content = s3:read("releases/v1.0.0/notes.txt")
print(content)
local prefix = s3:read("releases/v1.0.0/notes.txt", {
range = { start = 0, end = 5 },
})
print(prefix)
write
v0.10.0- Introduced. Unreleased - Changed.
Canonical API name: ptool.s3.Connection:write.
conn:write(path, content[, options]) writes a Lua string to an object as raw
bytes and returns an entry table.
path(string, required): The object path.content(string, required): The bytes to upload.options(table, optional): Write options.- Returns:
table.
options fields:
content_type(string, optional): Sets the object content type.cache_control(string, optional): Sets the object cache-control header.content_disposition(string, optional): Sets the object content-disposition header.content_encoding(string, optional): Sets the object content-encoding header.metadata(table, optional): Sets user-defined metadata as string key/value pairs.if_not_exists(boolean, optional): Write only when the object does not already exist. Defaults tofalse.if_match(string, optional): Write only when the current ETag matches.if_none_match(string, optional): Write only when the current ETag does not match.
Behavior:
contentis uploaded byte-for-byte.- Embedded NUL bytes and non-UTF-8 bytes are preserved.
- The returned entry avoids an immediate follow-up
stat()call for common metadata. - Some S3-compatible services may not echo user-defined
metadataoretagin the write response. When that happens, those fields remainniluntil a laterstat().
Example:
local s3 = ptool.s3.connect({ bucket = "artifacts" })
local entry = s3:write("tmp/hello.txt", "hello\n", {
content_type = "text/plain; charset=utf-8",
metadata = { author = "ptool" },
})
print(entry.path, entry.etag, entry.version)
s3:write("tmp/blob.bin", "\x00\xffABC")
delete
v0.10.0- Introduced.
Canonical API name: ptool.s3.Connection:delete.
conn:delete(path) deletes an object.
path(string, required): The object path.
exists
v0.10.0- Introduced.
Canonical API name: ptool.s3.Connection:exists.
conn:exists(path) checks whether an object exists.
path(string, required): The object path.- Returns:
boolean.
list
v0.10.0- Introduced.
Canonical API name: ptool.s3.Connection:list.
conn:list([prefix]) lists entries under a prefix and returns a dense Lua
array table.
prefix(string, optional): The prefix to list. Defaults to the connection root.- Returns:
table.
Example:
local s3 = ptool.s3.connect({ bucket = "artifacts", root = "builds/" })
local entries = s3:list("2026/")
for _, entry in ipairs(entries) do
print(entry.path, entry.mode, entry.size)
end
stat
v0.10.0- Introduced.
Canonical API name: ptool.s3.Connection:stat.
conn:stat(path) returns metadata for a single object.
path(string, required): The object path.- Returns:
table.
Example:
local s3 = ptool.s3.connect({ bucket = "artifacts" })
local meta = s3:stat("releases/v1.0.0/app.tar.zst")
print(meta.size, meta.etag, meta.last_modified)
put_bucket_acl
v0.12.0- Introduced.
Canonical API name: ptool.s3.Connection:put_bucket_acl.
conn:put_bucket_acl(options) replaces the bucket ACL and returns nil on
success.
options(table, required): Bucket ACL options.- Returns:
nil.
options fields:
acl(string, optional): A canned bucket ACL. Accepted values are"authenticated-read","private","public-read", and"public-read-write".expected_bucket_owner(string, optional): Fails the request if the bucket is not owned by this AWS account ID.grant_full_control(string, optional): Value for thex-amz-grant-full-controlheader.grant_read(string, optional): Value for thex-amz-grant-readheader.grant_read_acp(string, optional): Value for thex-amz-grant-read-acpheader.grant_write(string, optional): Value for thex-amz-grant-writeheader.grant_write_acp(string, optional): Value for thex-amz-grant-write-acpheader.
Behavior:
- Supply either
aclor one or moregrant_*fields. The two forms cannot be combined in one call. - Every supplied string must be non-empty.
- Grant strings use the S3 grant-header syntax, such as an account ID, email address, or group URI accepted by the target provider.
- The operation targets the connection bucket. The connection's
rootprefix is not applied. - Many S3-compatible providers disable ACLs or do not implement the ACL APIs.
In that case, the provider error is returned as an
s3_error.
Example:
local s3 = ptool.s3.connect({ bucket = "artifacts" })
s3:put_bucket_acl({ acl = "private" })
s3:put_bucket_acl({
grant_read = 'uri="http://acs.amazonaws.com/groups/global/AllUsers"',
})
put_object_acl
v0.12.0- Introduced.
Canonical API name: ptool.s3.Connection:put_object_acl.
conn:put_object_acl(path, options) replaces an object's ACL and returns nil
on success.
path(string, required): The object path.options(table, required): Object ACL options.- Returns:
nil.
options fields:
acl(string, optional): A canned object ACL. Accepted values are"authenticated-read","aws-exec-read","bucket-owner-full-control","bucket-owner-read","private","public-read", and"public-read-write".expected_bucket_owner(string, optional): Fails the request if the bucket is not owned by this AWS account ID.grant_full_control(string, optional): Value for thex-amz-grant-full-controlheader.grant_read(string, optional): Value for thex-amz-grant-readheader.grant_read_acp(string, optional): Value for thex-amz-grant-read-acpheader.grant_write(string, optional): Value for thex-amz-grant-writeheader.grant_write_acp(string, optional): Value for thex-amz-grant-write-acpheader.version_id(string, optional): Applies the ACL to this object version.request_payer(string, optional): Accepts only"requester"and sends the requester-pays acknowledgement.
Behavior:
- Supply either
aclor one or moregrant_*fields. The two forms cannot be combined in one call. - Every supplied string must be non-empty.
- Grant strings use the S3 grant-header syntax accepted by the target provider.
- Leading
/is ignored, and the connection'srootprefix is applied to the object key in the same way as other object operations. - Many S3-compatible providers disable ACLs or do not implement the ACL APIs.
In that case, the provider error is returned as an
s3_error.
Example:
local s3 = ptool.s3.connect({
bucket = "artifacts",
root = "public/",
})
s3:put_object_acl("index.html", { acl = "public-read" })
s3:put_object_acl("release.zip", {
acl = "bucket-owner-full-control",
version_id = "example-version-id",
request_payer = "requester",
})