Skip to content

Commit

Permalink
[server] Fix studio with base path #1497 (#1498)
Browse files Browse the repository at this point in the history
* add base path handling

* add test for studio behind base path
  • Loading branch information
michaelvlach authored Jan 16, 2025
1 parent 82541f5 commit 766347e
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 38 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/agdb_api_php.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ jobs:
- run: composer config --global use-parent-dir true
- run: composer install
- run: ./ci.sh analyse
- run: npm ci && npm run build
working-directory: agdb_api/typescript
- run: npm ci && npm run build
working-directory: agdb_studio
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: ./ci.sh coverage
- uses: actions/upload-artifact@v4
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/agdb_api_typescript.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ jobs:
- run: npm ci
- run: npm run format:check
- run: npm run lint:check
- run: npm run build
- run: npm ci && npm run build
working-directory: agdb_studio
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: npm run test
- uses: actions/upload-artifact@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/agdb_server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- run: cargo clippy -p agdb_server -p agdb_api --all-targets --all-features -- -D warnings
- uses: taiki-e/install-action@cargo-llvm-cov
- run: rustup component add llvm-tools-preview
- run: cargo llvm-cov -p agdb_server -p agdb_api --all-features --ignore-filename-regex "agdb(.|..)src|agdb_derive" --fail-uncovered-functions 41 --fail-uncovered-lines 200 --show-missing-lines
- run: cargo llvm-cov -p agdb_server -p agdb_api --all-features --ignore-filename-regex "agdb(.|..)src|agdb_derive" --fail-uncovered-functions 42 --fail-uncovered-lines 231 --show-missing-lines

agdb_server_image:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions agdb_api/typescript/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type AgdbApiClient = Client & AgdbApi;

export async function client(address: string): Promise<AgdbApiClient> {
const api: OpenAPIClientAxios = new OpenAPIClientAxios({
withServer: { url: address },
definition: `${address}/api/v1/openapi.json`,
});
const client = await api.init<AgdbApiClient>();
Expand Down
3 changes: 1 addition & 2 deletions agdb_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ tower = "0.5"
tower-http = { version = "0.6", features = ["cors"] }
tracing = "0.1"
tracing-subscriber = "0.3"
url = { version = "2", features = ["serde"] }
utoipa = "5"
utoipa-rapidoc = { version = "5", features = ["axum"] }
utoipa-rapidoc = { version = "6", features = ["axum"] }
uuid = { version = "1", features = ["v4"] }

[dev-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions agdb_server/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::db_pool::DbPool;
use crate::forward;
use crate::logger;
use crate::routes;
use crate::routes::studio;
use crate::server_db::ServerDb;
use crate::server_state::ServerState;
use axum::middleware;
Expand All @@ -24,6 +25,8 @@ pub(crate) fn app(
server_db: ServerDb,
shutdown_sender: Sender<()>,
) -> Router {
studio::init(&config);

let basepath = config.basepath.clone();

let state = ServerState {
Expand Down
5 changes: 3 additions & 2 deletions agdb_server/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,16 @@ impl ClusterNodeImpl {
};

let (requests_sender, requests_receiver) = tokio::sync::mpsc::unbounded_channel();
let base_url = base.trim_end_matches("/").to_string();

Ok(Self {
client: ReqwestClient::with_client(
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(60))
.build()?,
),
url: format!("{base}api/v1/cluster"),
base_url: base.trim_end_matches("/").to_string(),
url: format!("{base_url}/api/v1/cluster"),
base_url,
token: Some(token.to_string()),
requests_sender,
requests_receiver: RwLock::new(requests_receiver),
Expand Down
19 changes: 9 additions & 10 deletions agdb_server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tracing::level_filters::LevelFilter;
use url::Url;

pub(crate) type Config = Arc<ConfigImpl>;

Expand All @@ -17,7 +16,7 @@ pub(crate) struct LogLevel(pub(crate) LevelFilter);
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct ConfigImpl {
pub(crate) bind: String,
pub(crate) address: Url,
pub(crate) address: String,
pub(crate) basepath: String,
pub(crate) admin: String,
pub(crate) log_level: LogLevel,
Expand All @@ -26,7 +25,7 @@ pub(crate) struct ConfigImpl {
pub(crate) cluster_token: String,
pub(crate) cluster_heartbeat_timeout_ms: u64,
pub(crate) cluster_term_timeout_ms: u64,
pub(crate) cluster: Vec<Url>,
pub(crate) cluster: Vec<String>,
#[serde(skip)]
pub(crate) cluster_node_id: usize,
#[serde(skip)]
Expand Down Expand Up @@ -77,7 +76,7 @@ pub(crate) fn new(config_file: &str) -> ServerResult<Config> {

let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000")?,
address: "http://localhost:3000".to_string(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
Expand Down Expand Up @@ -167,7 +166,7 @@ mod tests {
let test_file = TestFile::new("test_config_invalid_cluster.yaml");
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
address: "http://localhost:3000".to_string(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
Expand All @@ -176,15 +175,15 @@ mod tests {
cluster_token: "cluster".to_string(),
cluster_heartbeat_timeout_ms: 1000,
cluster_term_timeout_ms: 3000,
cluster: vec![Url::parse("localhost:3001").unwrap()],
cluster: vec!["http://localhost:3001".to_string()],
cluster_node_id: 0,
start_time: 0,
pepper: None,
};
std::fs::write(test_file.filename, serde_yml::to_string(&config).unwrap()).unwrap();
assert_eq!(
config::new(test_file.filename).unwrap_err().description,
"cluster does not contain local node: localhost:3000"
"cluster does not contain local node: http://localhost:3000"
);
}

Expand All @@ -196,7 +195,7 @@ mod tests {
std::fs::write(pepper_file.filename, pepper).unwrap();
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
address: "http://localhost:3000".to_string(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
Expand All @@ -223,7 +222,7 @@ mod tests {
let test_file = TestFile::new("pepper_missing.yaml");
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
address: "http://localhost:3000".to_string(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
Expand All @@ -249,7 +248,7 @@ mod tests {
std::fs::write(pepper_file.filename, b"0123456789").unwrap();
let config = ConfigImpl {
bind: ":::3000".to_string(),
address: Url::parse("localhost:3000").unwrap(),
address: "http://localhost:3000".to_string(),
basepath: "".to_string(),
admin: "admin".to_string(),
log_level: LogLevel(LevelFilter::INFO),
Expand Down
2 changes: 1 addition & 1 deletion agdb_server/src/routes/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pub(crate) async fn status(
for (index, node) in config.cluster.iter().enumerate() {
if index != cluster.index {
let address = node.as_str().to_string();
let url = format!("{}api/v1/status", node.as_str());
let url = format!("{}/api/v1/status", node.trim_end_matches("/"));

tasks.push(tokio::spawn(async move {
let client = reqwest::Client::new();
Expand Down
107 changes: 92 additions & 15 deletions agdb_server/src/routes/studio.rs
Original file line number Diff line number Diff line change
@@ -1,44 +1,121 @@
use crate::config::Config;
use crate::routes::ServerResult;
use crate::server_error::ServerError;
use axum::extract::Path;
use axum::response::IntoResponse;
use axum_extra::headers::ContentType;
use include_dir::include_dir;
use include_dir::Dir;
use reqwest::StatusCode;
use std::sync::OnceLock;

static AGDB_STUDIO: Dir = include_dir!("agdb_studio/dist");
static AGDB_STUDIO_INDEX_JS: OnceLock<String> = OnceLock::new();
static AGDB_STUDIO_INDEX_HTML: OnceLock<String> = OnceLock::new();

pub(crate) fn init(config: &Config) {
AGDB_STUDIO_INDEX_JS.get_or_init(|| {
let index = AGDB_STUDIO
.get_dir("assets")
.expect("assets dir not found")
.files()
.find(|f| {
f.path().extension().unwrap_or_default() == "js"
&& f.path()
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.starts_with("index")
})
.expect("index.js not found")
.contents_utf8()
.expect("Failed to read index.js");

let f = index.replace("\"/studio", &format!("\"{}/studio", config.basepath));

if !config.basepath.is_empty() {
f.replace(
"http://localhost:3000",
&format!(
"{}{}",
config.address.trim_end_matches("/"),
&config.basepath
),
)
} else {
f
}
});

AGDB_STUDIO_INDEX_HTML.get_or_init(|| {
AGDB_STUDIO
.get_file("index.html")
.expect("index.html not found")
.contents_utf8()
.expect("Failed to read index.html")
.replace("\"/studio/", &format!("\"{}/studio/", config.basepath))
});
}

async fn studio_index() -> ServerResult<(
reqwest::StatusCode,
[(&'static str, &'static str); 1],
&'static [u8],
)> {
let content = AGDB_STUDIO_INDEX_HTML.get().ok_or(ServerError::new(
StatusCode::INTERNAL_SERVER_ERROR,
"index.html not found",
))?;

Ok((
StatusCode::OK,
[("Content-Type", "text/html")],
content.as_str().as_bytes(),
))
}

pub(crate) async fn studio_root() -> ServerResult<impl IntoResponse> {
studio(Path("index.html".to_string())).await
studio_index().await
}

pub(crate) async fn studio(Path(file): Path<String>) -> ServerResult<impl IntoResponse> {
if file.ends_with("index.html") {
return studio_index().await;
}

if file.ends_with(".js") && file.contains("index") {
let content = AGDB_STUDIO_INDEX_JS.get().ok_or(ServerError::new(
StatusCode::INTERNAL_SERVER_ERROR,
"index.js not found",
))?;

return Ok((
StatusCode::OK,
[("Content-Type", "application/javascript")],
content.as_str().as_bytes(),
));
}

let f = if let Some(f) = AGDB_STUDIO.get_file(&file) {
f
} else {
AGDB_STUDIO.get_file("index.html").ok_or(ServerError::new(
StatusCode::NOT_FOUND,
"index.html not found",
))?
return studio_index().await;
};

let content_type = if file.ends_with(".js") {
"application/javascript".to_string()
"application/javascript"
} else if file.ends_with(".css") {
"text/css".to_string()
"text/css"
} else if file.ends_with(".svg") {
"image/svg+xml".to_string()
"image/svg+xml"
} else if file.ends_with(".ico") {
"image/x-icon"
} else {
ContentType::html().to_string()
"text/html"
};

Ok((
StatusCode::CREATED,
StatusCode::OK,
[("Content-Type", content_type)],
f.contents_utf8().ok_or(ServerError::new(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("Failed to read content of '{file}'"),
))?,
f.contents(),
))
}
12 changes: 7 additions & 5 deletions agdb_server/tests/routes/misc_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,13 @@ async fn basepath_test() -> anyhow::Result<()> {
config.insert("cluster_term_timeout_ms", 3000.into());
config.insert("cluster", Vec::<String>::new().into());

let _server = TestServerImpl::with_config(config).await?;
let server = TestServerImpl::with_config(config).await?;

// If the base path does not work the server
// will not ever be considered ready, see
// TestServer implementation for details.
reqwest::Client::new()
.get(format!("{}/studio", server.address))
.send()
.await?
.error_for_status()?;

Ok(())
}
Expand Down Expand Up @@ -324,7 +326,7 @@ async fn studio() -> anyhow::Result<()> {
let server = TestServer::new().await?;
let client = reqwest::Client::new();
client
.get(format!("http://{}", server.url("/studio")))
.get(server.url("/studio"))
.send()
.await?
.error_for_status()?;
Expand Down
4 changes: 2 additions & 2 deletions agdb_server/tests/test_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ impl TestServerImpl {
.to_string()
} else {
let port = Self::next_port();
let address = format!("{HOST}:{port}");
config.insert("bind", address.to_owned().into());
let address = format!("http://{HOST}:{port}");
config.insert("bind", format!("{HOST}:{port}").into());
config.insert("address", address.to_owned().into());
address
};
Expand Down

0 comments on commit 766347e

Please sign in to comment.