Phase 1: storage + catalog layer
- storaged: object_store backend (LocalFileSystem), PUT/GET/DELETE/LIST endpoints
- shared: arrow_helpers with Parquet roundtrip + schema fingerprinting (2 tests)
- catalogd: in-memory registry with write-ahead manifest persistence to object storage
- catalogd: POST/GET /datasets, GET /datasets/by-name/{name}
- gateway: wires storaged + catalogd with shared object_store state
- Phase tracker updated: Phase 0 + Phase 1 gates passed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,3 +10,6 @@ axum = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
object_store = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use object_store::ObjectStore;
|
||||
use object_store::local::LocalFileSystem;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Initialize the object store backend.
|
||||
/// Starts with LocalFileSystem. Swap to S3/RustFS by changing this function.
|
||||
pub fn init_local(root: &str) -> Arc<dyn ObjectStore> {
|
||||
std::fs::create_dir_all(root).expect("failed to create storage root");
|
||||
Arc::new(LocalFileSystem::new_with_prefix(root).expect("failed to init local object store"))
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod backend;
|
||||
pub mod ops;
|
||||
pub mod service;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use bytes::Bytes;
|
||||
use futures::TryStreamExt;
|
||||
use object_store::{ObjectStore, path::Path};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn put(store: &Arc<dyn ObjectStore>, key: &str, data: Bytes) -> Result<(), String> {
|
||||
let path = Path::from(key);
|
||||
store.put(&path, data.into()).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(store: &Arc<dyn ObjectStore>, key: &str) -> Result<Bytes, String> {
|
||||
let path = Path::from(key);
|
||||
let result = store.get(&path).await.map_err(|e| e.to_string())?;
|
||||
let bytes = result.bytes().await.map_err(|e| e.to_string())?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub async fn delete(store: &Arc<dyn ObjectStore>, key: &str) -> Result<(), String> {
|
||||
let path = Path::from(key);
|
||||
store.delete(&path).await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list(store: &Arc<dyn ObjectStore>, prefix: Option<&str>) -> Result<Vec<String>, String> {
|
||||
let prefix_path = prefix.map(Path::from);
|
||||
let stream = store.list(prefix_path.as_ref());
|
||||
let items: Vec<_> = stream.try_collect().await.map_err(|e| e.to_string())?;
|
||||
Ok(items.into_iter().map(|m| m.location.to_string()).collect())
|
||||
}
|
||||
@@ -1,9 +1,75 @@
|
||||
use axum::{Router, routing::get};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, put},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use object_store::ObjectStore;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/health", get(health))
|
||||
use crate::ops;
|
||||
|
||||
pub type StorageState = Arc<dyn ObjectStore>;
|
||||
|
||||
pub fn router(store: Arc<dyn ObjectStore>) -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/objects/{*key}", put(put_object))
|
||||
.route("/objects/{*key}", get(get_object))
|
||||
.route("/objects/{*key}", delete(delete_object))
|
||||
.route("/objects", get(list_objects))
|
||||
.with_state(store)
|
||||
}
|
||||
|
||||
async fn health() -> &'static str {
|
||||
"storaged ok"
|
||||
}
|
||||
|
||||
async fn put_object(
|
||||
State(store): State<StorageState>,
|
||||
Path(key): Path<String>,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
match ops::put(&store, &key, body).await {
|
||||
Ok(()) => (StatusCode::CREATED, format!("stored: {key}")),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_object(
|
||||
State(store): State<StorageState>,
|
||||
Path(key): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match ops::get(&store, &key).await {
|
||||
Ok(data) => Ok(data),
|
||||
Err(e) => Err((StatusCode::NOT_FOUND, e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_object(
|
||||
State(store): State<StorageState>,
|
||||
Path(key): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match ops::delete(&store, &key).await {
|
||||
Ok(()) => (StatusCode::OK, format!("deleted: {key}")),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListQuery {
|
||||
prefix: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_objects(
|
||||
State(store): State<StorageState>,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> impl IntoResponse {
|
||||
match ops::list(&store, q.prefix.as_deref()).await {
|
||||
Ok(keys) => Ok(axum::Json(keys)),
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user