use chrono::{DateTime, Utc};
use serde::Serialize;
use vti_common::audit::{AuditEvent, WebsiteGenerationRolledBackData};
use crate::error::{AppError, TaskError};
use crate::server::AppState;
use crate::website::storage::{GenerationEntry, list_managed_generations, swap_current_symlink};
use trust_tasks_rs::specs::vtc::website as website_spec;
pub const GENERATIONS_LIST_ERR_NOT_MANAGED: &str =
website_spec::generations::list::v0_1::error_codes::NOT_MANAGED.code;
pub const ROLLBACK_ERR_NOT_MANAGED: &str =
website_spec::rollback::v0_1::error_codes::NOT_MANAGED.code;
pub const ROLLBACK_ERR_GENERATION_NOT_FOUND: &str =
website_spec::rollback::v0_1::error_codes::GENERATION_NOT_FOUND.code;
pub(crate) async fn list(state: &AppState) -> Result<GenerationsResponse, TaskError> {
let cfg = state.config.read().await;
let root_dir = cfg
.website
.root_dir
.clone()
.ok_or_else(|| AppError::Validation("website.root_dir is not configured".into()))?;
let deploy_mode = cfg.website.deploy_mode.clone();
drop(cfg);
if deploy_mode != "managed" {
return Err(TaskError::declared(
GENERATIONS_LIST_ERR_NOT_MANAGED,
AppError::Validation(
"website/generations/list is only available in managed deploy mode".into(),
),
));
}
let generations = list_managed_generations(&root_dir)?
.into_iter()
.map(GenerationRow::from)
.collect();
Ok(GenerationsResponse { generations })
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct GenerationsResponse {
pub generations: Vec<GenerationRow>,
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GenerationRow {
pub generation: String,
pub current: bool,
pub deployed_at: DateTime<Utc>,
pub size_bytes: u64,
}
impl From<GenerationEntry> for GenerationRow {
fn from(e: GenerationEntry) -> Self {
Self {
generation: e.generation.to_string(),
current: e.is_current,
deployed_at: DateTime::from_timestamp(e.deployed_at as i64, 0)
.unwrap_or(DateTime::UNIX_EPOCH),
size_bytes: e.size_bytes,
}
}
}
pub(crate) async fn rollback(
state: &AppState,
actor: &str,
gen_num: u32,
) -> Result<RollbackResponse, TaskError> {
let cfg = state.config.read().await;
let root_dir = cfg
.website
.root_dir
.clone()
.ok_or_else(|| AppError::Validation("website.root_dir is not configured".into()))?;
let deploy_mode = cfg.website.deploy_mode.clone();
drop(cfg);
if deploy_mode != "managed" {
return Err(TaskError::declared(
ROLLBACK_ERR_NOT_MANAGED,
AppError::Validation(
"website/rollback is only available in managed deploy mode".into(),
),
));
}
let from = swap_current_symlink(&root_dir, gen_num).map_err(|e| match e {
e @ AppError::NotFound(_) => TaskError::declared(ROLLBACK_ERR_GENERATION_NOT_FOUND, e),
e => TaskError::App(e),
})?;
if from != gen_num
&& let Some(writer) = state.audit_writer.as_ref()
{
let _ = writer
.write(
actor,
None,
AuditEvent::WebsiteGenerationRolledBack(WebsiteGenerationRolledBackData {
from_generation: from,
to_generation: gen_num,
}),
)
.await;
}
Ok(RollbackResponse {
generation: gen_num.to_string(),
current: true,
noop: from == gen_num,
})
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
#[schema(as = WebsiteRollbackResponse)]
pub struct RollbackResponse {
pub generation: String,
pub current: bool,
pub noop: bool,
}