Cloud Storage

Access S3-compatible object storage. Upload, download, list, and manage objects, presign download, upload and multipart-part URLs, and read objects with random access.

For storage configuration, see Cloud Storage.

Loading

local cloudstorage = require("cloudstorage")

Acquiring Storage

Acquire a cloud storage resource by registry ID:

local storage, err = cloudstorage.get("app.infra:files")
if err then
    return nil, err
end

local uploaded, upload_err = storage:upload_object("data/file.txt", "content")
storage:release()
if upload_err then return nil, upload_err end
return uploaded
Parameter Type Description
id string Storage resource ID

Returns: Storage, error

Uploading Objects

Upload content from a string or file:

local json = require("json")

local storage, storage_err = cloudstorage.get("app.infra:files")
if storage_err then return nil, storage_err end

-- Upload string content
local body, encode_err = json.encode({
    date = "2024-01-15",
    total = 1234
})
if encode_err then
    storage:release()
    return nil, encode_err
end
local ok, err = storage:upload_object("reports/daily.json", body)
if err then
    storage:release()
    return nil, err
end

-- Upload from file
local fs = require("fs")
local vol, fs_err = fs.get("app:data")
if fs_err then
    storage:release()
    return nil, fs_err
end
local file, open_err = vol:open("/large-file.bin", "r")
if open_err then
    storage:release()
    return nil, open_err
end

local uploaded, file_upload_err = storage:upload_object("backups/large-file.bin", file)
local _, close_err = file:close()

storage:release()
if file_upload_err then
    if close_err then report_cleanup_error(close_err) end
    return nil, file_upload_err
end
if close_err then return nil, close_err end
return uploaded
Parameter Type Description
key string Object key/path
content string or Reader Content as string or file reader
options table Optional metadata and conditional write options

Returns: boolean, error

Upload Options

Attach metadata or guard the write with an options table:

local uploaded, err = storage:upload_object("reports/daily.json", body, {
    content_type = "application/json",
    cache_control = "max-age=3600",
    metadata = { owner = "team-a", run_id = "1234" },  -- stored as x-amz-meta-*
    only_if_absent = true                              -- fail if the key already exists
})
if err then return nil, err end
return uploaded
Option Type Description
content_type string MIME type
cache_control string Cache-Control header
content_disposition string Content-Disposition header
content_encoding string Content-Encoding header
metadata table User metadata (string keys/values), stored as x-amz-meta-*
headers table Additional request headers (string keys/values)
if_match string Write only if the current object ETag matches
if_none_match string Write only if no object matches the ETag ("*" means any)
only_if_absent boolean Write only if the key does not exist (alias for if_none_match = "*")

A conditional write that fails its precondition returns a precondition_failed error.

Downloading Objects

Download an object to a file writer:

local fs = require("fs")
local storage, storage_err = cloudstorage.get("app.infra:files")
if storage_err then return nil, storage_err end
local vol, fs_err = fs.get("app:temp")
if fs_err then
    storage:release()
    return nil, fs_err
end

local file, open_err = vol:open("/downloaded.json", "w")
if open_err then
    storage:release()
    return nil, open_err
end
local ok, err = storage:download_object("reports/daily.json", file)
local _, close_err = file:close()
if err then
    if close_err then report_cleanup_error(close_err) end
    storage:release()
    return nil, err
end
if close_err then
    storage:release()
    return nil, close_err
end

-- Download partial content (first 1KB)
local partial, partial_open_err = vol:open("/partial.bin", "w")
if partial_open_err then
    storage:release()
    return nil, partial_open_err
end
local partial_ok, partial_err = storage:download_object("backups/large-file.bin", partial, {
    range = "bytes=0-1023"
})
local _, partial_close_err = partial:close()

storage:release()
if partial_err then
    if partial_close_err then report_cleanup_error(partial_close_err) end
    return nil, partial_err
end
if partial_close_err then return nil, partial_close_err end
return partial_ok
Parameter Type Description
key string Object key to download
writer Writer Destination file writer
options.range string Byte range (e.g., "bytes=0-1023")
options.if_match string Download only if the object ETag matches
options.if_none_match string Download only if the ETag does not match

Returns: boolean, error

A failed precondition (if_match/if_none_match) returns a precondition_failed error.

Listing Objects

List objects with optional prefix filtering:

local storage, storage_err = cloudstorage.get("app.infra:files")
if storage_err then return nil, storage_err end

local result, err = storage:list_objects({
    prefix = "reports/2024/",
    max_keys = 100
})
if err then
    storage:release()
    return nil, err
end

for _, obj in ipairs(result.objects) do
    print(obj.key, obj.size, obj.etag)
end

-- Paginate through large results
local token = nil
repeat
    local page, page_err = storage:list_objects({
        prefix = "logs/",
        max_keys = 1000,
        continuation_token = token
    })
    if page_err then
        storage:release()
        return nil, page_err
    end
    for _, obj in ipairs(page.objects) do
        process(obj)
    end
    token = page.next_continuation_token
    if not page.is_truncated then break end
until false

storage:release()
Parameter Type Description
options.prefix string Filter by key prefix
options.max_keys integer Maximum objects to return
options.continuation_token string Pagination token
options.include_owner boolean Include each object's owner (id, display_name)
options.include_versions boolean List object versions; each item includes version_id

Returns: table, error

Result contains objects, is_truncated, next_continuation_token. Each object has key, size, etag, storage_class, and optional last_modified, version_id, and owner.

In list results content_type is always empty — S3 list operations do not return it. Use head_object to read an object's content type and metadata.

Object Metadata

Fetch a single object's metadata without downloading its body:

local storage, storage_err = cloudstorage.get("app.infra:files")
if storage_err then return nil, storage_err end

local meta, err = storage:head_object("reports/daily.json")
if err then
    storage:release()
    return nil, err
end

print(meta.size, meta.etag, meta.content_type)
for k, v in pairs(meta.metadata) do
    print("meta", k, v)
end

storage:release()
Parameter Type Description
key string Object key

Returns: table, error

Result fields:

Field Type Description
size integer Object size in bytes
etag string Entity tag
content_type string MIME type
cache_control string Cache-Control header
content_disposition string Content-Disposition header
content_encoding string Content-Encoding header
storage_class string Storage class
version_id string Version ID (present when versioning is enabled)
last_modified integer Last modified time (Unix seconds)
metadata table User metadata (x-amz-meta-*)
headers table Raw response headers (lowercased keys)

A missing object returns a not_found error.

Deleting Objects

Remove multiple objects:

local storage, storage_err = cloudstorage.get("app.infra:files")
if storage_err then return nil, storage_err end

local deleted, err = storage:delete_objects({
    "temp/file1.txt",
    "temp/file2.txt",
    "temp/file3.txt"
})

storage:release()
if err then return nil, err end
return deleted
Parameter Type Description
keys string[] Array of object keys to delete

Returns: boolean, error

Every key is attempted. Deleting a key that does not exist is not an error. When the provider reports per-key failures, the call returns a single error naming each failed key and its provider error code.

Download URLs

Create a temporary URL that permits downloading an object without storage credentials. A client can use the URL until it expires.

local storage, err = cloudstorage.get("app.infra:files")
if err then
    return nil, err
end

local url, err = storage:presigned_get_url("reports/quarterly.pdf", {
    expiration = 3600
})

storage:release()

if err then
    return nil, err
end

-- Return URL to client for direct download
return {download_url = url}
Parameter Type Description
key string Object key
options.expiration integer Seconds until URL expires (default: 3600)

Returns: string, error

Upload URLs

Create a temporary URL that permits uploading an object without storage credentials. A client can upload directly to storage until the URL expires.

local storage, err = cloudstorage.get("app.infra:files")
if err then
    return nil, err
end

local url, err = storage:presigned_put_url("uploads/user-123/avatar.jpg", {
    expiration = 600,
    content_type = "image/jpeg",
    content_length = 1024 * 1024
})

storage:release()

if err then
    return nil, err
end

-- Return URL to client for direct upload
return {upload_url = url}
Parameter Type Description
key string Object key
options.expiration integer Seconds until URL expires (default: 3600)
options.content_type string Required content type for upload
options.content_length integer Expected upload size in bytes

Returns: string, error

Multipart Uploads

A single presigned PUT caps an object at 5 GiB. A presigned multipart upload splits a larger object into parts that a client uploads directly, then assembles them server-side. Multipart is a provider capability: S3 implements it, and providers without it return errors.UNAVAILABLE.

local storage = cloudstorage.get("app.infra:files")

local mp, err = storage:create_multipart_upload("backups/huge.zip", {
    content_type = "application/zip",
    metadata = { source = "uploader" },
})
if err then return nil, err end

local urls, err = storage:presigned_part_urls("backups/huge.zip", mp.upload_id, {
    count = 3,
    expiration = 900,
})
if err then
    storage:abort_multipart_upload("backups/huge.zip", mp.upload_id)
    return nil, err
end

-- The client PUTs each url and returns the ETag from the response headers.
local done, err = storage:complete_multipart_upload("backups/huge.zip", mp.upload_id, {
    { part_number = 1, etag = etag1 },
    { part_number = 2, etag = etag2 },
    { part_number = 3, etag = etag3 },
})

storage:release()

create_multipart_upload

Start a multipart upload for a key.

Parameter Type Description
key string Object key of the final object
options table content_type, cache_control, content_disposition, content_encoding, metadata, headers - same semantics as upload_object

Returns: table, error - the table carries upload_id, which identifies the upload for every later part, complete and abort call.

Conditional writes (if_match, if_none_match, only_if_absent) are not part of the multipart protocol and are not accepted here.

presigned_part_urls

Generate presigned PUT URLs for parts of an in-progress upload. Each URL is uploaded to with a plain HTTP PUT; the uploader must keep the ETag response header of each part for complete_multipart_upload.

Parameter Type Default Description
key string required Object key
upload_id string required From create_multipart_upload
options.parts int[] - Explicit part numbers (1-10000, no duplicates)
options.count int - Presign parts 1..count
options.headers table - Headers required on each part request; they are signed and must also be sent by the uploader
options.expiration int 3600 Seconds until the URLs expire

Exactly one of parts or count is required, and a single call presigns at most 1000 URLs - presign in pages for very large objects.

Returns: table, error - an array of { part_number, url }.

Every part except the last must be at least 5 MiB; the provider enforces this at completion time.

complete_multipart_upload

Assemble the final object from its uploaded parts. Parts may be reported in any order and are sorted by part number before completion.

Parameter Type Description
key string Object key
upload_id string From create_multipart_upload
parts table Array of { part_number = int, etag = string }

Returns: table, error - etag, plus version_id and location when the provider reports them. An unknown upload ID returns errors.NOT_FOUND.

abort_multipart_upload

Discard an in-progress upload and free its stored parts.

Parameter Type Description
key string Object key
upload_id string From create_multipart_upload

Returns: boolean, error

An upload that is never completed keeps its parts stored, and billed, until it is aborted. Abort on every failure path, and configure a bucket lifecycle rule as a backstop - see Cloud Storage.

Ranged Readers

open_reader opens random access over an object using ranged GETs - no local staging and no full download. Its main consumer is archive.open, which reads multi-GB archives straight out of object storage with bounded memory.

local archive = require("archive")
local storage = cloudstorage.get("app.infra:files")

local reader, err = storage:open_reader("uploads/huge.zip", {
    block_size = 8 * 1024 * 1024,
    cache_blocks = 4,
})
if err then return nil, err end

local r = assert(archive.open(reader))
for e in r:entries() do
    print(e.name, e.size)
end
r:close()
reader:close()

storage:release()
Parameter Type Default Description
key string required Object key
options.block_size int 8388608 Ranged-GET unit in bytes (64 KiB to 128 MiB)
options.cache_blocks int 4 Resident LRU blocks (1 to 64)

block_size * cache_blocks may not exceed 256 MiB. A missing object returns errors.NOT_FOUND.

Returns: Reader, error

The object's ETag is pinned when the reader opens and sent as If-Match on every ranged read, so an object overwritten mid-read fails the read with the provider's precondition error instead of serving a mix of two object generations; archive surfaces it as errors.INTERNAL. A provider that cannot supply an ETag returns errors.UNAVAILABLE; the reader never serves an unpinned object.

Cache-miss reads perform blocking network IO in the calling task and serialize concurrent readers, so sequential per-entry access - the archive pattern - is the intended shape.

Reader Methods

Method Returns Description
size() integer Object size in bytes, from the open-time stat
key() string Object key the reader reads from
close() boolean, error Release the block cache; idempotent

The reader is closed automatically at task scope if it is not closed explicitly.

Storage Methods

Method Returns Description
upload_object(key, content, opts?) boolean, error Upload string or file content
download_object(key, writer, opts?) boolean, error Download to file writer
head_object(key) table, error Fetch object metadata
list_objects(opts?) table, error List objects with prefix filter
delete_objects(keys) boolean, error Delete multiple objects
presigned_get_url(key, opts?) string, error Generate temporary download URL
presigned_put_url(key, opts?) string, error Generate temporary upload URL
create_multipart_upload(key, opts?) table, error Start a presigned multipart upload
presigned_part_urls(key, upload_id, opts) table, error Presign PUT URLs for upload parts
complete_multipart_upload(key, upload_id, parts) table, error Assemble the object from uploaded parts
abort_multipart_upload(key, upload_id) boolean, error Discard an in-progress multipart upload
open_reader(key, opts?) Reader, error Open a ranged random-access reader
release() boolean Release storage resource

Permissions

Security policy evaluation applies to cloud storage operations.

Action Resource Description
cloudstorage.get Storage ID Acquire a storage resource

Errors

Condition Kind Retryable
Empty resource ID errors.INVALID no
Resource not found errors.NOT_FOUND no
Not a cloud storage resource errors.INVALID no
Storage released errors.INVALID no
Empty key errors.INVALID no
Content nil errors.INVALID no
Writer not valid errors.INVALID no
Object not found errors.NOT_FOUND no
Unknown upload ID errors.NOT_FOUND no
Conditional precondition failed errors.CONFLICT no
Object overwritten during a ranged read (surfaced by archive) errors.INTERNAL no
Provider does not support multipart uploads errors.UNAVAILABLE no
Provider supplies no ETag for open_reader errors.UNAVAILABLE no
Permission denied raised as a Lua error, not returned -
Provider operation failed errors.UNKNOWN unset

See Error Handling for working with errors.