S3 storage: signing and request builders

akurai-s3 lets an application talk to S3-compatible object storage (AWS, MinIO, any SigV4 store) without linking an AWS SDK, an XML crate, or a hash crate. It is the whole client except the socket: the framework links no TLS, so you hand it a Transport that performs one HTTP round-trip, and it does everything else.

Transport

use akurai_s3::{HttpRequest, HttpResponse, Transport};

struct Ureq;

impl Transport for Ureq {
    fn send(&self, r: HttpRequest) -> Result<HttpResponse, String> {
        let mut req = ureq::request(&r.method, &r.url);
        for (k, v) in &r.headers { req = req.set(k, v); }
        let resp = req.send_bytes(&r.body).or_any_status().map_err(|e| e.to_string())?;
        Ok(HttpResponse {
            status: resp.status(),
            headers: resp.headers_names().iter()
                .map(|n| (n.clone(), resp.header(n).unwrap_or("").to_string())).collect(),
            body: { let mut b = Vec::new(); resp.into_reader().read_to_end(&mut b).ok(); b },
        })
    }
}

Rules: follow no redirects, and return non-2xx as Ok — the client parses S3's <Error> body into a typed S3Error { status, code, message }.

Client

use akurai_s3::{Client, Credentials, LifecycleRule};

let s3 = Client::new(
    Credentials { access_key, secret_key, session_token: None },
    "eu-west-1",
    Ureq,
);
// Self-hosted / MinIO:
// let s3 = s3.path_style("192.168.1.10:9000", false);

let page = s3.list_objects("bucket", Some("docs/"), Some("/"), None, 1000)?;
page.common_prefixes;       // ["docs/photos/", …]  — folders
page.objects;               // key, size, etag, last_modified
page.next_continuation;     // opaque cursor for the next page

let meta = s3.head_object("bucket", "docs/a.pdf")?;
s3.copy_object("bucket", "docs/a.pdf", None, "bucket", "archive/a.pdf")?;
s3.delete_object("bucket", "docs/a.pdf", None)?;           // delete marker if versioned
let failed = s3.delete_objects("bucket", &[("x", None), ("y", Some("v3"))])?;

Presigned URLs

Hand browsers and sync clients a time-limited URL so bytes never pass through your server:

let put = s3.presign_object("PUT", "bucket", "up/big.bin", 900, &[]);
let get = s3.presign_object("GET", "bucket", "up/big.bin", 900,
    &[("response-content-disposition", "attachment; filename=\"big.bin\"")]);

Multipart uploads

let id = s3.create_multipart_upload("bucket", "up/big.bin", "application/octet-stream")?;
let part_url = s3.presign_upload_part("bucket", "up/big.bin", &id, 1, 900);
// …client PUTs bytes to part_url, collects the ETag…
let done = s3.list_parts("bucket", "up/big.bin", &id)?;       // resume support
s3.complete_multipart_upload("bucket", "up/big.bin", &id, &[(1, "etag1")])?;
// or s3.abort_multipart_upload(...)

Versioning, trash, lifecycle

s3.put_bucket_versioning("bucket", true)?;
let versions = s3.list_object_versions("bucket", Some("docs/"), None, None, 1000)?;
// versions[i].is_delete_marker → "in trash"; restore = delete_object(key, Some(marker_id))
s3.put_bucket_lifecycle("bucket", &[LifecycleRule {
    id: "trash".into(), prefix: String::new(), enabled: true,
    noncurrent_days: Some(30), abort_multipart_days: Some(1), expire_delete_markers: true,
}])?;

Signing directly

sigv4::sign_headers and sigv4::presign are pure functions of (credentials, region, now, request); use them when you need to sign a request the builders do not cover. crates/s3/tests/s3.rs checks them against the four vectors AWS publishes (presigned GET, header-signed GET with Range, GET ?lifecycle, and PUT with a signed payload).

What it is not

No streaming bodies (use presigned URLs for large transfers), no S3 Select, no bucket policies/ACLs, no MD5 (x-amz-checksum-sha256 is sent instead where AWS requires an integrity header). Add builders as apps need them.