diff --git a/Cargo.toml b/Cargo.toml index 3d642ea..4b23850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/client/src/archive.rs b/client/src/archive.rs index f365b91..eb89d29 100644 --- a/client/src/archive.rs +++ b/client/src/archive.rs @@ -1,5 +1,6 @@ use crate::utils::{self, ProjectDirError}; use log::{debug, info, trace, warn}; +use save_sync::db::{establish_connection, query::GameSaveQuery, Database}; use save_sync::game::{GameFile, GameSaveLocation}; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -63,7 +64,9 @@ impl Archive { 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 +77,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,6 +86,9 @@ impl Archive { /// ``` pub fn track_game>(&mut self, path: P) -> Result<(), GameTrackError> { let game_save_loc = self.get_game_save_files(path, None)?; + + Self::write_game_save(&game_save_loc); + self.tracked_games.push(game_save_loc); Ok(()) } @@ -97,7 +103,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,6 +115,9 @@ impl Archive { P: AsRef, { let game_save_loc = self.get_game_save_files(path, Some(name))?; + + Self::write_game_save(&game_save_loc); + self.tracked_games.push(game_save_loc); Ok(()) } @@ -155,50 +164,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>(&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>(path: P) -> Result, 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, 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

(path: P) -> Result, GameGetError> + where + P: AsRef, + { + 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, 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>, 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 +217,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 +227,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), } diff --git a/client/src/main.rs b/client/src/main.rs index a62840b..8d85da5 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -3,7 +3,9 @@ use clap::{App, Arg, SubCommand}; use client::archive::Archive; use dotenv::dotenv; use log::{debug, info}; +use save_sync::db::{establish_connection, Database}; use std::path::Path; + fn main() { dotenv().ok(); env_logger::init(); @@ -32,29 +34,135 @@ 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") + }; + + let game = maybe_game.expect("No tracked game save found"); + + 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!(); + } +} + +fn list_tracked_saves(_matches: &ArgMatches) { + let games = Archive::get_all_games() + .expect("Failed to get all Games from the Archive") + .expect("There are no tracked Games"); + + for game in games { + if let Some(name) = game.friendly_name { + print!("[{}] ", name); + } + println!("{:?}", game.original_path); + println!("UUID: {:?}", game.uuid); + println!("---"); + } +} + +fn drop_save(matches: &ArgMatches) { + let maybe_compromised = 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 maybe_compromised { + Some(()) => println!("Game successfully dropped from the list"), + None => panic!("Database Invariant broken. Database is corrupted."), + } } diff --git a/migrations/2021-03-02-022949_create_game_file_table/up.sql b/migrations/2021-03-02-022949_create_game_file_table/up.sql index b4d4faa..d98b898 100644 --- a/migrations/2021-03-02-022949_create_game_file_table/up.sql +++ b/migrations/2021-03-02-022949_create_game_file_table/up.sql @@ -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, -- TEXT assumes a Unicode Encoding + file_hash BLOB NOT NULL, game_save_id INTEGER NOT NULL, FOREIGN KEY (game_save_id) REFERENCES game_save_location (id) ) \ No newline at end of file diff --git a/migrations/2021-03-02-025744_create_game_save_loc_table/up.sql b/migrations/2021-03-02-025744_create_game_save_loc_table/up.sql index 2efd290..5b4d873 100644 --- a/migrations/2021-03-02-025744_create_game_save_loc_table/up.sql +++ b/migrations/2021-03-02-025744_create_game_save_loc_table/up.sql @@ -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, + id INTEGER PRIMARY KEY NOT NULL, + friendly_name TEXT, -- This can be null + original_path TEXT NOT NULL, uuid BLOB NOT NULL ) \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index 5a8476e..117a464 100644 --- a/src/db.rs +++ b/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 = std::result::Result; +pub type ResultantOption = std::result::Result, DatabaseError>; + +embed_migrations!("./migrations"); /// Establishes a DB Connection with a Sqlite Database /// @@ -21,5 +30,389 @@ 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::InvalidPathError; + + let hash_bytes: [u8; 8] = file.hash.to_be_bytes(); + let path = &file.original_path; + let path_str = path + .to_str() + .ok_or_else(|| InvalidPathError(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 { + use super::models::DbGameFile; + use super::schema::game_file::dsl::{file_hash, game_file, original_path}; + use DatabaseError::InvalidPathError; + + 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::(conn)? + } + GameFileQuery::Path(path) => { + let path_str = path + .to_str() + .ok_or_else(|| InvalidPathError(path.to_path_buf()))?; + game_file + .filter(original_path.eq(path_str)) + .load::(conn)? + } + }; + + let maybe_game_files = if game_files.len() == 1 { + Some((&game_files[0]).into()) + } else { + None + }; + + Ok(maybe_game_files) + } +} + +// Get GameFiles Implementations +impl Database { + pub fn get_game_files( + conn: &SqliteConnection, + query: GameSaveQuery, + ) -> ResultantOption> { + 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> { + 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::(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) -> ResultantOption<()> { + use super::schema::game_file::dsl::{file_hash, game_file, original_path}; + use DatabaseError::InvalidPathError; + + 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(|| InvalidPathError(path.to_path_buf()))?; + let expr = game_file.filter(original_path.eq(path_str)); + diesel::delete(expr).execute(conn)? + } + }; + + Ok(if num_deleted == 1 { Some(()) } else { None }) + } +} + +// 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 = 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 DatabaseEntry::Location; + use DatabaseError::{InvalidPathError, MissingDatabaseEntry}; + + let path = &save_loc.original_path; + let original_path = path + .to_str() + .ok_or_else(|| InvalidPathError(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 maybe_id = Self::get_game_save_id(conn, GameSaveQuery::Uuid(save_loc.uuid))?; + + match maybe_id { + Some(id) => { + // Write all the GameFiles into the database + for game_file in &save_loc.files { + Self::write_game_file(conn, game_file, id)?; + } + } + None => return Err(MissingDatabaseEntry(Location(save_loc.clone()))), + } + + Ok(()) + } +} + +// Get GameSaveLocation Implementations +impl Database { + pub fn get_game_save( + conn: &SqliteConnection, + query: GameSaveQuery, + ) -> ResultantOption { + use super::models::DbGameSaveLocation; + use super::schema::game_save_location::dsl::{ + friendly_name, game_save_location, id, original_path, uuid, + }; + use DatabaseError::InvalidPathError; + + let save_locs = match query { + GameSaveQuery::Id(needle) => game_save_location + .filter(id.eq(needle)) + .load::(conn)?, + GameSaveQuery::FriendlyName(name) => game_save_location + .filter(friendly_name.eq(name)) + .load::(conn)?, + GameSaveQuery::Path(path) => { + let path_str = path + .to_str() + .ok_or_else(|| InvalidPathError(path.to_path_buf()))?; + game_save_location + .filter(original_path.eq(path_str)) + .load::(conn)? + } + GameSaveQuery::Uuid(uuid_value) => { + let uuid_bytes: &[u8] = uuid_value.as_bytes(); + game_save_location + .filter(uuid.eq(uuid_bytes)) + .load::(conn)? + } + }; + + let maybe_save_loc = if save_locs.len() == 1 { + Some((&save_locs[0]).into()) + } else { + None + }; + + Ok(maybe_save_loc) + } + + pub fn get_game_save_id(conn: &SqliteConnection, query: GameSaveQuery) -> ResultantOption { + use super::schema::game_save_location::dsl::{ + friendly_name, game_save_location, id, original_path, uuid, + }; + use DatabaseError::InvalidPathError; + + 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::(conn)? + } + GameSaveQuery::FriendlyName(name) => game_save_location + .select(id) + .filter(friendly_name.eq(name)) + .load::(conn)?, + GameSaveQuery::Path(path) => { + let path_str = path + .to_str() + .ok_or_else(|| InvalidPathError(path.to_path_buf()))?; + game_save_location + .select(id) + .filter(original_path.eq(path_str)) + .load::(conn)? + } + }; + + // FIXME: Is there are more ergonomic way of doing this? + Ok(if ids.len() == 1 { Some(ids[0]) } else { None }) + } + + pub fn get_all_game_saves(conn: &SqliteConnection) -> ResultantOption> { + use super::models::DbGameSaveLocation; + use super::schema::game_save_location::dsl::game_save_location; + + let db_save_locs = game_save_location + .load::(conn)? + .iter() + .map(GameSaveLocation::from) + .collect(); + + Ok(Some(db_save_locs)) + } +} + +// Drop GameSaveLocation Implementations +impl Database { + pub fn drop_game_save(conn: &SqliteConnection, query: GameSaveQuery) -> ResultantOption<()> { + use super::schema::game_save_location::dsl::{ + friendly_name, game_save_location, id, original_path, uuid, + }; + use DatabaseError::InvalidPathError; + + 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(|| InvalidPathError(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)? + } + }; + + // FIXME: Is there are more ergonomic way of doing this? + Ok(if num_deleted == 1 { Some(()) } else { None }) + } +} + +#[derive(Debug, Error)] +pub enum DatabaseError { + #[error("The path {0:?} can not be converted to a UTF-8 String")] + InvalidPathError(PathBuf), + #[error(transparent)] + OrmError(#[from] diesel::result::Error), + #[error("Expected {0:?} to be present in the DB but it was not")] + MissingDatabaseEntry(DatabaseEntry), +} + +#[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), + } } diff --git a/src/game.rs b/src/game.rs index 6a37593..286d40c 100644 --- a/src/game.rs +++ b/src/game.rs @@ -14,8 +14,8 @@ const XXHASH64_SEED: u64 = 1337; pub struct GameSaveLocation { pub friendly_name: Option, pub original_path: PathBuf, - files: Vec, - uuid: Uuid, + pub files: Vec, + 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, -} - -impl BackupPath { - fn new>(path: P) -> Self { - Self { - inner: Some(path.as_ref().to_path_buf()), - } - } -} - struct HashWriter(T); impl Write for HashWriter { diff --git a/src/lib.rs b/src/lib.rs index a94397d..c62b49c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #[macro_use] extern crate diesel; +#[macro_use] +extern crate diesel_migrations; pub mod db; pub mod game; diff --git a/src/models.rs b/src/models.rs index 91ad98a..7d50605 100644 --- a/src/models.rs +++ b/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, + pub game_save_id: i32, +} + +impl From 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, + pub original_path: String, + pub uuid: Vec, +} + +impl From 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 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], } diff --git a/src/schema.rs b/src/schema.rs index 549c02f..ba9d3bc 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,16 +1,17 @@ table! { game_file (id) { - id -> Nullable, - 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, - friendly_name -> Text, + id -> Integer, + friendly_name -> Nullable, + original_path -> Text, uuid -> Binary, } }