//! The backups API (oxygenhub#179, docs/backups.md ยง5): what the Backups //! pages do, for a user's own repositories and an organisation's (its //! owners only), with the scope `repo:admin`. use super::{caller, not_found, ApiError, Caller}; use crate::auth; use crate::backups::{self, Run, Setting}; use crate::db::{self, accounts::Account}; use crate::web::backups::{setting_of, SettingInput}; use crate::Shared; use axum::extract::{Path, State}; use axum::http::{HeaderMap, StatusCode}; use axum::Json; use serde::{Deserialize, Serialize}; /// A setting as the API takes it: the page's fields, `keep_days` a number. #[derive(Deserialize)] pub struct Body { pub every: String, #[serde(default)] pub destination: String, /// A directory destination: one the hub allows. #[serde(default)] pub dir: String, /// A GCS destination: its bucket and workload identity provider. #[serde(default)] pub bucket: String, #[serde(default)] pub provider: String, #[serde(default)] pub prefix: String, #[serde(default = "thirty")] pub keep_days: i64, /// An S3 destination: its bucket, region and the IAM role's ARN. #[serde(default)] pub s3_bucket: String, #[serde(default)] pub s3_region: String, #[serde(default)] pub s3_role: String, } fn thirty() -> i64 { 30 } #[derive(Serialize)] pub struct State_ { pub setting: Option, /// Where its runs go, as the pages say it. pub place: Option, pub runs: Vec, } /// The owner a call acts for: the caller's own account, or an organisation /// it owns. Someone else's is not found. async fn owner_of(app: &Shared, c: &Caller, org: Option<&str>) -> Result<(Account, String, &'static str), ApiError> { let me = c.require("repo:admin")?.clone(); match org { None => { let name = me.username.clone(); Ok((me, name, "user")) } Some(name) => { let o = db::accounts::org_by_name(&app.db, name).await?.ok_or_else(|| not_found("no such organisation"))?; if !auth::roles::is_org_owner(&app.db, &me, o.id).await? { return Err(not_found("no such organisation")); } Ok((me, o.name, "org")) } } } async fn state(app: &Shared, owner: &str) -> Result { let setting = backups::setting(&app.db, owner).await?; let place = setting.as_ref().map(backups::place); Ok(State_ { setting, place, runs: backups::runs(&app.db, owner, 10).await? }) } async fn get(app: Shared, headers: HeaderMap, org: Option) -> Result, ApiError> { let c = caller(&app, &headers).await?; let (_, owner, _) = owner_of(&app, &c, org.as_deref()).await?; Ok(Json(state(&app, &owner).await?)) } async fn put(app: Shared, headers: HeaderMap, org: Option, b: Body) -> Result, ApiError> { let c = caller(&app, &headers).await?; let (me, owner, kind) = owner_of(&app, &c, org.as_deref()).await?; let input = SettingInput { every: b.every, destination: b.destination, dir: b.dir, bucket: b.bucket, provider: b.provider, prefix: b.prefix, keep_days: b.keep_days.to_string(), s3_bucket: b.s3_bucket, s3_region: b.s3_region, s3_role: b.s3_role, }; let s = setting_of(&app, &owner, kind, &input).map_err(|e| ApiError(StatusCode::BAD_REQUEST, e))?; backups::save(&app.db, &s).await?; db::accounts::audit(&app.db, Some(&me), "backup.setting", &owner, &format!("{} to {}", s.every, backups::place(&s))).await.ok(); Ok(Json(state(&app, &owner).await?)) } async fn test(app: Shared, headers: HeaderMap, org: Option) -> Result, ApiError> { let c = caller(&app, &headers).await?; let (_, owner, _) = owner_of(&app, &c, org.as_deref()).await?; let set = backups::setting(&app.db, &owner).await?.ok_or_else(|| ApiError(StatusCode::CONFLICT, "save where backups go first".into()))?; let dest = backups::destination(&app, &set).map_err(|e| ApiError(StatusCode::CONFLICT, format!("{e:#}")))?; tokio::task::spawn_blocking(move || backups::probe(dest.as_ref())) .await .map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .map_err(|e| ApiError(StatusCode::CONFLICT, format!("TEST failed: {e:#}")))?; Ok(Json(serde_json::json!({ "ok": true, "place": backups::place(&set) }))) } async fn run(app: Shared, headers: HeaderMap, org: Option) -> Result<(StatusCode, Json), ApiError> { let c = caller(&app, &headers).await?; let (_, owner, _) = owner_of(&app, &c, org.as_deref()).await?; if backups::setting(&app.db, &owner).await?.is_none() { return Err(ApiError(StatusCode::CONFLICT, "save where backups go first".into())); } let (app2, owner2) = (app.clone(), owner.clone()); tokio::spawn(async move { if let Err(e) = backups::run(&app2, &owner2, "now").await { tracing::warn!("backup of {owner2}: {e:#}"); } }); Ok((StatusCode::ACCEPTED, Json(serde_json::json!({ "owner": owner, "started": true })))) } async fn runs(app: Shared, headers: HeaderMap, org: Option) -> Result>, ApiError> { let c = caller(&app, &headers).await?; let (_, owner, _) = owner_of(&app, &c, org.as_deref()).await?; Ok(Json(backups::runs(&app.db, &owner, 50).await?)) } pub async fn user_get(State(app): State, headers: HeaderMap) -> Result, ApiError> { get(app, headers, None).await } pub async fn user_put(State(app): State, headers: HeaderMap, Json(b): Json) -> Result, ApiError> { put(app, headers, None, b).await } pub async fn user_test(State(app): State, headers: HeaderMap) -> Result, ApiError> { test(app, headers, None).await } pub async fn user_run(State(app): State, headers: HeaderMap) -> Result<(StatusCode, Json), ApiError> { run(app, headers, None).await } pub async fn user_runs(State(app): State, headers: HeaderMap) -> Result>, ApiError> { runs(app, headers, None).await } pub async fn org_get(State(app): State, headers: HeaderMap, Path(name): Path) -> Result, ApiError> { get(app, headers, Some(name)).await } pub async fn org_put( State(app): State, headers: HeaderMap, Path(name): Path, Json(b): Json, ) -> Result, ApiError> { put(app, headers, Some(name), b).await } pub async fn org_test( State(app): State, headers: HeaderMap, Path(name): Path, ) -> Result, ApiError> { test(app, headers, Some(name)).await } pub async fn org_run( State(app): State, headers: HeaderMap, Path(name): Path, ) -> Result<(StatusCode, Json), ApiError> { run(app, headers, Some(name)).await } pub async fn org_runs(State(app): State, headers: HeaderMap, Path(name): Path) -> Result>, ApiError> { runs(app, headers, Some(name)).await }