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.