Files
kakeibo/backend/src/routes/backup.rs
T

220 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! JSON tam hane yedeği (`services::backup`).
//!
//! | Yöntem | Yol | Kim | İş |
//! |---|---|---|---|
//! | GET | `/api/backup/download` | üye | yedek dosyası (indirme) |
//! | GET | `/api/backup/status` | üye | son yedek zamanı + geri dönüş noktaları (yalnız sahip görür) |
//! | POST | `/api/backup/preview` | sahip | gövde: yedek dosyası → ne değişecek (hiçbir şey yazılmaz) |
//! | POST | `/api/backup/restore` | sahip | gövde: yedek dosyası → hane verisi değiştirilir |
//! | POST | `/api/backup/restore-points/{id}/restore` | sahip | geri dönüş noktasına dön ("geri al") |
use axum::{
Json,
body::Bytes,
extract::{Path, State},
http::header,
response::IntoResponse,
};
use mongodb::bson::DateTime;
use serde::Serialize;
use ts_rs::TS;
use crate::{
auth::AuthUser,
error::{AppError, AppResult},
models::user::Role,
services::{
backup::{self, Reason},
calendar::{format_date, today_tr},
},
state::AppState,
};
fn iso(d: DateTime) -> String {
d.try_to_rfc3339_string().unwrap_or_default()
}
fn parse_id(id: &str) -> AppResult<mongodb::bson::Uuid> {
mongodb::bson::Uuid::parse_str(id).map_err(|_| AppError::NotFound)
}
pub async fn download(State(s): State<AppState>, auth: AuthUser) -> AppResult<impl IntoResponse> {
let snap = backup::collect(&s.db, auth.household_id).await?;
let body = backup::to_json(&snap)?;
backup::mark_exported(&s.db, auth.household_id).await?;
let file = format!(
"kakeibo-yedek-{}-{}.json",
backup::slug(&snap.household_name),
format_date(today_tr())
);
Ok((
[
(
header::CONTENT_TYPE,
"application/json; charset=utf-8".to_string(),
),
(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{file}\""),
),
(header::CACHE_CONTROL, "no-store".to_string()),
],
body,
))
}
#[derive(Serialize, TS)]
#[ts(export)]
pub struct RestorePoint {
id: String,
/// RFC 3339
created_at: String,
/// `before_restore`: dosyadan geri yüklemeden önce · `before_undo`: geri almadan önce
#[ts(type = "\"before_restore\" | \"before_undo\"")]
reason: String,
#[ts(type = "number")]
periods: i64,
#[ts(type = "number")]
entries: i64,
/// Sıkıştırılmış boyut (bayt)
#[ts(type = "number")]
size: i64,
}
#[derive(Serialize, TS)]
#[ts(export)]
pub struct BackupStatus {
/// Son dışa aktarma (RFC 3339); hiç alınmadıysa `null`
last_backup_at: Option<String>,
/// En yeniden eskiye; sahip olmayana boş
restore_points: Vec<RestorePoint>,
keep_points: u32,
keep_days: u32,
}
pub async fn status(State(s): State<AppState>, auth: AuthUser) -> AppResult<Json<BackupStatus>> {
let points = if auth.role == Role::Owner {
backup::points(&s.db, auth.household_id).await?
} else {
Vec::new()
};
Ok(Json(BackupStatus {
last_backup_at: backup::last_exported(&s.db, auth.household_id)
.await?
.map(iso),
restore_points: points
.into_iter()
.map(|p| RestorePoint {
id: p.id.to_string(),
created_at: iso(p.created_at),
reason: p.reason,
periods: p.periods,
entries: p.entries,
size: p.size,
})
.collect(),
keep_points: backup::KEEP_POINTS as u32,
keep_days: backup::KEEP_DAYS as u32,
}))
}
#[derive(Serialize, TS)]
#[ts(export)]
pub struct BackupCount {
/// Koleksiyon adı (`entries`, `periods`, …)
kind: String,
/// Türkçe ad
label: String,
#[ts(type = "number")]
backup: i64,
#[ts(type = "number")]
current: i64,
}
#[derive(Serialize, TS)]
#[ts(export)]
pub struct BackupPreview {
source_name: String,
same_household: bool,
/// RFC 3339
exported_at: String,
app_version: String,
counts: Vec<BackupCount>,
/// Yedekteki dönemlerin ilk ve son günü
first_date: Option<String>,
last_date: Option<String>,
/// Aynı hane: yedekten sonra eklenen/değişen (kaybolacak) kayıt
#[ts(type = "number")]
changed_since: i64,
warnings: Vec<String>,
}
pub async fn preview(
State(s): State<AppState>,
auth: AuthUser,
body: Bytes,
) -> AppResult<Json<BackupPreview>> {
auth.require_owner()?;
let snap = backup::parse(&body)?;
let p = backup::preview(&s.db, auth.household_id, &snap).await?;
Ok(Json(BackupPreview {
source_name: p.source_name,
same_household: p.same_household,
exported_at: iso(p.exported_at),
app_version: p.app_version,
counts: p
.counts
.into_iter()
.map(|c| BackupCount {
kind: c.kind.into(),
label: c.label.into(),
backup: c.backup,
current: c.current,
})
.collect(),
first_date: p.first_date,
last_date: p.last_date,
changed_since: p.changed_since,
warnings: p.warnings,
}))
}
#[derive(Serialize, TS)]
#[ts(export)]
pub struct RestoreResult {
/// İşlemden önceki durumun geri dönüş noktası ("Geri al" bununla)
restore_point_id: String,
}
pub async fn restore(
State(s): State<AppState>,
auth: AuthUser,
body: Bytes,
) -> AppResult<Json<RestoreResult>> {
auth.require_owner()?;
let snap = backup::parse(&body)?;
let id = backup::restore(
&s.db,
auth.household_id,
auth.user_id,
&snap,
Reason::BeforeRestore,
)
.await?;
Ok(Json(RestoreResult {
restore_point_id: id.to_string(),
}))
}
pub async fn undo(
State(s): State<AppState>,
auth: AuthUser,
Path(id): Path<String>,
) -> AppResult<Json<RestoreResult>> {
auth.require_owner()?;
let id = backup::undo(&s.db, auth.household_id, auth.user_id, parse_id(&id)?).await?;
Ok(Json(RestoreResult {
restore_point_id: id.to_string(),
}))
}