feat: Implement list info, and drop commands

Save Sync now has persistent storage. Currently, you can add new Saves,
Remove them, and get info about them. The part of CRUD that is remaining
is Update. Documentation needs to be written, a lot of the public API
changed.
This commit is contained in:
Rekai Nyangadzayi Musuka 2021-03-02 20:21:57 -06:00
parent ca934e370d
commit 125bc24ed0
10 changed files with 702 additions and 83 deletions

View File

@ -14,6 +14,7 @@ uuid = { version = "^0.8", features = ["v4"] }
twox-hash = "^1.6" twox-hash = "^1.6"
thiserror = "^1.0" thiserror = "^1.0"
diesel = { version = "^1.4", features = ["sqlite"] } diesel = { version = "^1.4", features = ["sqlite"] }
diesel_migrations = "^1.4"
dotenv = "^0.15" dotenv = "^0.15"
[dev-dependencies] [dev-dependencies]

View File

@ -1,5 +1,6 @@
use crate::utils::{self, ProjectDirError}; use crate::utils::{self, ProjectDirError};
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use save_sync::db::{establish_connection, query::GameSaveQuery, Database};
use save_sync::game::{GameFile, GameSaveLocation}; use save_sync::game::{GameFile, GameSaveLocation};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
@ -63,7 +64,9 @@ impl Archive {
config_root: config_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 /// 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 /// TODO: Add note here about how GameSaveLocation, GameFile and tracking individual files rather than directories work
@ -74,7 +77,7 @@ impl Archive {
/// # Examples /// # Examples
/// ``` /// ```
/// # use client::archive::Archive; /// # 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 save_path = "/home/user/Documents/generic_company/generic_game/save_folder";
/// match archive.track_game(save_path) { /// match archive.track_game(save_path) {
/// Ok(_) => println!("Save Sync is now tracking {}", save_path), /// Ok(_) => println!("Save Sync is now tracking {}", save_path),
@ -83,6 +86,9 @@ impl Archive {
/// ``` /// ```
pub fn track_game<P: AsRef<Path>>(&mut self, path: P) -> Result<(), GameTrackError> { pub fn track_game<P: AsRef<Path>>(&mut self, path: P) -> Result<(), GameTrackError> {
let game_save_loc = self.get_game_save_files(path, None)?; 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); self.tracked_games.push(game_save_loc);
Ok(()) Ok(())
} }
@ -97,7 +103,7 @@ impl Archive {
/// # Examples /// # Examples
/// ``` /// ```
/// # use client::archive::Archive; /// # 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 save_path = "/home/user/Documents/generic_company/generic_game/save_folder";
/// let friendly_name = "Generic Game"; /// let friendly_name = "Generic Game";
/// match archive.track_game_with_friendly(save_path, friendly_name) { /// match archive.track_game_with_friendly(save_path, friendly_name) {
@ -109,6 +115,9 @@ impl Archive {
P: AsRef<Path>, P: AsRef<Path>,
{ {
let game_save_loc = self.get_game_save_files(path, Some(name))?; 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); self.tracked_games.push(game_save_loc);
Ok(()) Ok(())
} }
@ -155,50 +164,50 @@ impl Archive {
let friendly_name = friendly_name.map(|s| s.to_owned()); let friendly_name = friendly_name.map(|s| s.to_owned());
Ok(GameSaveLocation::new(path, game_files, friendly_name)) 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(())
} }
/// Removes a game from the list of tracked games using the game's friendly name impl Archive {
/// pub fn drop_game<P: AsRef<Path>>(path: P) -> Result<Option<()>, GameDropError> {
/// Otherwise, is identical to [`Archive::drop_game`] let conn = establish_connection();
/// let query = GameSaveQuery::Path(path.as_ref());
/// # Arguments Ok(Database::drop_game_save(&conn, query)?)
/// * `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,
});
// TODO: Remove backup copy of game save location on disk pub fn drop_game_with_friendly(name: &str) -> Result<Option<()>, GameDropError> {
Ok(()) let conn = establish_connection();
let query = GameSaveQuery::FriendlyName(name);
Ok(Database::drop_game_save(&conn, query)?)
}
}
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 +217,8 @@ pub enum GameTrackError {
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("{0:?} is not a supported inode type (File System Object)")] #[error("{0:?} is not a supported inode type (File System Object)")]
UnknownFileSystemObject(PathBuf), // FIXME: Is there a better name for this? UnknownFileSystemObject(PathBuf), // FIXME: Is there a better name for this?
#[error(transparent)]
DatabaseError(#[from] save_sync::db::DatabaseError),
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -216,4 +227,12 @@ pub enum GameDropError {
UnknownFriendlyName(String), UnknownFriendlyName(String),
#[error("Unable to find game with the path {0:?}")] #[error("Unable to find game with the path {0:?}")]
UnknownPath(PathBuf), UnknownPath(PathBuf),
#[error(transparent)]
DatabaseError(#[from] save_sync::db::DatabaseError),
}
#[derive(Error, Debug)]
pub enum GameGetError {
#[error(transparent)]
DatabaseError(#[from] save_sync::db::DatabaseError),
} }

View File

@ -3,7 +3,9 @@ use clap::{App, Arg, SubCommand};
use client::archive::Archive; use client::archive::Archive;
use dotenv::dotenv; use dotenv::dotenv;
use log::{debug, info}; use log::{debug, info};
use save_sync::db::{establish_connection, Database};
use std::path::Path; use std::path::Path;
fn main() { fn main() {
dotenv().ok(); dotenv().ok();
env_logger::init(); env_logger::init();
@ -32,29 +34,135 @@ fn main() {
.help("A friendly name for a tracked file / directory"), .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(); .get_matches();
match m.subcommand() { 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"), _ => eprintln!("No valid subcommand / argument provided"),
} }
} }
fn track_path(matches: &ArgMatches) { fn track_save(matches: &ArgMatches) {
let path = Path::new(matches.value_of("path").unwrap()); let path_str = matches
let mut archive = Archive::try_default().expect("Failed to create an Archive struct"); .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") { 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 archive
.track_game_with_friendly(path, f_name) .track_game_with_friendly(path_str, f_name)
.expect("Archive failed to track Game Save Location") .expect("Archive failed to track Game Save Location")
} else { } else {
info!("No friendly name present for {:?}", path); info!("No friendly name present for {:?}", path_str);
archive archive
.track_game(path) .track_game(path_str)
.expect("Archive failed to track Game Save Location"); .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."),
}
} }

View File

@ -1,8 +1,8 @@
-- Your SQL goes here -- Your SQL goes here
CREATE TABLE game_file ( CREATE TABLE game_file (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY NOT NULL,
original_path BLOB NOT NULL, -- TEXT assumes a Unicode Encoding original_path TEXT NOT NULL, -- TEXT assumes a Unicode Encoding
file_hash INTEGER NOT NULL, -- u64, but that's not file_hash BLOB NOT NULL,
game_save_id INTEGER NOT NULL, game_save_id INTEGER NOT NULL,
FOREIGN KEY (game_save_id) REFERENCES game_save_location (id) FOREIGN KEY (game_save_id) REFERENCES game_save_location (id)
) )

View File

@ -1,7 +1,7 @@
-- Your SQL goes here -- Your SQL goes here
CREATE TABLE game_save_location ( CREATE TABLE game_save_location (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY NOT NULL,
friendly_name TEXT -- This can be null friendly_name TEXT, -- This can be null
original_PATH BLOB NOT NULL, original_path TEXT NOT NULL,
uuid BLOB NOT NULL uuid BLOB NOT NULL
) )

397
src/db.rs
View File

@ -1,7 +1,16 @@
use super::game::{GameFile, GameSaveLocation};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sqlite::SqliteConnection; use diesel::sqlite::SqliteConnection;
use diesel_migrations::embed_migrations;
use dotenv::dotenv; 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 /// 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 // or have establish_connection return a Result with a thiserror enum
let db_url = env::var("DATABASE_URL").expect("$DATABASE_URL was not set"); 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<GameFile> {
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::<DbGameFile>(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::<DbGameFile>(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<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) -> 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<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 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<GameSaveLocation> {
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::<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(|| InvalidPathError(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 = 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<i32> {
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::<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(|| InvalidPathError(path.to_path_buf()))?;
game_save_location
.select(id)
.filter(original_path.eq(path_str))
.load::<i32>(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<Vec<GameSaveLocation>> {
use super::models::DbGameSaveLocation;
use super::schema::game_save_location::dsl::game_save_location;
let db_save_locs = game_save_location
.load::<DbGameSaveLocation>(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),
}
} }

View File

@ -14,8 +14,8 @@ const XXHASH64_SEED: u64 = 1337;
pub struct GameSaveLocation { pub struct GameSaveLocation {
pub friendly_name: Option<String>, pub friendly_name: Option<String>,
pub original_path: PathBuf, pub original_path: PathBuf,
files: Vec<GameFile>, pub files: Vec<GameFile>,
uuid: Uuid, pub uuid: Uuid,
} }
impl GameSaveLocation { impl GameSaveLocation {
@ -69,7 +69,7 @@ impl GameFile {
/// # Examples /// # Examples
/// ``` /// ```
/// # use save_sync::game::GameFile; /// # 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) { /// match GameFile::new(path) {
/// Ok(_) => { /* Do something with the file */ } /// Ok(_) => { /* Do something with the file */ }
/// Err(err) => { eprintln!("Error while attempting to calculate the hash of {}", path)} /// Err(err) => { eprintln!("Error while attempting to calculate the hash of {}", path)}
@ -99,19 +99,6 @@ pub enum GameFileError {
IoError(#[from] std::io::Error), 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); struct HashWriter<T: Hasher>(T);
impl<T: Hasher> Write for HashWriter<T> { impl<T: Hasher> Write for HashWriter<T> {

View File

@ -1,5 +1,7 @@
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
pub mod db; pub mod db;
pub mod game; pub mod game;

View File

@ -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 std::path::PathBuf;
use uuid::Uuid;
#[derive(Queryable)] #[derive(Queryable)]
pub struct GameFile { pub struct DbGameFile {
pub id: i32, pub id: i32,
pub original_path: PathBuf, pub original_path: String,
pub file_hash: u64, 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],
} }

View File

@ -1,16 +1,17 @@
table! { table! {
game_file (id) { game_file (id) {
id -> Nullable<Integer>, id -> Integer,
original_path -> Binary, original_path -> Text,
file_hash -> Integer, file_hash -> Binary,
game_save_id -> Integer, game_save_id -> Integer,
} }
} }
table! { table! {
game_save_location (id) { game_save_location (id) {
id -> Nullable<Integer>, id -> Integer,
friendly_name -> Text, friendly_name -> Nullable<Text>,
original_path -> Text,
uuid -> Binary, uuid -> Binary,
} }
} }