Compare commits
	
		
			4 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f098430a78 | |||
| bf2336386e | |||
| a0357b3ee3 | |||
| 125bc24ed0 | 
| @@ -14,6 +14,7 @@ uuid = { version = "^0.8", features = ["v4"] } | ||||
| twox-hash = "^1.6" | ||||
| thiserror = "^1.0" | ||||
| diesel = { version = "^1.4", features = ["sqlite"] } | ||||
| diesel_migrations = "^1.4" | ||||
| dotenv = "^0.15" | ||||
|  | ||||
| [dev-dependencies] | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| use crate::utils::{self, ProjectDirError}; | ||||
| use log::{debug, info, trace, warn}; | ||||
| use log::debug; | ||||
| use save_sync::db::{establish_connection, query::GameSaveQuery, Database}; | ||||
| use save_sync::game::{GameFile, GameSaveLocation}; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use thiserror::Error; | ||||
| @@ -7,7 +8,6 @@ use walkdir::WalkDir; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Archive { | ||||
|     tracked_games: Vec<GameSaveLocation>, | ||||
|     data_root: PathBuf, | ||||
|     config_root: PathBuf, | ||||
| } | ||||
| @@ -31,7 +31,6 @@ impl Archive { | ||||
|         debug!("Created default Archive with: {:?} and {:?}", data, config); | ||||
|  | ||||
|         Ok(Self { | ||||
|             tracked_games: Vec::new(), | ||||
|             data_root: data, | ||||
|             config_root: config, | ||||
|         }) | ||||
| @@ -58,12 +57,13 @@ impl Archive { | ||||
|         ); | ||||
|  | ||||
|         Self { | ||||
|             tracked_games: Vec::new(), | ||||
|             data_root: data_root.to_path_buf(), | ||||
|             config_root: config_root.to_path_buf(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Archive { | ||||
|     /// Adds a path and its contents to the list of tracked game files | ||||
|     /// | ||||
|     /// TODO: Add note here about how GameSaveLocation, GameFile and tracking individual files rather than directories work | ||||
| @@ -74,7 +74,7 @@ impl Archive { | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// # use client::archive::Archive; | ||||
|     /// let mut archive = Archive::try_default().unwrap(); | ||||
|     /// let mut archive = Archive::try_default().expect("Failed to create an Archive"); | ||||
|     /// let save_path = "/home/user/Documents/generic_company/generic_game/save_folder"; | ||||
|     /// match archive.track_game(save_path) { | ||||
|     ///     Ok(_) => println!("Save Sync is now tracking {}", save_path), | ||||
| @@ -83,7 +83,7 @@ impl Archive { | ||||
|     /// ``` | ||||
|     pub fn track_game<P: AsRef<Path>>(&mut self, path: P) -> Result<(), GameTrackError> { | ||||
|         let game_save_loc = self.get_game_save_files(path, None)?; | ||||
|         self.tracked_games.push(game_save_loc); | ||||
|         Self::write_game_save(&game_save_loc)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -97,7 +97,7 @@ impl Archive { | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// # use client::archive::Archive; | ||||
|     /// let mut archive = Archive::try_default().unwrap(); | ||||
|     /// let mut archive = Archive::try_default().expect("Failed to create an Archive"); | ||||
|     /// let save_path = "/home/user/Documents/generic_company/generic_game/save_folder"; | ||||
|     /// let friendly_name = "Generic Game"; | ||||
|     /// match archive.track_game_with_friendly(save_path, friendly_name) { | ||||
| @@ -109,7 +109,7 @@ impl Archive { | ||||
|         P: AsRef<Path>, | ||||
|     { | ||||
|         let game_save_loc = self.get_game_save_files(path, Some(name))?; | ||||
|         self.tracked_games.push(game_save_loc); | ||||
|         Self::write_game_save(&game_save_loc)?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -155,50 +155,50 @@ impl Archive { | ||||
|         let friendly_name = friendly_name.map(|s| s.to_owned()); | ||||
|         Ok(GameSaveLocation::new(path, game_files, friendly_name)) | ||||
|     } | ||||
| } | ||||
|  | ||||
|     /// Removes a game from the list of tracked games | ||||
|     /// | ||||
|     /// Will fail if: | ||||
|     /// * The path provided isn't associated with any game | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `path` - The path of the tracked game save location which will be dropped | ||||
|     /// | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// # use client::archive::Archive; | ||||
|     /// let mut archive = Archive::try_default().unwrap(); | ||||
|     /// let drop_res = archive.drop_game("/home/user/Documents/generic_company/generic_game/save_folder"); | ||||
|     /// ``` | ||||
|     pub fn drop_game<P: AsRef<Path>>(&mut self, path: P) -> Result<(), GameDropError> { | ||||
|         self.tracked_games | ||||
|             .retain(|game| game.original_path != path.as_ref()); | ||||
|  | ||||
|         // TODO: Remove backup copy of game save location on disk | ||||
|         Ok(()) | ||||
| impl Archive { | ||||
|     pub fn drop_game<P: AsRef<Path>>(path: P) -> Result<usize, GameDropError> { | ||||
|         let conn = establish_connection(); | ||||
|         let query = GameSaveQuery::Path(path.as_ref()); | ||||
|         Ok(Database::drop_game_save(&conn, query)?) | ||||
|     } | ||||
|  | ||||
|     /// Removes a game from the list of tracked games using the game's friendly name | ||||
|     /// | ||||
|     /// Otherwise, is identical to [`Archive::drop_game`] | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `name` - The friendly name of the tracked game save location which will be dropped | ||||
|     /// | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// # use client::archive::Archive; | ||||
|     /// let mut archive = Archive::try_default().unwrap(); | ||||
|     /// let drop_res = archive.drop_game("raging_loop"); | ||||
|     /// ``` | ||||
|     pub fn drop_game_with_friendly(&mut self, name: &str) -> Result<(), GameDropError> { | ||||
|         self.tracked_games.retain(|game| match &game.friendly_name { | ||||
|             Some(f_name) => f_name != name, | ||||
|             None => false, | ||||
|         }); | ||||
|     pub fn drop_game_with_friendly(name: &str) -> Result<usize, GameDropError> { | ||||
|         let conn = establish_connection(); | ||||
|         let query = GameSaveQuery::FriendlyName(name); | ||||
|         Ok(Database::drop_game_save(&conn, query)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
|         // TODO: Remove backup copy of game save location on disk | ||||
|         Ok(()) | ||||
| impl Archive { | ||||
|     pub fn get_game<P>(path: P) -> Result<Option<GameSaveLocation>, GameGetError> | ||||
|     where | ||||
|         P: AsRef<Path>, | ||||
|     { | ||||
|         let conn = establish_connection(); | ||||
|         let query = GameSaveQuery::Path(path.as_ref()); | ||||
|         Ok(Database::get_game_save(&conn, query)?) | ||||
|     } | ||||
|  | ||||
|     pub fn get_game_with_friendly(name: &str) -> Result<Option<GameSaveLocation>, GameGetError> { | ||||
|         let conn = establish_connection(); | ||||
|         let query = GameSaveQuery::FriendlyName(name); | ||||
|         Ok(Database::get_game_save(&conn, query)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Archive { | ||||
|     pub fn get_all_games() -> Result<Option<Vec<GameSaveLocation>>, GameGetError> { | ||||
|         let conn = establish_connection(); | ||||
|         Ok(Database::get_all_game_saves(&conn)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Archive { | ||||
|     fn write_game_save(game_save_loc: &GameSaveLocation) -> Result<(), GameTrackError> { | ||||
|         let conn = establish_connection(); | ||||
|         Ok(Database::write_game_save(&conn, &game_save_loc)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -208,6 +208,8 @@ pub enum GameTrackError { | ||||
|     IoError(#[from] std::io::Error), | ||||
|     #[error("{0:?} is not a supported inode type (File System Object)")] | ||||
|     UnknownFileSystemObject(PathBuf), // FIXME: Is there a better name for this? | ||||
|     #[error(transparent)] | ||||
|     DatabaseError(#[from] save_sync::db::DatabaseError), | ||||
| } | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| @@ -216,4 +218,12 @@ pub enum GameDropError { | ||||
|     UnknownFriendlyName(String), | ||||
|     #[error("Unable to find game with the path {0:?}")] | ||||
|     UnknownPath(PathBuf), | ||||
|     #[error(transparent)] | ||||
|     DatabaseError(#[from] save_sync::db::DatabaseError), | ||||
| } | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum GameGetError { | ||||
|     #[error(transparent)] | ||||
|     DatabaseError(#[from] save_sync::db::DatabaseError), | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,8 @@ use clap::{crate_authors, crate_description, crate_version, ArgMatches}; | ||||
| use clap::{App, Arg, SubCommand}; | ||||
| use client::archive::Archive; | ||||
| use dotenv::dotenv; | ||||
| use log::{debug, info}; | ||||
| use std::path::Path; | ||||
| use log::info; | ||||
|  | ||||
| fn main() { | ||||
|     dotenv().ok(); | ||||
|     env_logger::init(); | ||||
| @@ -32,29 +32,142 @@ fn main() { | ||||
|                         .help("A friendly name for a tracked file / directory"), | ||||
|                 ), | ||||
|         ) | ||||
|         .subcommand( | ||||
|             SubCommand::with_name("info") | ||||
|                 .arg( | ||||
|                     Arg::with_name("path") | ||||
|                         .value_name("PATH") | ||||
|                         .takes_value(true) | ||||
|                         .required_unless("friendly") | ||||
|                         .index(1) | ||||
|                         .help("The path of the game save"), | ||||
|                 ) | ||||
|                 .arg( | ||||
|                     Arg::with_name("friendly") | ||||
|                         .long("friendly") | ||||
|                         .short("f") | ||||
|                         .value_name("NAME") | ||||
|                         .takes_value(true) | ||||
|                         .help("The friendly name of the game save"), | ||||
|                 ), | ||||
|         ) | ||||
|         .subcommand(SubCommand::with_name("list")) | ||||
|         .subcommand( | ||||
|             SubCommand::with_name("drop") | ||||
|                 .arg( | ||||
|                     Arg::with_name("path") | ||||
|                         .value_name("PATH") | ||||
|                         .takes_value(true) | ||||
|                         .required_unless("friendly") | ||||
|                         .index(1) | ||||
|                         .help("The path of the files you no longer want to track"), | ||||
|                 ) | ||||
|                 .arg( | ||||
|                     Arg::with_name("friendly") | ||||
|                         .long("friendly") | ||||
|                         .short("f") | ||||
|                         .value_name("NAME") | ||||
|                         .takes_value(true) | ||||
|                         .help("The friendly name of the game save you no longer want tracked."), | ||||
|                 ), | ||||
|         ) | ||||
|         .get_matches(); | ||||
|  | ||||
|     match m.subcommand() { | ||||
|         ("track", Some(sub_m)) => track_path(sub_m), | ||||
|         ("track", Some(sub_m)) => track_save(sub_m), | ||||
|         ("drop", Some(sub_m)) => drop_save(sub_m), | ||||
|         ("info", Some(sub_m)) => tracked_save_info(sub_m), | ||||
|         ("list", Some(sub_m)) => list_tracked_saves(sub_m), | ||||
|         _ => eprintln!("No valid subcommand / argument provided"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn track_path(matches: &ArgMatches) { | ||||
|     let path = Path::new(matches.value_of("path").unwrap()); | ||||
|     let mut archive = Archive::try_default().expect("Failed to create an Archive struct"); | ||||
| fn track_save(matches: &ArgMatches) { | ||||
|     let path_str = matches | ||||
|         .value_of("path") | ||||
|         .expect("The path argument was not set despite it being required"); | ||||
|     let mut archive = Archive::try_default().expect("Failed to create a new Archive"); | ||||
|  | ||||
|     if let Some(f_name) = matches.value_of("friendly") { | ||||
|         info!("Name {} present for {:?}", f_name, path); | ||||
|         info!("Name {} present for {:?}", f_name, path_str); | ||||
|         archive | ||||
|             .track_game_with_friendly(path, f_name) | ||||
|             .track_game_with_friendly(path_str, f_name) | ||||
|             .expect("Archive failed to track Game Save Location") | ||||
|     } else { | ||||
|         info!("No friendly name present for {:?}", path); | ||||
|         info!("No friendly name present for {:?}", path_str); | ||||
|         archive | ||||
|             .track_game(path) | ||||
|             .track_game(path_str) | ||||
|             .expect("Archive failed to track Game Save Location"); | ||||
|     } | ||||
|  | ||||
|     info!("Now Tracking: {:?}", path); | ||||
|     info!("Now Tracking: {:?}", path_str); | ||||
| } | ||||
|  | ||||
| fn tracked_save_info(matches: &ArgMatches) { | ||||
|     let maybe_game = if let Some(f_name) = matches.value_of("friendly") { | ||||
|         Archive::get_game_with_friendly(f_name).expect("Failed to get game save info from Archive") | ||||
|     } else { | ||||
|         // There is guaranteed to be a path given by the user | ||||
|         let path_str = matches | ||||
|             .value_of("path") | ||||
|             .expect("The path argument was not set despite it being required"); | ||||
|         Archive::get_game(path_str).expect("Failed to get game save info from Archive") | ||||
|     }; | ||||
|  | ||||
|     match maybe_game { | ||||
|         Some(game) => { | ||||
|             if let Some(name) = game.friendly_name { | ||||
|                 println!("Friendly Name: {}", name); | ||||
|             } else { | ||||
|                 println!("Friendly Name: None"); | ||||
|             } | ||||
|  | ||||
|             println!("Original Path: {:?}", game.original_path); | ||||
|             println!("UUID: {:?}", game.uuid); | ||||
|             println!("---\nFiles:"); | ||||
|  | ||||
|             for file in game.files { | ||||
|                 println!("Path: {:?}", file.original_path); | ||||
|                 println!("Hash: {}", file.hash); | ||||
|                 println!(); | ||||
|             } | ||||
|         } | ||||
|         None => println!("No tracked game save was found."), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn list_tracked_saves(_matches: &ArgMatches) { | ||||
|     let maybe_games = Archive::get_all_games().expect("Failed to get all Games from the Archive"); | ||||
|  | ||||
|     match maybe_games { | ||||
|         Some(games) => { | ||||
|             for game in games { | ||||
|                 if let Some(name) = game.friendly_name { | ||||
|                     print!("[{}] ", name); | ||||
|                 } | ||||
|                 println!("{:?}", game.original_path); | ||||
|                 println!("UUID: {:?}", game.uuid); | ||||
|                 println!("---"); | ||||
|             } | ||||
|         } | ||||
|         None => println!("There are no tracked games"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn drop_save(matches: &ArgMatches) { | ||||
|     let items_deleted = if let Some(name) = matches.value_of("friendly") { | ||||
|         Archive::drop_game_with_friendly(name).expect("Archive failed to delete from database") | ||||
|     } else { | ||||
|         let path_str = matches | ||||
|             .value_of("path") | ||||
|             .expect("The path argument was not set despite it being required"); | ||||
|  | ||||
|         Archive::drop_game(path_str).expect("Archive failed to delete from database") | ||||
|     }; | ||||
|  | ||||
|     match items_deleted { | ||||
|         0 => println!("Save Sync can't drop what was never tracked to begin with"), | ||||
|         1 => println!("Save Sync successfully dropped the game from the list of tracked games"), | ||||
|         _ => unreachable!(), | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,27 +1,6 @@ | ||||
| use directories::ProjectDirs; | ||||
| use std::path::Path; | ||||
| use thiserror::Error; | ||||
|  | ||||
| pub fn calc_file_hash(path: &Path) -> Option<u64> { | ||||
|     unimplemented!() | ||||
| } | ||||
|  | ||||
| pub fn archive_directory(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     unimplemented!() | ||||
| } | ||||
|  | ||||
| pub fn archive_file(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     unimplemented!() | ||||
| } | ||||
|  | ||||
| pub fn unarchive_directory(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     unimplemented!() | ||||
| } | ||||
|  | ||||
| pub fn unarchive_file(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     unimplemented!() | ||||
| } | ||||
|  | ||||
| pub fn get_project_dirs() -> Result<ProjectDirs, ProjectDirError> { | ||||
|     ProjectDirs::from("dev", "musuka", "save-sync").ok_or(ProjectDirError::HomeNotSet) | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| -- Your SQL goes here | ||||
| CREATE TABLE game_file ( | ||||
|     id INTEGER PRIMARY KEY, | ||||
|     original_path BLOB NOT NULL, -- TEXT assumes a Unicode Encoding | ||||
|     file_hash INTEGER NOT NULL, -- u64, but that's not | ||||
|     id INTEGER PRIMARY KEY NOT NULL, | ||||
|     original_path TEXT NOT NULL UNIQUE, -- TEXT assumes a Unicode Encoding | ||||
|     file_hash BLOB NOT NULL UNIQUE, | ||||
|     game_save_id INTEGER NOT NULL, | ||||
|     FOREIGN KEY (game_save_id) REFERENCES game_save_location (id) | ||||
| ) | ||||
| @@ -1,7 +1,7 @@ | ||||
| -- Your SQL goes here | ||||
| CREATE TABLE game_save_location ( | ||||
|     id INTEGER PRIMARY KEY, | ||||
|     friendly_name TEXT -- This can be null | ||||
|     original_PATH BLOB NOT NULL, | ||||
|     uuid BLOB NOT NULL | ||||
|     id INTEGER PRIMARY KEY NOT NULL, | ||||
|     friendly_name TEXT UNIQUE, -- This can be null | ||||
|     original_path TEXT NOT NULL UNIQUE, | ||||
|     uuid BLOB NOT NULL UNIQUE | ||||
| ) | ||||
							
								
								
									
										410
									
								
								src/db.rs
									
									
									
									
									
								
							
							
						
						
									
										410
									
								
								src/db.rs
									
									
									
									
									
								
							| @@ -1,7 +1,16 @@ | ||||
| use super::game::{GameFile, GameSaveLocation}; | ||||
| use diesel::prelude::*; | ||||
| use diesel::sqlite::SqliteConnection; | ||||
| use diesel_migrations::embed_migrations; | ||||
| use dotenv::dotenv; | ||||
| use std::env; | ||||
| use query::{GameFileQuery, GameSaveQuery}; | ||||
| use std::{env, path::PathBuf}; | ||||
| use thiserror::Error; | ||||
|  | ||||
| pub type Result<T> = std::result::Result<T, DatabaseError>; | ||||
| pub type ResultantOption<T> = std::result::Result<std::option::Option<T>, DatabaseError>; | ||||
|  | ||||
| embed_migrations!("./migrations"); | ||||
|  | ||||
| /// Establishes a DB Connection with a Sqlite Database | ||||
| /// | ||||
| @@ -21,5 +30,402 @@ pub fn establish_connection() -> SqliteConnection { | ||||
|     // or have establish_connection return a Result with a thiserror enum | ||||
|  | ||||
|     let db_url = env::var("DATABASE_URL").expect("$DATABASE_URL was not set"); | ||||
|     SqliteConnection::establish(&db_url).expect(&format!("Error connecting to {}", db_url)) | ||||
|     let conn = SqliteConnection::establish(&db_url) | ||||
|         .unwrap_or_else(|_| panic!("Error connecting to {}", db_url)); | ||||
|  | ||||
|     // Perform all Migrations on Database | ||||
|     embedded_migrations::run(&conn).expect("Failed to run migrations on sqlite3 database"); | ||||
|     conn | ||||
| } | ||||
|  | ||||
| /// The Database struct contains methods which interact with the Sqlite3 backend | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct Database; | ||||
|  | ||||
| // Add GameFile Implementations | ||||
| impl Database { | ||||
|     /// Writes a GameFile to the Database | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `conn` - A reference to a [`SqliteConnection`] | ||||
|     /// * `file` - A reference to the [`GameFile`] that will be Saved | ||||
|     /// * `needle` - The ID of the [`GameSaveLocation`] that the [`GameFile`] belongs to. | ||||
|     /// | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// // Create the GameFile | ||||
|     /// # use save_sync::game::GameFile; | ||||
|     /// # use save_sync::db::{establish_connection, Database}; | ||||
|     /// let path = "/home/user/Documents/some_company/some_game/saves/save01.sav"; | ||||
|     /// match GameFile::new(path) { | ||||
|     ///     Ok(file) => { | ||||
|     ///         //  TODO: Better explain how we can get the id of a GameSaveLocation | ||||
|     ///         let conn = establish_connection(); | ||||
|     ///         let id = 1; | ||||
|     ///          | ||||
|     ///         match Database::write_game_file(&conn, &file, id) { | ||||
|     ///             Ok(_) => println!("Writing a File was a Success"), | ||||
|     ///             Err(err) => eprintln!("Database Error: {}", err), | ||||
|     ///         }; | ||||
|     ///     } | ||||
|     ///     Err(err) => { eprintln!("Error while attempting to calculate the hash of {}", path)} | ||||
|     /// }; | ||||
|     /// ``` | ||||
|     pub fn write_game_file(conn: &SqliteConnection, file: &GameFile, needle: i32) -> Result<()> { | ||||
|         use super::models::NewGameFile; | ||||
|         use super::schema::game_file; | ||||
|         use DatabaseError::InvalidPath; | ||||
|  | ||||
|         let hash_bytes: [u8; 8] = file.hash.to_be_bytes(); | ||||
|         let path = &file.original_path; | ||||
|         let path_str = path.to_str().ok_or_else(|| InvalidPath(path.clone()))?; | ||||
|  | ||||
|         let new_game_file = NewGameFile { | ||||
|             original_path: path_str, | ||||
|             file_hash: &hash_bytes, | ||||
|             game_save_id: needle, | ||||
|         }; | ||||
|  | ||||
|         diesel::insert_into(game_file::table) | ||||
|             .values(&new_game_file) | ||||
|             .execute(conn)?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Get GameFile Implementations | ||||
| impl Database { | ||||
|     pub fn get_game_file( | ||||
|         conn: &SqliteConnection, | ||||
|         query: GameFileQuery, | ||||
|     ) -> ResultantOption<GameFile> { | ||||
|         use super::models::DbGameFile; | ||||
|         use super::schema::game_file::dsl::{file_hash, game_file, original_path}; | ||||
|         use DatabaseError::{InvalidPath, UnexpectedBehaviour}; | ||||
|  | ||||
|         let game_files = match query { | ||||
|             GameFileQuery::Hash(hash) => { | ||||
|                 let hash_slice: &[u8] = &hash.to_be_bytes(); | ||||
|                 game_file | ||||
|                     .filter(file_hash.eq(hash_slice)) | ||||
|                     .load::<DbGameFile>(conn)? | ||||
|             } | ||||
|             GameFileQuery::Path(path) => { | ||||
|                 let path_str = path | ||||
|                     .to_str() | ||||
|                     .ok_or_else(|| InvalidPath(path.to_path_buf()))?; | ||||
|                 game_file | ||||
|                     .filter(original_path.eq(path_str)) | ||||
|                     .load::<DbGameFile>(conn)? | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let maybe_game_files = match game_files.len() { | ||||
|             0 => None, | ||||
|             1 => Some((&game_files[0]).into()), | ||||
|             num => return Err(UnexpectedBehaviour(format!("{:?}", query), num)), | ||||
|         }; | ||||
|  | ||||
|         Ok(maybe_game_files) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Get GameFiles Implementations | ||||
| impl Database { | ||||
|     pub fn get_game_files( | ||||
|         conn: &SqliteConnection, | ||||
|         query: GameSaveQuery, | ||||
|     ) -> ResultantOption<Vec<GameFile>> { | ||||
|         match query { | ||||
|             GameSaveQuery::Id(id) => Self::_get_game_files(conn, id), | ||||
|             _ => match Self::get_game_save_id(conn, query)? { | ||||
|                 Some(id) => Self::_get_game_files(conn, id), | ||||
|                 None => Ok(None), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn _get_game_files(conn: &SqliteConnection, needle: i32) -> ResultantOption<Vec<GameFile>> { | ||||
|         use super::models::DbGameFile; | ||||
|         use super::schema::game_file::dsl::{game_file, game_save_id}; | ||||
|  | ||||
|         let game_files = game_file | ||||
|             .filter(game_save_id.eq(needle)) | ||||
|             .load::<DbGameFile>(conn)?; | ||||
|  | ||||
|         let maybe_game_files = if !game_files.is_empty() { | ||||
|             Some(game_files.iter().map(GameFile::from).collect()) | ||||
|         } else { | ||||
|             None | ||||
|         }; | ||||
|  | ||||
|         Ok(maybe_game_files) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Drop GameFile Implementations | ||||
| impl Database { | ||||
|     fn drop_game_file(conn: &SqliteConnection, query: GameFileQuery) -> Result<usize> { | ||||
|         use super::schema::game_file::dsl::{file_hash, game_file, original_path}; | ||||
|         use DatabaseError::{InvalidPath, UnexpectedBehaviour}; | ||||
|  | ||||
|         let num_deleted = match query { | ||||
|             GameFileQuery::Hash(hash) => { | ||||
|                 let hash_bytes: &[u8] = &hash.to_be_bytes(); | ||||
|                 let expr = game_file.filter(file_hash.eq(hash_bytes)); | ||||
|                 diesel::delete(expr).execute(conn)? | ||||
|             } | ||||
|             GameFileQuery::Path(path) => { | ||||
|                 let path_str = path | ||||
|                     .to_str() | ||||
|                     .ok_or_else(|| InvalidPath(path.to_path_buf()))?; | ||||
|                 let expr = game_file.filter(original_path.eq(path_str)); | ||||
|                 diesel::delete(expr).execute(conn)? | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if num_deleted <= 1 { | ||||
|             Ok(num_deleted) | ||||
|         } else { | ||||
|             Err(UnexpectedBehaviour(format!("{:?}", query), num_deleted)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Add GameSaveLocation Implementations | ||||
| impl Database { | ||||
|     /// Writes a GameSaveLocation to the Database | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `conn` - A reference to a [`SqliteConnection`] | ||||
|     /// * `save_loc` - A reference to a [`GameSaveLocation`] | ||||
|     /// | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// # use save_sync::game::{GameSaveLocation, GameFile}; | ||||
|     /// # use save_sync::db::{Database, establish_connection}; | ||||
|     /// # use save_sync::db::query::GameSaveQuery; | ||||
|     /// // Create the GameSaveLocation | ||||
|     /// let path = "/home/user/Documents/some_company/some_game/saves"; | ||||
|     /// let files: Vec<GameFile> = Vec::new(); | ||||
|     /// let friendly_name = "Some Game".to_string(); | ||||
|     /// let game_save_location = GameSaveLocation::new(path, files, Some(friendly_name)); | ||||
|     /// | ||||
|     /// // Write the GameSaveLocation to the Database | ||||
|     /// let conn = establish_connection(); | ||||
|     /// Database::write_game_save(&conn, &game_save_location); | ||||
|     /// # Database::drop_game_save(&conn, GameSaveQuery::Uuid(game_save_location.uuid)); // Clean up | ||||
|     /// ``` | ||||
|     pub fn write_game_save(conn: &SqliteConnection, save_loc: &GameSaveLocation) -> Result<()> { | ||||
|         // Write Game Save Location to Database | ||||
|         use super::models::NewGameSaveLocation; | ||||
|         use super::schema::game_save_location; | ||||
|         use DatabaseError::InvalidPath; | ||||
|  | ||||
|         let path = &save_loc.original_path; | ||||
|         let original_path = path.to_str().ok_or_else(|| InvalidPath(path.clone()))?; | ||||
|  | ||||
|         let new_game_save = NewGameSaveLocation { | ||||
|             friendly_name: save_loc.friendly_name.as_deref(), | ||||
|             original_path, | ||||
|             uuid: save_loc.uuid.as_bytes(), | ||||
|         }; | ||||
|  | ||||
|         diesel::insert_into(game_save_location::table) | ||||
|             .values(&new_game_save) | ||||
|             .execute(conn)?; | ||||
|  | ||||
|         // Get the ID of the Game Save in the database | ||||
|         let id = Self::get_game_save_id(conn, GameSaveQuery::Uuid(save_loc.uuid))? | ||||
|             .expect("Failed to read entry in database after writing it."); | ||||
|  | ||||
|         // Write all the GameFiles into the database | ||||
|         for game_file in &save_loc.files { | ||||
|             Self::write_game_file(conn, game_file, id)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Get GameSaveLocation Implementations | ||||
| impl Database { | ||||
|     pub fn get_game_save( | ||||
|         conn: &SqliteConnection, | ||||
|         query: GameSaveQuery, | ||||
|     ) -> ResultantOption<GameSaveLocation> { | ||||
|         use super::models::DbGameSaveLocation; | ||||
|         use super::schema::game_save_location::dsl::{ | ||||
|             friendly_name, game_save_location, id, original_path, uuid, | ||||
|         }; | ||||
|         use DatabaseError::{InvalidPath, UnexpectedBehaviour}; | ||||
|  | ||||
|         let save_locs = match query { | ||||
|             GameSaveQuery::Id(needle) => game_save_location | ||||
|                 .filter(id.eq(needle)) | ||||
|                 .load::<DbGameSaveLocation>(conn)?, | ||||
|             GameSaveQuery::FriendlyName(name) => game_save_location | ||||
|                 .filter(friendly_name.eq(name)) | ||||
|                 .load::<DbGameSaveLocation>(conn)?, | ||||
|             GameSaveQuery::Path(path) => { | ||||
|                 let path_str = path | ||||
|                     .to_str() | ||||
|                     .ok_or_else(|| InvalidPath(path.to_path_buf()))?; | ||||
|                 game_save_location | ||||
|                     .filter(original_path.eq(path_str)) | ||||
|                     .load::<DbGameSaveLocation>(conn)? | ||||
|             } | ||||
|             GameSaveQuery::Uuid(uuid_value) => { | ||||
|                 let uuid_bytes: &[u8] = uuid_value.as_bytes(); | ||||
|                 game_save_location | ||||
|                     .filter(uuid.eq(uuid_bytes)) | ||||
|                     .load::<DbGameSaveLocation>(conn)? | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let maybe_save_loc = match save_locs.len() { | ||||
|             0 => None, | ||||
|             1 => Some((&save_locs[0]).into()), | ||||
|             num => return Err(UnexpectedBehaviour(format!("{:?}", query), num)), | ||||
|         }; | ||||
|  | ||||
|         Ok(maybe_save_loc) | ||||
|     } | ||||
|  | ||||
|     pub fn get_game_save_id(conn: &SqliteConnection, query: GameSaveQuery) -> ResultantOption<i32> { | ||||
|         use super::schema::game_save_location::dsl::{ | ||||
|             friendly_name, game_save_location, id, original_path, uuid, | ||||
|         }; | ||||
|         use DatabaseError::{InvalidPath, UnexpectedBehaviour}; | ||||
|  | ||||
|         let ids = match query { | ||||
|             GameSaveQuery::Id(id_value) => vec![id_value], // Why? | ||||
|             GameSaveQuery::Uuid(uuid_value) => { | ||||
|                 let uuid_bytes: &[u8] = uuid_value.as_bytes(); | ||||
|                 game_save_location | ||||
|                     .select(id) | ||||
|                     .filter(uuid.eq(uuid_bytes)) | ||||
|                     .load::<i32>(conn)? | ||||
|             } | ||||
|             GameSaveQuery::FriendlyName(name) => game_save_location | ||||
|                 .select(id) | ||||
|                 .filter(friendly_name.eq(name)) | ||||
|                 .load::<i32>(conn)?, | ||||
|             GameSaveQuery::Path(path) => { | ||||
|                 let path_str = path | ||||
|                     .to_str() | ||||
|                     .ok_or_else(|| InvalidPath(path.to_path_buf()))?; | ||||
|                 game_save_location | ||||
|                     .select(id) | ||||
|                     .filter(original_path.eq(path_str)) | ||||
|                     .load::<i32>(conn)? | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let maybe_id = match ids.len() { | ||||
|             0 => None, | ||||
|             1 => Some(ids[0]), | ||||
|             num => return Err(UnexpectedBehaviour(format!("{:?}", query), num)), | ||||
|         }; | ||||
|  | ||||
|         Ok(maybe_id) | ||||
|     } | ||||
|  | ||||
|     pub fn get_all_game_saves(conn: &SqliteConnection) -> ResultantOption<Vec<GameSaveLocation>> { | ||||
|         use super::models::DbGameSaveLocation; | ||||
|         use super::schema::game_save_location::dsl::game_save_location; | ||||
|  | ||||
|         let save_locs = game_save_location | ||||
|             .load::<DbGameSaveLocation>(conn)? | ||||
|             .iter() | ||||
|             .map(GameSaveLocation::from) | ||||
|             .collect::<Vec<GameSaveLocation>>(); | ||||
|  | ||||
|         let empty = save_locs.is_empty(); | ||||
|         Ok(if empty { None } else { Some(save_locs) }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Drop GameSaveLocation Implementations | ||||
| impl Database { | ||||
|     pub fn drop_game_save(conn: &SqliteConnection, query: GameSaveQuery) -> Result<usize> { | ||||
|         use super::schema::game_save_location::dsl::{ | ||||
|             friendly_name, game_save_location, id, original_path, uuid, | ||||
|         }; | ||||
|         use DatabaseError::{InvalidPath, UnexpectedBehaviour}; | ||||
|  | ||||
|         // Drop all files related to the to-be-deleted Game Save | ||||
|         if let Some(game_save) = Self::get_game_save(conn, query)? { | ||||
|             for file in game_save.files { | ||||
|                 Self::drop_game_file(conn, GameFileQuery::Hash(file.hash))?; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let num_deleted = match query { | ||||
|             GameSaveQuery::Id(needle) => { | ||||
|                 let expr = game_save_location.filter(id.eq(needle)); | ||||
|                 diesel::delete(expr).execute(conn)? | ||||
|             } | ||||
|             GameSaveQuery::FriendlyName(name) => { | ||||
|                 let expr = game_save_location.filter(friendly_name.eq(name)); | ||||
|                 diesel::delete(expr).execute(conn)? | ||||
|             } | ||||
|             GameSaveQuery::Path(path) => { | ||||
|                 let path_str = path | ||||
|                     .to_str() | ||||
|                     .ok_or_else(|| InvalidPath(path.to_path_buf()))?; | ||||
|                 let expr = game_save_location.filter(original_path.eq(path_str)); | ||||
|                 diesel::delete(expr).execute(conn)? | ||||
|             } | ||||
|             GameSaveQuery::Uuid(uuid_value) => { | ||||
|                 let uuid_bytes: &[u8] = uuid_value.as_bytes(); | ||||
|                 let expr = game_save_location.filter(uuid.eq(uuid_bytes)); | ||||
|                 diesel::delete(expr).execute(conn)? | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if num_deleted <= 1 { | ||||
|             Ok(num_deleted) | ||||
|         } else { | ||||
|             Err(UnexpectedBehaviour(format!("{:?}", query), num_deleted)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Error)] | ||||
| pub enum DatabaseError { | ||||
|     #[error("The path {0:?} can not be converted to a UTF-8 String")] | ||||
|     InvalidPath(PathBuf), | ||||
|     #[error(transparent)] | ||||
|     Orm(#[from] diesel::result::Error), | ||||
|     #[error("Expected {0:?} to be present in the DB but it was not")] | ||||
|     MissingDatabaseEntry(DatabaseEntry), | ||||
|     #[error("The Query: \"{0}\" affected {1} database entries (it should only affect one)")] | ||||
|     UnexpectedBehaviour(String, usize), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum DatabaseEntry { | ||||
|     Location(GameSaveLocation), | ||||
|     File(GameFile), | ||||
| } | ||||
|  | ||||
| pub mod query { | ||||
|     use std::path::Path; | ||||
|     use uuid::Uuid; | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub enum GameSaveQuery<'a> { | ||||
|         Id(i32), | ||||
|         Uuid(Uuid), | ||||
|         FriendlyName(&'a str), | ||||
|         Path(&'a Path), | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub enum GameFileQuery<'a> { | ||||
|         Hash(u64), | ||||
|         Path(&'a Path), | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										19
									
								
								src/game.rs
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/game.rs
									
									
									
									
									
								
							| @@ -14,8 +14,8 @@ const XXHASH64_SEED: u64 = 1337; | ||||
| pub struct GameSaveLocation { | ||||
|     pub friendly_name: Option<String>, | ||||
|     pub original_path: PathBuf, | ||||
|     files: Vec<GameFile>, | ||||
|     uuid: Uuid, | ||||
|     pub files: Vec<GameFile>, | ||||
|     pub uuid: Uuid, | ||||
| } | ||||
|  | ||||
| impl GameSaveLocation { | ||||
| @@ -69,7 +69,7 @@ impl GameFile { | ||||
|     /// # Examples | ||||
|     /// ``` | ||||
|     /// # use save_sync::game::GameFile; | ||||
|     /// let path = "/home/user/Documents/some_company/some_game/saves"; | ||||
|     /// let path = "/home/user/Documents/some_company/some_game/saves/save01.sav"; | ||||
|     /// match GameFile::new(path) { | ||||
|     ///     Ok(_) => { /* Do something with the file  */ } | ||||
|     ///     Err(err) => { eprintln!("Error while attempting to calculate the hash of {}", path)} | ||||
| @@ -99,19 +99,6 @@ pub enum GameFileError { | ||||
|     IoError(#[from] std::io::Error), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| struct BackupPath { | ||||
|     inner: Option<PathBuf>, | ||||
| } | ||||
|  | ||||
| impl BackupPath { | ||||
|     fn new<P: AsRef<Path>>(path: P) -> Self { | ||||
|         Self { | ||||
|             inner: Some(path.as_ref().to_path_buf()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct HashWriter<T: Hasher>(T); | ||||
|  | ||||
| impl<T: Hasher> Write for HashWriter<T> { | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| #[macro_use] | ||||
| extern crate diesel; | ||||
| #[macro_use] | ||||
| extern crate diesel_migrations; | ||||
|  | ||||
| pub mod db; | ||||
| pub mod game; | ||||
|   | ||||
							
								
								
									
										114
									
								
								src/models.rs
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								src/models.rs
									
									
									
									
									
								
							| @@ -1,8 +1,116 @@ | ||||
| use super::db::query::GameSaveQuery; | ||||
| use super::db::Database; | ||||
| use super::game::{GameFile, GameSaveLocation}; | ||||
| use super::schema::{game_file, game_save_location}; | ||||
| use std::convert::TryInto; | ||||
| use std::path::PathBuf; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| #[derive(Queryable)] | ||||
| pub struct GameFile { | ||||
| pub struct DbGameFile { | ||||
|     pub id: i32, | ||||
|     pub original_path: PathBuf, | ||||
|     pub file_hash: u64, | ||||
|     pub original_path: String, | ||||
|     pub file_hash: Vec<u8>, | ||||
|     pub game_save_id: i32, | ||||
| } | ||||
|  | ||||
| impl From<DbGameFile> for GameFile { | ||||
|     fn from(db_file: DbGameFile) -> Self { | ||||
|         let original_path = PathBuf::from(db_file.original_path); | ||||
|         let hash_bytes: [u8; 8] = db_file | ||||
|             .file_hash | ||||
|             .try_into() | ||||
|             .expect("GameFile hash from DB was not 8 bytes long"); | ||||
|         let hash = u64::from_be_bytes(hash_bytes); | ||||
|  | ||||
|         Self { | ||||
|             original_path, | ||||
|             hash, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&DbGameFile> for GameFile { | ||||
|     fn from(db_file: &DbGameFile) -> Self { | ||||
|         let original_path = PathBuf::from(&db_file.original_path); | ||||
|         let buf: &[u8] = db_file.file_hash.as_ref(); | ||||
|         let hash_bytes: [u8; 8] = buf | ||||
|             .try_into() | ||||
|             .expect("GameFile hash from DB was not 8 bytes long"); | ||||
|         let hash = u64::from_be_bytes(hash_bytes); | ||||
|  | ||||
|         Self { | ||||
|             original_path, | ||||
|             hash, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Insertable)] | ||||
| #[table_name = "game_file"] | ||||
| pub struct NewGameFile<'a> { | ||||
|     pub original_path: &'a str, | ||||
|     pub file_hash: &'a [u8], | ||||
|     pub game_save_id: i32, | ||||
| } | ||||
|  | ||||
| #[derive(Queryable)] | ||||
| pub struct DbGameSaveLocation { | ||||
|     pub id: i32, | ||||
|     pub friendly_name: Option<String>, | ||||
|     pub original_path: String, | ||||
|     pub uuid: Vec<u8>, | ||||
| } | ||||
|  | ||||
| impl From<DbGameSaveLocation> for GameSaveLocation { | ||||
|     fn from(db_save_loc: DbGameSaveLocation) -> Self { | ||||
|         // FIXME: This makes .into() and ::from() rather resource intensive | ||||
|         // This isn't that intuitive. It might be best to abandon the From<> traits here | ||||
|         // and go for dedicated methods that express how much the database is read from here | ||||
|         let original_path = PathBuf::from(db_save_loc.original_path); | ||||
|  | ||||
|         let uuid = | ||||
|             Uuid::from_slice(&db_save_loc.uuid).expect("UUIDv4 from Database was not 16bytes long"); | ||||
|         let conn = super::db::establish_connection(); | ||||
|         let files = Database::get_game_files(&conn, GameSaveQuery::Id(db_save_loc.id)) | ||||
|             .expect("Failed to Interact w/ the Database") | ||||
|             .expect("Turn this into a TryFrom Please"); | ||||
|  | ||||
|         Self { | ||||
|             friendly_name: db_save_loc.friendly_name, | ||||
|             original_path, | ||||
|             files, | ||||
|             uuid, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&DbGameSaveLocation> for GameSaveLocation { | ||||
|     fn from(db_save_loc: &DbGameSaveLocation) -> Self { | ||||
|         // FIXME: See  From<DbGameSaveLocation> for GameSaveLocation | ||||
|         let original_path = PathBuf::from(&db_save_loc.original_path); | ||||
|  | ||||
|         // FIXME: Is it reasonable to assume that this will **never** fail? | ||||
|         let uuid = | ||||
|             Uuid::from_slice(&db_save_loc.uuid).expect("UUIDv4 from Database was not 16bytes long"); | ||||
|         let conn = super::db::establish_connection(); | ||||
|         let files = Database::get_game_files(&conn, GameSaveQuery::Id(db_save_loc.id)) | ||||
|             .expect("Failed to interact w/ the Database") | ||||
|             .expect("Turn this into a TryFrom please"); | ||||
|  | ||||
|         Self { | ||||
|             friendly_name: db_save_loc.friendly_name.clone(), | ||||
|             original_path, | ||||
|             files, | ||||
|             uuid, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Insertable)] | ||||
| #[table_name = "game_save_location"] | ||||
| pub struct NewGameSaveLocation<'a> { | ||||
|     pub friendly_name: Option<&'a str>, | ||||
|     pub original_path: &'a str, | ||||
|     pub uuid: &'a [u8], | ||||
| } | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| table! { | ||||
|     game_file (id) { | ||||
|         id -> Nullable<Integer>, | ||||
|         original_path -> Binary, | ||||
|         file_hash -> Integer, | ||||
|         id -> Integer, | ||||
|         original_path -> Text, | ||||
|         file_hash -> Binary, | ||||
|         game_save_id -> Integer, | ||||
|     } | ||||
| } | ||||
|  | ||||
| table! { | ||||
|     game_save_location (id) { | ||||
|         id -> Nullable<Integer>, | ||||
|         friendly_name -> Text, | ||||
|         id -> Integer, | ||||
|         friendly_name -> Nullable<Text>, | ||||
|         original_path -> Text, | ||||
|         uuid -> Binary, | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user