bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ByteRange {
    pub(crate) start: usize,
    pub(crate) end: usize,
    total: usize,
}

impl ByteRange {
    fn len(self) -> usize {
        self.end - self.start + 1
    }

    fn content_range(self) -> String {
        format!("bytes {}-{}/{}", self.start, self.end, self.total)
    }
}

pub(crate) fn object_response(
    mut result: GetObjectResult,
    range_header: Option<&str>,
    query: &BTreeMap<String, String>,
) -> Result<S3HttpResponse, RuntimeError> {
    let range = range_header
        .map(|value| parse_byte_range(value, result.body.len()))
        .transpose()?;
    if let Some(range) = range {
        result.body = result.body[range.start..=range.end].to_vec();
    }
    let status = if range.is_some() { 206 } else { 200 };
    let encryption = result.metadata.encryption.clone();
    let mut response = S3HttpResponse::new(status)
        .with_header("content-type", result.metadata.content_type)
        .with_header("content-length", result.body.len().to_string())
        .with_header("etag", quote_etag(&result.etag))
        .with_header(
            "last-modified",
            result.last_modified_epoch_seconds.to_string(),
        )
        .with_header("x-amz-version-id", result.version_id)
        .with_body(result.body);
    if let Some(range) = range {
        response
            .headers
            .insert("content-range".to_string(), range.content_range());
    }
    if let Some(content_encoding) = result.metadata.content_encoding {
        response
            .headers
            .insert("content-encoding".to_string(), content_encoding);
    }
    for (key, value) in result.metadata.user_metadata {
        response.headers.insert(format!("x-amz-meta-{key}"), value);
    }
    response = apply_encryption_response_headers(response, encryption.as_ref());
    response = apply_replication_status_header(response, result.replication_status.as_deref());
    response = apply_response_header_overrides(response, query);
    Ok(response)
}

pub(crate) fn head_response(
    result: HeadObjectResult,
    range_header: Option<&str>,
    query: &BTreeMap<String, String>,
) -> Result<S3HttpResponse, RuntimeError> {
    let range = range_header
        .map(|value| parse_byte_range(value, result.content_length))
        .transpose()?;
    let status = if range.is_some() { 206 } else { 200 };
    let content_length = range
        .map(ByteRange::len)
        .unwrap_or(result.content_length)
        .to_string();
    let encryption = result.metadata.encryption.clone();
    let mut response = S3HttpResponse::new(status)
        .with_header("content-type", result.metadata.content_type)
        .with_header("content-length", content_length)
        .with_header("etag", quote_etag(&result.etag))
        .with_header(
            "last-modified",
            result.last_modified_epoch_seconds.to_string(),
        )
        .with_header("x-amz-version-id", result.version_id);
    if let Some(range) = range {
        response
            .headers
            .insert("content-range".to_string(), range.content_range());
    }
    if let Some(content_encoding) = result.metadata.content_encoding {
        response
            .headers
            .insert("content-encoding".to_string(), content_encoding);
    }
    for (key, value) in result.metadata.user_metadata {
        response.headers.insert(format!("x-amz-meta-{key}"), value);
    }
    response = apply_encryption_response_headers(response, encryption.as_ref());
    response = apply_replication_status_header(response, result.replication_status.as_deref());
    response = apply_response_header_overrides(response, query);
    Ok(response)
}

pub(crate) fn parse_byte_range(value: &str, total: usize) -> Result<ByteRange, RuntimeError> {
    let Some(spec) = value.trim().strip_prefix("bytes=") else {
        return Err(RuntimeError::InvalidRange(value.to_string()));
    };
    if total == 0 || spec.contains(',') {
        return Err(RuntimeError::InvalidRange(value.to_string()));
    }
    let Some((start, end)) = spec.split_once('-') else {
        return Err(RuntimeError::InvalidRange(value.to_string()));
    };
    let (start, end) = if start.is_empty() {
        let suffix_len = end
            .parse::<usize>()
            .map_err(|_| RuntimeError::InvalidRange(value.to_string()))?;
        if suffix_len == 0 {
            return Err(RuntimeError::InvalidRange(value.to_string()));
        }
        (total.saturating_sub(suffix_len), total - 1)
    } else {
        let start = start
            .parse::<usize>()
            .map_err(|_| RuntimeError::InvalidRange(value.to_string()))?;
        let end = if end.is_empty() {
            total - 1
        } else {
            end.parse::<usize>()
                .map_err(|_| RuntimeError::InvalidRange(value.to_string()))?
        };
        (start, end.min(total - 1))
    };
    if start > end || start >= total {
        return Err(RuntimeError::InvalidRange(value.to_string()));
    }
    Ok(ByteRange { start, end, total })
}