use axum::Json;
use axum::Router;
use axum::extract::Path;
use axum::extract::Query;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::get;
use semver::Version;
use serde::Deserialize;
use serde::Serialize;
use crate::client::UpdatePlan;
use crate::store::ReleaseStore;
use crate::store::StoreError;
#[derive(Debug, Clone)]
pub struct CodePushState {
store: ReleaseStore,
pub download_base: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CheckQuery {
pub version: String,
pub build: u64,
}
#[derive(Debug, Serialize)]
pub struct DeltaOffer {
pub from_version: String,
pub base_hash: String,
}
pub fn codepush_router(store: ReleaseStore) -> Router {
Router::new()
.route("/{app}/{channel}/check", get(check))
.route("/{app}/{channel}/download/{version}/{build}", get(download))
.route(
"/{app}/{channel}/delta/{from_version}/{from_build}",
get(delta),
)
.with_state(CodePushState {
store,
download_base: None,
})
}
async fn check(
State(state): State<CodePushState>,
Path((app, channel)): Path<(String, String)>,
Query(query): Query<CheckQuery>,
) -> Result<Json<UpdatePlan>, ApiError> {
let current = Version::parse(&query.version)
.map_err(|error| ApiError::bad_request(format!("invalid version: {error}")))?;
let plan = state
.store
.plan_update(&app, &channel, ¤t, query.build)?;
Ok(Json(plan))
}
async fn download(
State(state): State<CodePushState>,
Path((app, channel, version, build)): Path<(String, String, String, u64)>,
) -> Result<Vec<u8>, ApiError> {
let version = Version::parse(&version)
.map_err(|error| ApiError::bad_request(format!("invalid version: {error}")))?;
let record = state
.store
.find(&app, &channel, &version, build)?
.ok_or_else(|| ApiError::not_found("no such release"))?;
Ok(record.container)
}
async fn delta(
State(state): State<CodePushState>,
Path((app, channel, from_version, from_build)): Path<(String, String, String, u64)>,
) -> Result<Vec<u8>, ApiError> {
let from_version = Version::parse(&from_version)
.map_err(|error| ApiError::bad_request(format!("invalid version: {error}")))?;
let latest = state
.store
.latest(&app, &channel)?
.ok_or_else(|| ApiError::not_found("no releases on channel"))?;
let delta = latest.delta_from_previous.filter(|delta| {
state
.store
.find(&app, &channel, &from_version, from_build)
.ok()
.flatten()
.is_some_and(|base| {
base.manifest.bundle_hash() == crate::manifest::hash_bytes(&base.container)
&& delta.base_hash == crate::manifest::hash_bytes(&base.container)
})
});
match delta {
Some(delta) => Ok(serde_json::to_vec(&delta)?),
None => Err(ApiError::not_found("no delta for that base")),
}
}
#[derive(Debug)]
pub struct ApiError {
status: StatusCode,
message: String,
}
impl ApiError {
fn bad_request(message: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: message.into(),
}
}
fn not_found(message: impl Into<String>) -> Self {
Self {
status: StatusCode::NOT_FOUND,
message: message.into(),
}
}
}
impl From<StoreError> for ApiError {
fn from(error: StoreError) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: error.to_string(),
}
}
}
impl From<serde_json::Error> for ApiError {
fn from(error: serde_json::Error) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: error.to_string(),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
(self.status, self.message).into_response()
}
}