Skip to content

Commit e986e73

Browse files
committed
Wire up S3 storage configuration.
1 parent 3178180 commit e986e73

File tree

8 files changed

+186
-26
lines changed

8 files changed

+186
-26
lines changed

Cargo.lock

Lines changed: 85 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/content/docs/_roadmap.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ friction.
66
For context, some larger features we have on our Roadmap:
77

88
- Realtime/notification APIs for subscribing to data changes.
9-
- S3 buckets and other cloud storage. The backend already supports it but it isn't wired up.
109
- Auth: more customizable settings, more customizable UI, and multi-factor.
1110
Also, service-accounts to auth other backends as opposed to end-users.
1211
- Many SQLite databases: imagine a separate database by tenant or user.

proto/config.proto

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ message AuthConfig {
5454
map<string, OAuthProviderConfig> oauth_providers = 11;
5555
}
5656

57+
message S3StorageConfig {
58+
optional string endpoint = 1;
59+
optional string region = 2;
60+
61+
optional string bucket_name = 5;
62+
63+
/// S3 access key, a.k.a. username.
64+
optional string access_key = 8;
65+
/// S3 secret access key, a.k.a. password.
66+
optional string secret_access_key = 9 [ (secret) = true ];
67+
}
68+
5769
message ServerConfig {
5870
/// Application name presented to users, e.g. when sending emails. Default:
5971
/// "TrailBase".
@@ -71,6 +83,9 @@ message ServerConfig {
7183
/// Interval at which backups are persisted. Setting it to 0 will disable
7284
/// backups. Default: 0.
7385
optional int64 backup_interval_sec = 12;
86+
87+
/// If present will use S3 setup over local file-system based storage.
88+
optional S3StorageConfig s3_storage_config = 13;
7489
}
7590

7691
/// Sqlite specific (as opposed to standard SQL) constrained-violation

trailbase-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ libsql = { workspace = true }
4040
log = "^0.4.21"
4141
minijinja = "2.1.2"
4242
oauth2 = { version = "5.0.0-alpha.4", default-features = false, features = ["reqwest", "rustls-tls"] }
43-
object_store = { version = "0.11.0", default-features = false }
43+
object_store = { version = "0.11.0", default-features = false, features=["aws"] }
4444
parking_lot = "0.12.3"
4545
prost = "^0.12.6"
4646
prost-reflect = { version = "^0.13.0", features = ["derive", "text-format"] }

trailbase-core/src/app_state.rs

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::sync::Arc;
66

77
use crate::auth::jwt::JwtHelper;
88
use crate::auth::oauth::providers::{ConfiguredOAuthProviders, OAuthProviderType};
9-
use crate::config::proto::{Config, QueryApiConfig, RecordApiConfig};
9+
use crate::config::proto::{Config, QueryApiConfig, RecordApiConfig, S3StorageConfig};
1010
use crate::config::{validate_config, write_config_and_vault_textproto};
1111
use crate::constants::SITE_URL_DEFAULT;
1212
use crate::data_dir::DataDir;
@@ -36,8 +36,8 @@ struct InternalState {
3636
jwt: JwtHelper,
3737

3838
table_metadata: TableMetadataCache,
39+
object_store: Box<dyn ObjectStore + Send + Sync>,
3940

40-
#[allow(unused)]
4141
runtime: RuntimeHandle,
4242

4343
#[cfg(test)]
@@ -54,6 +54,7 @@ pub(crate) struct AppStateArgs {
5454
pub conn: Connection,
5555
pub logs_conn: Connection,
5656
pub jwt: JwtHelper,
57+
pub object_store: Box<dyn ObjectStore + Send + Sync>,
5758
pub js_runtime_threads: Option<usize>,
5859
}
5960

@@ -125,6 +126,7 @@ impl AppState {
125126
logs_conn: args.logs_conn,
126127
jwt: args.jwt,
127128
table_metadata: args.table_metadata,
129+
object_store: args.object_store,
128130
runtime,
129131
#[cfg(test)]
130132
cleanup: vec![],
@@ -166,13 +168,8 @@ impl AppState {
166168
self.table_metadata().invalidate_all().await
167169
}
168170

169-
pub(crate) fn objectstore(
170-
&self,
171-
) -> Result<Box<dyn ObjectStore + Send + Sync>, object_store::Error> {
172-
// FIXME: We should probably have a long-lived store on AppState.
173-
return Ok(Box::new(
174-
object_store::local::LocalFileSystem::new_with_prefix(self.data_dir().uploads_path())?,
175-
));
171+
pub(crate) fn objectstore(&self) -> &(dyn ObjectStore + Send + Sync) {
172+
return &*self.state.object_store;
176173
}
177174

178175
pub(crate) fn get_oauth_provider(&self, name: &str) -> Option<Arc<OAuthProviderType>> {
@@ -371,12 +368,32 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
371368
let main_conn_clone1 = main_conn.clone();
372369
let table_metadata_clone = table_metadata.clone();
373370

371+
let data_dir = DataDir(temp_dir.path().to_path_buf());
372+
373+
let object_store = if std::env::var("TEST_S3_OBJECT_STORE").map_or(false, |v| v == "TRUE") {
374+
info!("Use S3 Storage for tests");
375+
376+
build_objectstore(
377+
&data_dir,
378+
Some(&S3StorageConfig {
379+
endpoint: Some("http://127.0.0.1:9000".to_string()),
380+
region: None,
381+
bucket_name: Some("test".to_string()),
382+
access_key: Some("minioadmin".to_string()),
383+
secret_access_key: Some("minioadmin".to_string()),
384+
}),
385+
)
386+
.unwrap()
387+
} else {
388+
build_objectstore(&data_dir, None).unwrap()
389+
};
390+
374391
let runtime = RuntimeHandle::new();
375392
runtime.set_connection(main_conn.clone());
376393

377394
return Ok(AppState {
378395
state: Arc::new(InternalState {
379-
data_dir: DataDir(temp_dir.path().to_path_buf()),
396+
data_dir,
380397
public_dir: None,
381398
dev: true,
382399
oauth: Computed::new(&config, |c| {
@@ -415,6 +432,7 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
415432
logs_conn,
416433
jwt: jwt::test_jwt_helper(),
417434
table_metadata,
435+
object_store,
418436
runtime,
419437
cleanup: vec![Box::new(temp_dir)],
420438
}),
@@ -445,3 +463,44 @@ fn build_query_api(conn: libsql::Connection, config: QueryApiConfig) -> Result<Q
445463
// TODO: Check virtual table exists
446464
return QueryApi::from(conn, config);
447465
}
466+
467+
pub(crate) fn build_objectstore(
468+
data_dir: &DataDir,
469+
config: Option<&S3StorageConfig>,
470+
) -> Result<Box<dyn ObjectStore + Send + Sync>, object_store::Error> {
471+
if let Some(config) = config {
472+
let mut builder = object_store::aws::AmazonS3Builder::from_env();
473+
474+
if let Some(ref endpoint) = config.endpoint {
475+
builder = builder.with_endpoint(endpoint);
476+
477+
if endpoint.starts_with("http://") {
478+
builder =
479+
builder.with_client_options(object_store::ClientOptions::default().with_allow_http(true))
480+
}
481+
}
482+
483+
if let Some(ref region) = config.region {
484+
builder = builder.with_region(region);
485+
}
486+
487+
let Some(ref bucket_name) = config.bucket_name else {
488+
panic!("S3StorageConfig missing 'bucket_name'.");
489+
};
490+
builder = builder.with_bucket_name(bucket_name);
491+
492+
if let Some(ref access_key) = config.access_key {
493+
builder = builder.with_access_key_id(access_key);
494+
}
495+
496+
if let Some(ref secret_access_key) = config.secret_access_key {
497+
builder = builder.with_secret_access_key(secret_access_key);
498+
}
499+
500+
return Ok(Box::new(builder.build()?));
501+
}
502+
503+
return Ok(Box::new(
504+
object_store::local::LocalFileSystem::new_with_prefix(data_dir.uploads_path())?,
505+
));
506+
}

trailbase-core/src/records/files.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub(crate) async fn read_file_into_response(
2525
state: &AppState,
2626
file_upload: FileUpload,
2727
) -> Result<Response, FileError> {
28-
let store = state.objectstore()?;
28+
let store = state.objectstore();
2929
let path = object_store::path::Path::from(file_upload.path());
3030
let result = store.get(&path).await?;
3131

@@ -69,19 +69,19 @@ pub(crate) async fn delete_files_in_row(
6969
};
7070

7171
if let Some(json) = &column_metadata.json {
72-
let store = state.objectstore()?;
72+
let store = state.objectstore();
7373
match json {
7474
JsonColumnMetadata::SchemaName(name) if name == "std.FileUpload" => {
7575
if let Ok(json) = row.get_str(i) {
7676
let file: FileUpload = serde_json::from_str(json)?;
77-
delete_file(&*store, file).await?;
77+
delete_file(store, file).await?;
7878
}
7979
}
8080
JsonColumnMetadata::SchemaName(name) if name == "std.FileUploads" => {
8181
if let Ok(json) = row.get_str(i) {
8282
let file_uploads: FileUploads = serde_json::from_str(json)?;
8383
for file in file_uploads.0 {
84-
delete_file(&*store, file).await?;
84+
delete_file(store, file).await?;
8585
}
8686
}
8787
}

0 commit comments

Comments
 (0)