use std::{ fs::{self, File, create_dir_all}, io::{self, Read, Write}, path::{Path, PathBuf}, }; use crate::error::BackupError; use super::{backup_manager::BackupHandler, placeholder::*}; use database::GameVersion; use log::{debug, warn}; use rustix::path::Arg; use tempfile::tempfile; use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize}; pub fn resolve(meta: &mut CloudSaveMetadata) -> File { let f = File::create_new("save").unwrap(); let compressor = zstd::Encoder::new(f, 22).unwrap(); let mut tarball = tar::Builder::new(compressor); let manager = BackupManager::new(); for file in meta.files.iter_mut() { let id = uuid::Uuid::new_v4().to_string(); let os = match file .conditions .iter() .find_map(|p| match p { super::conditions::Condition::Os(os) => Some(os), _ => None }) .cloned() { Some(os) => os, None => { warn!( "File {:?} could not be backed up because it did not provide an OS", &file ); continue; } }; let handler = match manager.sources.get(&(manager.current_platform, os)) { Some(h) => *h, None => continue, }; let t_path = PathBuf::from(normalize(&file.path, os)); println!("{:?}", &t_path); let path = parse_path(t_path, handler, &meta.game_version).unwrap(); let f = std::fs::metadata(&path).unwrap(); // TODO: Fix unwrap here if f.is_dir() { tarball.append_dir_all(&id, path).unwrap(); } else if f.is_file() { tarball .append_file(&id, &mut File::open(path).unwrap()) .unwrap(); } file.id = Some(id); } let binding = serde_json::to_string(meta).unwrap(); let serialized = binding.as_bytes(); let mut file = tempfile().unwrap(); file.write_all(serialized).unwrap(); tarball.append_file("metadata", &mut file).unwrap(); tarball.into_inner().unwrap().finish().unwrap() } pub fn extract(file: PathBuf) -> Result<(), BackupError> { let tmpdir = tempfile::tempdir().unwrap(); // Reopen the file for reading let file = File::open(file).unwrap(); let decompressor = zstd::Decoder::new(file).unwrap(); let mut f = tar::Archive::new(decompressor); f.unpack(tmpdir.path()).unwrap(); let path = tmpdir.path(); let mut manifest = File::open(path.join("metadata")).unwrap(); let mut manifest_slice = Vec::new(); manifest.read_to_end(&mut manifest_slice).unwrap(); let manifest: CloudSaveMetadata = serde_json::from_slice(&manifest_slice).unwrap(); for file in manifest.files { let current_path = path.join(file.id.as_ref().unwrap()); let manager = BackupManager::new(); let os = match file .conditions .iter() .find_map(|p| match p { super::conditions::Condition::Os(os) => Some(os), _ => None }) .cloned() { Some(os) => os, None => { warn!( "File {:?} could not be replaced up because it did not provide an OS", &file ); continue; } }; let handler = match manager.sources.get(&(manager.current_platform, os)) { Some(h) => *h, None => continue, }; let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?; create_dir_all(new_path.parent().unwrap()).unwrap(); println!( "Current path {:?} copying to {:?}", ¤t_path, &new_path ); copy_item(current_path, new_path).unwrap(); } Ok(()) } pub fn copy_item>(src: P, dest: P) -> io::Result<()> { let src_path = src.as_ref(); let dest_path = dest.as_ref(); let metadata = fs::metadata(src_path)?; if metadata.is_file() { // Ensure the parent directory of the destination exists for a file copy if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent)?; } fs::copy(src_path, dest_path)?; } else if metadata.is_dir() { // For directories, we call the recursive helper function. // The destination for the recursive copy is the `dest_path` itself. copy_dir_recursive(src_path, dest_path)?; } else { // Handle other file types like symlinks if necessary, // for now, return an error or skip. return Err(io::Error::other( format!("Source {:?} is neither a file nor a directory", src_path), )); } Ok(()) } fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> { fs::create_dir_all(dest)?; for entry in fs::read_dir(src)? { let entry = entry?; let entry_path = entry.path(); let entry_file_name = match entry_path.file_name() { Some(name) => name, None => continue, // Skip if somehow there's no file name }; let dest_entry_path = dest.join(entry_file_name); let metadata = entry.metadata()?; if metadata.is_file() { debug!( "Writing file {} to {}", entry_path.display(), dest_entry_path.display() ); fs::copy(&entry_path, &dest_entry_path)?; } else if metadata.is_dir() { copy_dir_recursive(&entry_path, &dest_entry_path)?; } // Ignore other types like symlinks for this basic implementation } Ok(()) } pub fn parse_path( path: PathBuf, backup_handler: &dyn BackupHandler, game: &GameVersion, ) -> Result { println!("Parsing: {:?}", &path); let mut s = PathBuf::new(); for component in path.components() { match component.as_str().unwrap() { ROOT => s.push(backup_handler.root_translate(&path, game)?), GAME => s.push(backup_handler.game_translate(&path, game)?), BASE => s.push(backup_handler.base_translate(&path, game)?), HOME => s.push(backup_handler.home_translate(&path, game)?), STORE_USER_ID => s.push(backup_handler.store_user_id_translate(&path, game)?), OS_USER_NAME => s.push(backup_handler.os_user_name_translate(&path, game)?), WIN_APP_DATA => s.push(backup_handler.win_app_data_translate(&path, game)?), WIN_LOCAL_APP_DATA => s.push(backup_handler.win_local_app_data_translate(&path, game)?), WIN_LOCAL_APP_DATA_LOW => { s.push(backup_handler.win_local_app_data_low_translate(&path, game)?) } WIN_DOCUMENTS => s.push(backup_handler.win_documents_translate(&path, game)?), WIN_PUBLIC => s.push(backup_handler.win_public_translate(&path, game)?), WIN_PROGRAM_DATA => s.push(backup_handler.win_program_data_translate(&path, game)?), WIN_DIR => s.push(backup_handler.win_dir_translate(&path, game)?), XDG_DATA => s.push(backup_handler.xdg_data_translate(&path, game)?), XDG_CONFIG => s.push(backup_handler.xdg_config_translate(&path, game)?), SKIP => s.push(backup_handler.skip_translate(&path, game)?), _ => s.push(PathBuf::from(component.as_os_str())), } } println!("Final line: {:?}", &s); Ok(s) }