use std::{ fs::{create_dir_all, File}, io::{Read, Write}, path::PathBuf, time::{SystemTime, UNIX_EPOCH}, }; use serde::{Deserialize, Serialize}; use tracing::info; use uuid::Uuid; use crate::{ config::Config, error::{Error, Result}, packages::{Manager, PackageList}, pathinfo::PathInfo, }; pub type Id = String; #[derive(Debug, Serialize, Deserialize)] pub struct Backup { pub id: String, pub timestamp: u64, pub packages: PackageList, pub files: Vec, pub device: String, } impl Backup { pub fn create(config: &Config, manager: Option) -> Result { let mut files: Vec = Vec::new(); for dir in &config.directories { files.push(PathInfo::from_path(config, dir)?); } Ok(Self { // TODO: UUID not really needed, maybe a shorter hash id: Uuid::new_v4().to_string(), timestamp: Self::get_timestamp(), packages: Manager::get_manager(manager)?.get_installed()?, files, device: config.device.clone(), }) } pub fn save(&self, config: &Config) -> Result<()> { info!("Save Backup {:?}", self.get_location(config)); let loc = self.get_location(config); loc.append_to_root(config)?; let backup_root = loc.get_absolute_dir(config); create_dir_all(&backup_root).unwrap(); let path = format!("{backup_root}/index.json"); let mut f = File::create(path).unwrap(); f.write_all(&serde_json::to_vec(self).unwrap()).unwrap(); for path in &self.files { path.save(&backup_root)?; } Ok(()) } pub fn get_last(config: &Config) -> Result> { let backup_index_root = format!("{}/index.json", config.root); info!(?backup_index_root, "backup index location:"); let list: Vec = match Self::get_json_content(&backup_index_root) { Ok(list) => list, Err(err) => { if err.to_string() == "No such file or directory (os error 2)" { return Ok(None); }; return Err(err); } }; info!(?list, "backup index:"); Ok(Some(Self::from_index( config, &list.last().ok_or(Error::BackupNotFound)?.id, )?)) } pub fn from_index(config: &Config, id: &Id) -> Result { let backup_index_root = format!("{}/index.json", config.root); let list: Vec = Self::get_json_content(&backup_index_root)?; let index_loc = list .iter() .find(|bl| &bl.id == id) .ok_or(Error::BackupNotFound)? .rel_location .clone(); let path = format!("{}/{index_loc}/index.json", config.root); let index_file: Self = Self::get_json_content(&path)?; Ok(index_file) } pub fn get_location(&self, config: &Config) -> IndexEntry { let rel_location = format!("{}_{}", config.device, self.timestamp); IndexEntry { id: self.id.to_string(), rel_location, } } pub fn get_absolute_file_location(&self, config: &Config, rel_location: &str) -> String { let loc = self.get_location(config).get_absolute_dir(config); format!("{loc}/{rel_location}") } pub fn restore(&self, config: &Config) -> Result<()> { info!(?self.id, ?self.timestamp, "Restore Backup"); let backup_root = self.get_location(config).get_absolute_dir(config); for path in &self.files { path.restore(config, &backup_root)?; } Ok(()) } fn get_json_content Deserialize<'a>>(path: &str) -> Result { let mut file = File::open(path)?; let mut content = String::new(); file.read_to_string(&mut content)?; Ok(serde_json::from_str(&content)?) } fn get_timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexEntry { id: Id, rel_location: String, } impl IndexEntry { pub fn get_absolute_dir(&self, config: &Config) -> String { format!("{}/{}", config.root, self.rel_location) } pub fn append_to_root(&self, config: &Config) -> Result<()> { let backup_index_root = format!("{}/index.json", config.root); let path = PathBuf::from(&backup_index_root); if path.exists() { let mut f = File::open(&path)?; let mut content = String::new(); f.read_to_string(&mut content)?; let mut loc: Vec = serde_json::from_str(&content)?; let mut f = File::create(path)?; loc.push(self.clone()); f.write_all(&serde_json::to_vec(&loc)?)?; } else { create_dir_all(&config.root).unwrap(); let mut f = File::create(backup_index_root)?; f.write_all(&serde_json::to_vec(&vec![self])?)?; }; Ok(()) } }