use async_trait::async_trait;
use cratefield_core::sigv4::{self, Credentials, SignableRequest};
use cratefield_core::{Blob, BlobError, BlobObject, Clock, PresignedPut, check_blob_size};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use worker::send::IntoSendFuture;
use worker::{Bucket, HttpMetadata};
const R2_REGION: &str = "auto";
const R2_SERVICE: &str = "s3";
fn op_err(err: &worker::Error) -> BlobError {
BlobError::Operation(err.to_string())
}
fn unsupported_presign() -> BlobError {
BlobError::Unsupported(
"R2 presigning needs the S3-compatible API and its access keys; configure \
Cloudflare::blob_presign(account_id, access_key_id, secret_access_key, bucket), \
or serve the bytes through the harness"
.to_owned(),
)
}
fn presigner(presigner: Option<&R2Presigner>) -> Result<&R2Presigner, BlobError> {
presigner.ok_or_else(unsupported_presign)
}
pub(crate) struct R2Presigner {
account_id: String,
bucket: String,
credentials: Credentials,
clock: Arc<dyn Clock>,
}
impl R2Presigner {
pub(crate) fn new(
account_id: impl Into<String>,
bucket: impl Into<String>,
credentials: Credentials,
clock: Arc<dyn Clock>,
) -> Self {
Self {
account_id: account_id.into(),
bucket: bucket.into(),
credentials,
clock,
}
}
fn sign(&self, method: &str, key: &str, headers: &[(String, String)], ttl: Duration) -> String {
let host = format!("{}.r2.cloudflarestorage.com", self.account_id);
let path = format!("/{}{}", self.bucket, sigv4::s3_key_path(key));
sigv4::presign(
&self.credentials,
R2_REGION,
R2_SERVICE,
&SignableRequest {
method,
host: &host,
path: &path,
query: &[],
headers,
},
ttl.as_secs().max(1),
self.clock.now(),
)
}
fn signed_url(&self, key: &str, ttl: Duration) -> String {
self.sign("GET", key, &[], ttl)
}
fn signed_put_url(
&self,
key: &str,
content_type: &str,
content_length: Option<u64>,
ttl: Duration,
) -> PresignedPut {
let mut headers = vec![("content-type".to_owned(), content_type.to_owned())];
if let Some(length) = content_length {
headers.push(("content-length".to_owned(), length.to_string()));
}
PresignedPut {
url: self.sign("PUT", key, &headers, ttl),
method: "PUT",
headers,
}
}
}
impl fmt::Debug for R2Presigner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("R2Presigner")
.field("account_id", &self.account_id)
.field("bucket", &self.bucket)
.field("credentials", &self.credentials)
.finish_non_exhaustive()
}
}
pub(crate) struct R2Blob {
bucket: Bucket,
presigner: Option<R2Presigner>,
}
impl R2Blob {
pub(crate) fn new(bucket: Bucket, presigner: Option<R2Presigner>) -> Self {
Self { bucket, presigner }
}
}
#[async_trait]
impl Blob for R2Blob {
async fn put(&self, key: &str, bytes: &[u8], content_type: &str) -> Result<(), BlobError> {
check_blob_size(bytes)?;
self.bucket
.put(key, bytes.to_vec())
.http_metadata(HttpMetadata {
content_type: Some(content_type.to_owned()),
..Default::default()
})
.execute()
.into_send()
.await
.map(|_| ())
.map_err(|err| op_err(&err))
}
async fn get(&self, key: &str) -> Result<Option<BlobObject>, BlobError> {
let Some(object) = self
.bucket
.get(key)
.execute()
.into_send()
.await
.map_err(|err| op_err(&err))?
else {
return Ok(None);
};
let content_type = object
.http_metadata()
.content_type
.unwrap_or_else(|| "application/octet-stream".to_owned());
let bytes = match object.body() {
Some(body) => body.bytes().into_send().await.map_err(|err| op_err(&err))?,
None => Vec::new(),
};
Ok(Some(BlobObject {
bytes,
content_type,
}))
}
async fn delete(&self, key: &str) -> Result<(), BlobError> {
self.bucket
.delete(key)
.into_send()
.await
.map_err(|err| op_err(&err))
}
async fn signed_url(&self, key: &str, ttl: Duration) -> Result<String, BlobError> {
Ok(presigner(self.presigner.as_ref())?.signed_url(key, ttl))
}
async fn signed_put_url(
&self,
key: &str,
content_type: &str,
content_length: Option<u64>,
ttl: Duration,
) -> Result<PresignedPut, BlobError> {
Ok(presigner(self.presigner.as_ref())?.signed_put_url(
key,
content_type,
content_length,
ttl,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use cratefield_core::sigv4::{SigV4Error, verify_presigned};
use time::OffsetDateTime;
const ACCESS_KEY: &str = "AKIAEXAMPLE";
const SECRET_KEY: &str = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
const KEY: &str = "renders/job 1/out.mp4";
const PATH: &str = "/media/renders/job%201/out.mp4";
struct TestClock(OffsetDateTime);
#[async_trait]
impl Clock for TestClock {
fn now(&self) -> OffsetDateTime {
self.0
}
}
fn at(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> OffsetDateTime {
let date =
time::Date::from_calendar_date(year, time::Month::try_from(month).expect("month"), day)
.expect("a valid test date");
let time = time::Time::from_hms(hour, minute, second).expect("a valid test time");
time::PrimitiveDateTime::new(date, time).assume_utc()
}
fn credentials() -> Credentials {
Credentials::new(ACCESS_KEY, SECRET_KEY)
}
fn presigner_at(now: OffsetDateTime) -> R2Presigner {
R2Presigner::new("acct123", "media", credentials(), Arc::new(TestClock(now)))
}
#[test]
fn a_signed_get_url_has_the_r2_shape_and_verifies() {
let now = at(2026, 1, 1, 0, 0, 0);
let url = presigner_at(now).signed_url(KEY, Duration::from_secs(600));
assert!(
url.starts_with(&format!("https://acct123.r2.cloudflarestorage.com{PATH}?")),
"{url}"
);
verify_presigned(&url, "GET", &[], &credentials(), R2_REGION, R2_SERVICE, now)
.expect("the URL verifies against the same credentials");
}
#[test]
fn a_signed_put_url_signs_content_type_and_length() {
let now = at(2026, 1, 1, 0, 0, 0);
let put = presigner_at(now).signed_put_url(
KEY,
"video/mp4",
Some(1024),
Duration::from_secs(600),
);
assert_eq!(put.method, "PUT");
assert_eq!(
put.headers,
vec![
("content-type".to_owned(), "video/mp4".to_owned()),
("content-length".to_owned(), "1024".to_owned()),
]
);
verify_presigned(
&put.url,
"PUT",
&put.headers,
&credentials(),
R2_REGION,
R2_SERVICE,
now,
)
.expect("the signed headers verify");
}
#[test]
fn a_url_is_refused_after_it_expires() {
let url = presigner_at(at(2026, 1, 1, 0, 0, 0)).signed_url(KEY, Duration::from_secs(600));
let verify_at =
|now| verify_presigned(&url, "GET", &[], &credentials(), R2_REGION, R2_SERVICE, now);
verify_at(at(2026, 1, 1, 0, 10, 0)).expect("the deadline itself still verifies");
assert_eq!(
verify_at(at(2026, 1, 1, 0, 10, 1)),
Err(SigV4Error::Expired)
);
}
#[test]
fn presigning_is_unsupported_without_a_presigner() {
assert!(matches!(presigner(None), Err(BlobError::Unsupported(_))));
assert!(presigner(Some(&presigner_at(at(2026, 1, 1, 0, 0, 0)))).is_ok());
}
}