use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use axum::Router;
use tracing::{info, warn};
use crate::{DevS3, DEV_S3_BUCKET};
pub const DEV_STATE_DIR: &str = ".mesofact-dev";
pub struct DevServer {
s3: DevS3,
state_dir: PathBuf,
}
impl DevServer {
pub async fn start(root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref();
let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let state_dir = root.join(DEV_STATE_DIR);
tokio::fs::create_dir_all(&state_dir)
.await
.with_context(|| format!("creating dev state dir {}", state_dir.display()))?;
let s3 = DevS3::start(state_dir.join("s3"), DEV_S3_BUCKET).await?;
info!(
endpoint = %s3.endpoint,
bucket = %s3.bucket,
"mesofact-dev: local object store ready (stands in for R2)",
);
let discovery = state_dir.join("s3.json");
if let Err(e) = std::fs::write(
&discovery,
serde_json::json!({ "endpoint": s3.endpoint, "bucket": s3.bucket }).to_string(),
) {
warn!(error = %e, path = %discovery.display(), "dev S3: could not write discovery file");
}
Ok(Self { s3, state_dir })
}
pub fn s3(&self) -> &DevS3 {
&self.s3
}
pub fn state_dir(&self) -> &Path {
&self.state_dir
}
pub fn export_env(&self) {
for (key, value) in self.s3.env_vars() {
if let Ok(prior) = std::env::var(&key) {
if prior != value {
warn!(%key, %prior, "mesofact-dev: overriding inherited value with the local store's");
}
}
std::env::set_var(&key, &value);
}
}
pub async fn serve(self, app: Router, addr: SocketAddr) -> Result<()> {
warn!(
addr = %addr,
s3_endpoint = %self.s3.endpoint,
"mesofact-dev: DEV BINARY — dev affordances are linked in; do not ship this target",
);
mesofact::serve_app(app, addr).await
}
}
pub async fn serve_app(app: Router, addr: SocketAddr) -> Result<()> {
let dev = DevServer::start(std::env::current_dir().context("reading current directory")?)
.await?;
dev.export_env();
dev.serve(app, addr).await
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{Request, StatusCode};
use tempfile::tempdir;
#[tokio::test]
async fn start_creates_state_dir_and_discovery_file() {
let root = tempdir().unwrap();
let dev = DevServer::start(root.path()).await.unwrap();
assert_eq!(dev.state_dir(), dev.state_dir().canonicalize().unwrap());
assert!(dev.state_dir().ends_with(DEV_STATE_DIR));
assert!(dev.state_dir().join("s3").join(DEV_S3_BUCKET).is_dir());
let discovery: serde_json::Value = serde_json::from_slice(
&std::fs::read(dev.state_dir().join("s3.json")).expect("discovery file written"),
)
.unwrap();
assert_eq!(discovery["endpoint"], dev.s3().endpoint);
assert_eq!(discovery["bucket"], DEV_S3_BUCKET);
}
#[cfg(feature = "ssr")]
#[tokio::test]
async fn handler_reads_r2_from_env_against_the_dev_surface() {
use axum::body::Body;
use axum::routing::get;
use mesofact_publisher::{ObjectStore, S3Store};
use tower::ServiceExt;
let root = tempdir().unwrap();
let dev = DevServer::start(root.path()).await.unwrap();
dev.export_env();
let put = reqwest::Client::new()
.put(format!("{}/{}/greeting.txt", dev.s3().endpoint, dev.s3().bucket))
.body("hello from the dev store")
.send()
.await
.unwrap();
assert!(put.status().is_success(), "seed PUT status: {}", put.status());
async fn greeting() -> String {
let store = S3Store::new(
std::env::var("R2_ENDPOINT").unwrap(),
std::env::var("R2_BUCKET").unwrap(),
"auto",
std::env::var("R2_ACCESS_KEY_ID").unwrap(),
std::env::var("R2_SECRET_ACCESS_KEY").unwrap(),
)
.unwrap();
let bytes = store.get("greeting.txt").await.unwrap().unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
let app = mesofact::wrap(Router::new().route("/greeting", get(greeting)));
let response = app
.clone()
.oneshot(Request::builder().uri("/greeting").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(
String::from_utf8(body.to_vec()).unwrap(),
"hello from the dev store"
);
let probe = app
.oneshot(
Request::builder()
.uri(mesofact::LIVE_PATH)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(probe.status(), StatusCode::OK);
}
}