0d70576df7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
6.1 KiB
Rust
220 lines
6.1 KiB
Rust
//! 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(),
|
||
}))
|
||
}
|