mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change. Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
119 lines
3.6 KiB
Rust
119 lines
3.6 KiB
Rust
use std::time::Duration;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use tokio::task::JoinHandle;
|
|
use tracing::warn;
|
|
|
|
use super::config::S3BackupConfig;
|
|
use crate::{AppState, GatewayError};
|
|
|
|
pub(crate) const S3_BACKUP_WORKER_TASK_KEY: &str =
|
|
crate::task_runtime::TASK_KEY_SYSTEM_S3_BACKUP_WORKER;
|
|
const S3_BACKUP_WORKER_INTERVAL: Duration = Duration::from_secs(60);
|
|
|
|
pub(crate) fn should_start_scheduled_backup(last_slot: Option<&str>, current_slot: &str) -> bool {
|
|
last_slot != Some(current_slot)
|
|
}
|
|
|
|
pub(crate) fn spawn_s3_backup_worker(app: AppState) -> Option<JoinHandle<()>> {
|
|
if !app.data.has_system_config_store() {
|
|
return None;
|
|
}
|
|
|
|
Some(crate::task_runtime::spawn_singleton_worker(
|
|
app,
|
|
S3_BACKUP_WORKER_TASK_KEY,
|
|
|app| async move {
|
|
let mut interval = tokio::time::interval(S3_BACKUP_WORKER_INTERVAL);
|
|
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
|
interval.tick().await;
|
|
loop {
|
|
interval.tick().await;
|
|
if run_s3_backup_schedule_tick(&app, Utc::now()).await.is_err() {
|
|
warn!(
|
|
error_category = "schedule_tick_failed",
|
|
"S3 backup schedule tick failed"
|
|
);
|
|
}
|
|
}
|
|
},
|
|
))
|
|
}
|
|
|
|
async fn run_s3_backup_schedule_tick(
|
|
app: &AppState,
|
|
now: DateTime<Utc>,
|
|
) -> Result<(), GatewayError> {
|
|
let values = match super::task::load_s3_backup_config_values(app).await {
|
|
Ok(values) => values,
|
|
Err(_) => {
|
|
warn!(
|
|
error_category = "config_load_failed",
|
|
"S3 backup schedule config load failed"
|
|
);
|
|
return Ok(());
|
|
}
|
|
};
|
|
let config = match S3BackupConfig::from_json_map(&values) {
|
|
Ok(config) => config,
|
|
Err(_) => {
|
|
warn!(
|
|
error_category = "config_invalid",
|
|
"S3 backup schedule config is invalid"
|
|
);
|
|
return Ok(());
|
|
}
|
|
};
|
|
if !config.enabled {
|
|
return Ok(());
|
|
}
|
|
let Some(slot) = config.schedule.due_slot(now) else {
|
|
return Ok(());
|
|
};
|
|
let last_slot = read_last_backup_slot(app).await?;
|
|
if !should_start_scheduled_backup(last_slot.as_deref(), &slot) {
|
|
return Ok(());
|
|
}
|
|
|
|
match super::task::start_s3_backup_task_for_schedule(app.clone(), slot).await {
|
|
Ok(_) => {}
|
|
Err(_) => {
|
|
warn!(
|
|
error_category = "task_submission_failed",
|
|
"S3 backup scheduled task submission failed"
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn read_last_backup_slot(app: &AppState) -> Result<Option<String>, GatewayError> {
|
|
Ok(app
|
|
.read_system_config_json_value(super::S3_BACKUP_LAST_SLOT_KEY)
|
|
.await?
|
|
.and_then(|value| value.as_str().map(str::trim).map(str::to_string))
|
|
.filter(|value| !value.is_empty()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::task_runtime::{task_definition, TASK_KEY_SYSTEM_S3_BACKUP};
|
|
|
|
#[test]
|
|
fn backup_worker_skips_already_recorded_slot() {
|
|
let slot = "days:2026-05-23T19:00:00Z";
|
|
|
|
assert!(super::should_start_scheduled_backup(
|
|
Some("days:2026-05-22T19:00:00Z"),
|
|
slot
|
|
));
|
|
assert!(!super::should_start_scheduled_backup(Some(slot), slot));
|
|
}
|
|
|
|
#[test]
|
|
fn backup_worker_has_distinct_supervisor_task_key() {
|
|
assert_ne!(super::S3_BACKUP_WORKER_TASK_KEY, TASK_KEY_SYSTEM_S3_BACKUP);
|
|
assert!(task_definition(super::S3_BACKUP_WORKER_TASK_KEY).is_some());
|
|
}
|
|
}
|