use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::client::GatewayApi;
use crate::client::backup::BackupType;
use crate::error::CoreError;
#[derive(Debug, Serialize)]
pub struct BackupDownloadResult {
pub file: String,
pub r#type: String,
}
#[derive(Debug, Serialize)]
pub struct BackupRestoreResult {
pub restored: bool,
}
fn safe_stem(stem: &str) -> String {
stem.replace(['/', '\\'], "_")
}
fn sanitize_basename(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
Some(trimmed.replace(['/', '\\'], "_"))
}
pub async fn backup_download(
api: &dyn GatewayApi,
out: Option<&Path>,
host_stem: &str,
backup_type: BackupType,
) -> Result<BackupDownloadResult, CoreError> {
let wire_type = backup_type.wire().to_string();
if let Some(out) = out {
api.backup_download(out, backup_type).await?;
return Ok(BackupDownloadResult {
file: out.display().to_string(),
r#type: wire_type,
});
}
let fallback = format!("{}-backup.gwbk", safe_stem(host_stem));
let part = PathBuf::from(format!("{fallback}.part"));
let meta = match api.backup_download(&part, backup_type).await {
Ok(meta) => meta,
Err(err) => {
let _ = std::fs::remove_file(&part); return Err(err);
}
};
let final_name = meta
.filename
.as_deref()
.and_then(sanitize_basename)
.unwrap_or(fallback);
if let Err(err) = std::fs::rename(&part, &final_name) {
let _ = std::fs::remove_file(&part); return Err(CoreError::Internal(format!(
"cannot finalize backup {final_name}: {err}"
)));
}
Ok(BackupDownloadResult {
file: final_name,
r#type: wire_type,
})
}
pub async fn backup_restore(
api: &dyn GatewayApi,
gwbk: &Path,
) -> Result<BackupRestoreResult, CoreError> {
let meta = std::fs::metadata(gwbk).map_err(|_| CoreError::InvalidInput {
reason: format!("gwbk file {} not found", gwbk.display()),
})?;
if !meta.is_file() {
return Err(CoreError::InvalidInput {
reason: format!("gwbk file {} is not a regular file", gwbk.display()),
});
}
if meta.len() == 0 {
return Err(CoreError::InvalidInput {
reason: format!("gwbk file {} is empty", gwbk.display()),
});
}
api.backup_restore(gwbk).await?;
Ok(BackupRestoreResult { restored: true })
}
#[cfg(test)]
mod tests {
use super::{safe_stem, sanitize_basename};
#[test]
fn basename_sanitizer_strips_separators() {
assert_eq!(
sanitize_basename("backup.gwbk").as_deref(),
Some("backup.gwbk")
);
assert_eq!(
sanitize_basename("../../etc/passwd").as_deref(),
Some(".._.._etc_passwd")
);
assert_eq!(sanitize_basename("a\\b.gwbk").as_deref(), Some("a_b.gwbk"));
assert_eq!(sanitize_basename(" "), None, "blank names nothing");
}
#[test]
fn fallback_stem_sanitizes() {
assert_eq!(safe_stem("gw/dev"), "gw_dev");
assert_eq!(safe_stem("plain"), "plain");
}
}