r2kit 0.1.0

A safe, ergonomic Rust toolkit for Cloudflare R2 object storage.
Documentation
use r2kit::{
    Error, ManagedUploadCancellation, MultipartSessionSnapshot, R2Client, R2Config, ValidationError,
};

fn offline_bucket() -> r2kit::Bucket {
    let config = R2Config::builder()
        .account_id("0123456789abcdef0123456789abcdef")
        .access_key_id("managed-access")
        .secret_access_key("managed-secret")
        .build()
        .unwrap();
    R2Client::new(config).bucket("managed-tests").unwrap()
}

#[tokio::test]
async fn rejects_invalid_managed_limits_before_file_or_network_io() {
    let bucket = offline_bucket();

    let concurrency = bucket
        .managed_multipart("object.bin")
        .unwrap()
        .concurrency(0)
        .upload_file("path-does-not-exist")
        .await
        .unwrap_err();
    assert!(matches!(
        concurrency.error(),
        Error::Validation(ValidationError::ConcurrencyOutOfRange {
            provided: 0,
            min: 1,
            max: 64
        })
    ));

    let attempts = bucket
        .managed_multipart("object.bin")
        .unwrap()
        .max_attempts(11)
        .upload_file("path-does-not-exist")
        .await
        .unwrap_err();
    assert!(matches!(
        attempts.error(),
        Error::Validation(ValidationError::AttemptsOutOfRange {
            provided: 11,
            min: 1,
            max: 10
        })
    ));

    let part_size = bucket
        .managed_multipart("object.bin")
        .unwrap()
        .part_size(1024)
        .upload_file("path-does-not-exist")
        .await
        .unwrap_err();
    assert!(matches!(
        part_size.error(),
        Error::Validation(ValidationError::PartSizeOutOfRange {
            provided: 1024,
            min: 5_242_880,
            max: 5_363_466_240
        })
    ));

    let overflowed_mib = bucket
        .managed_multipart("object.bin")
        .unwrap()
        .part_size_mib(u64::MAX)
        .upload_file("path-does-not-exist")
        .await
        .unwrap_err();
    assert!(matches!(
        overflowed_mib.error(),
        Error::Validation(ValidationError::PartSizeOutOfRange {
            provided: u64::MAX,
            ..
        })
    ));

    let memory_budget = bucket
        .managed_multipart("object.bin")
        .unwrap()
        .part_size_mib(64)
        .concurrency(8)
        .max_buffered_mib(256)
        .upload_file("path-does-not-exist")
        .await
        .unwrap_err();
    assert!(matches!(
        memory_budget.error(),
        Error::Validation(ValidationError::ManagedMemoryBudgetExceeded {
            required: 536_870_912,
            max: 268_435_456,
        })
    ));

    let exact_memory_budget = bucket
        .managed_multipart("object.bin")
        .unwrap()
        .part_size_mib(64)
        .concurrency(4)
        .max_buffered_mib(256)
        .upload_file("path-does-not-exist")
        .await
        .unwrap_err();
    assert!(matches!(
        exact_memory_budget.error(),
        Error::Io {
            operation: "metadata"
        }
    ));
}

#[test]
fn resumed_builder_redacts_the_upload_id() {
    let bucket = offline_bucket();
    let snapshot = MultipartSessionSnapshot::restore(
        "managed-tests",
        "object.bin",
        "sensitive-managed-upload-id",
        11 * 1024 * 1024,
        5 * 1024 * 1024,
    )
    .unwrap();
    let builder = bucket.resume_managed_multipart(snapshot).unwrap();
    let debug = format!("{builder:?}");
    assert!(!debug.contains("sensitive-managed-upload-id"));
}

#[tokio::test]
async fn pre_cancelled_upload_never_starts_a_remote_session() {
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("cancelled.bin");
    tokio::fs::write(&path, vec![0_u8; 5 * 1024 * 1024])
        .await
        .unwrap();
    let cancellation = ManagedUploadCancellation::new();
    cancellation.cancel();

    let error = offline_bucket()
        .managed_multipart("cancelled.bin")
        .unwrap()
        .cancellation_token(cancellation)
        .upload_file(path)
        .await
        .unwrap_err();

    assert!(matches!(error.error(), Error::Cancelled));
    assert!(!error.was_aborted());
    assert!(error.snapshot().is_none());
}