Skip to content

Commit 45279e3

Browse files
emmaling27Spioune
authored and
Convex, Inc.
committed
convex-backend PR 53: Add support for Cloudflare R2 Storage (#34993)
This PR introduces support for Cloudflare R2 by adding a new environment variable, AWS_ENDPOINT_URL, and replacing get_object_attributes with head_object. From the [S3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/get_object_attributes.html), get_object_attributes combines the functionality of HeadObject and ListParts in a single call. In the original code get_object_attributes was used to get the file size. Since R2 does not support get_object_attributes, I use head_object instead. @emmaling27 I would like your help on this. I wanted to name the env variable S3_ENDPOINT_URL but I read that AWS automatically provides AWS_ENDPOINT_URL to lambda functions. I can see you use `aws_config::defaults(BehaviorVersion::v2024_03_28())` which is equivalent to `aws_config::from_env()`, which I assumed automatically set defaults based on env variables ? From my tests it doesn't seem to do anything. So right now I call `.endpoint_url(endpoint_url)` explicitly. What is the best way to make it optional ? (I don't know much about rust) Right now it's a mandatory variable ⚠️ which is not what we want. ---- By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Spioune <[email protected]> GitOrigin-RevId: f465784d59ebd1de2bb1de054669a78e5e68b293
1 parent 64bbd8e commit 45279e3

File tree

4 files changed

+25
-18
lines changed

4 files changed

+25
-18
lines changed

crates/aws_s3/src/storage.rs

+11-16
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use async_trait::async_trait;
99
use aws_config::retry::RetryConfig;
1010
use aws_sdk_s3::{
1111
operation::{
12-
get_object_attributes::{
13-
GetObjectAttributesError,
14-
GetObjectAttributesOutput,
12+
head_object::{
13+
HeadObjectError,
14+
HeadObjectOutput,
1515
},
1616
upload_part::builders::UploadPartFluentBuilder,
1717
},
@@ -26,7 +26,7 @@ use aws_sdk_s3::{
2626
Client,
2727
};
2828
use aws_utils::{
29-
must_config_from_env,
29+
must_s3_config_from_env,
3030
s3::S3Client,
3131
};
3232
use bytes::Bytes;
@@ -132,7 +132,7 @@ impl<RT: Runtime> S3Storage<RT> {
132132
key_prefix: String,
133133
runtime: RT,
134134
) -> anyhow::Result<Self> {
135-
let config = must_config_from_env()
135+
let config = must_s3_config_from_env()
136136
.context("AWS env variables are required when using S3 storage")?
137137
.retry_config(RetryConfig::standard())
138138
.load()
@@ -433,27 +433,22 @@ impl<RT: Runtime> Storage for S3Storage<RT> {
433433
.as_str()
434434
.split_once('/')
435435
.with_context(|| format!("Invalid fully qualified S3 key {:?}", key))?;
436-
let result: Result<
437-
GetObjectAttributesOutput,
438-
aws_sdk_s3::error::SdkError<GetObjectAttributesError>,
439-
> = self
436+
let result: Result<HeadObjectOutput, aws_sdk_s3::error::SdkError<HeadObjectError>> = self
440437
.client
441-
.get_object_attributes()
438+
.head_object()
442439
.bucket(bucket)
443440
.key(s3_key)
444-
.object_attributes(aws_sdk_s3::types::ObjectAttributes::Checksum)
445-
.object_attributes(aws_sdk_s3::types::ObjectAttributes::ObjectSize)
446441
.send()
447442
.await;
448443
match result {
449-
Ok(object_attributes) => {
450-
let size = object_attributes
451-
.object_size
444+
Ok(head_attributes) => {
445+
let size = head_attributes
446+
.content_length
452447
.context("Object is missing size")? as u64;
453448
Ok(Some(ObjectAttributes { size }))
454449
},
455450
Err(aws_sdk_s3::error::SdkError::ServiceError(err)) => match err.err() {
456-
GetObjectAttributesError::NoSuchKey(_) => Ok(None),
451+
HeadObjectError::NotFound(_) => Ok(None),
457452
// Other service errors from S3
458453
_ => Err(err.into_err().into()),
459454
},

crates/aws_utils/src/lib.rs

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use aws_types::region::Region;
1414

1515
pub mod s3;
1616

17+
static S3_ENDPOINT_URL: LazyLock<Option<String>> =
18+
LazyLock::new(|| env::var("S3_ENDPOINT_URL").ok());
19+
1720
static AWS_ACCESS_KEY_ID: LazyLock<Option<String>> =
1821
LazyLock::new(|| env::var("AWS_ACCESS_KEY_ID").ok());
1922

@@ -41,3 +44,11 @@ pub fn must_config_from_env() -> anyhow::Result<ConfigLoader> {
4144
.region(region)
4245
.credentials_provider(credentials))
4346
}
47+
48+
pub fn must_s3_config_from_env() -> anyhow::Result<ConfigLoader> {
49+
let mut config_loader = must_config_from_env()?;
50+
if let Some(s3_endpoint_url) = S3_ENDPOINT_URL.clone() {
51+
config_loader = config_loader.endpoint_url(s3_endpoint_url);
52+
}
53+
Ok(config_loader)
54+
}

crates/aws_utils/src/s3.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use aws_sdk_s3::{
88
};
99
use futures_async_stream::try_stream;
1010

11-
use crate::must_config_from_env;
11+
use crate::must_s3_config_from_env;
1212

1313
#[derive(Clone, Debug)]
1414
pub struct S3Client(pub Client);
@@ -21,7 +21,7 @@ impl S3Client {
2121
true => RetryConfig::standard(),
2222
false => RetryConfig::disabled(),
2323
};
24-
let config = must_config_from_env()
24+
let config = must_s3_config_from_env()
2525
.context("AWS env variables are required when using AWS Lambda")?
2626
.retry_config(retry_config)
2727
.load()

self-hosted/docker/docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ services:
3030
- S3_STORAGE_MODULES_BUCKET=${S3_STORAGE_MODULES_BUCKET:-}
3131
- S3_STORAGE_FILES_BUCKET=${S3_STORAGE_FILES_BUCKET:-}
3232
- S3_STORAGE_SEARCH_BUCKET=${S3_STORAGE_SEARCH_BUCKET:-}
33+
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
3334

3435
healthcheck:
3536
test: curl -f http://localhost:3210/version

0 commit comments

Comments
 (0)