Compare commits

...

4 Commits
main ... dev

Author SHA1 Message Date
Rekai Nyangadzayi Musuka f098430a78 chore: clean up warnings and public api 2021-03-02 21:40:09 -06:00
Rekai Nyangadzayi Musuka bf2336386e feat(db): Ensure no duplicate data in the DB 2021-03-02 21:39:16 -06:00
Rekai Nyangadzayi Musuka a0357b3ee3 fix(client): Remove tracked_files property
Maybe there might be value in looking to caching values from the
database, but as of right now it's complexity that doesn't really makes
sense since the program never lives long enough for us to ever use the
cache.
2021-03-02 20:32:24 -06:00
Rekai Nyangadzayi Musuka 125bc24ed0 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.
2021-03-02 20:21:57 -06:00
11 changed files with 720 additions and 113 deletions

View File

@ -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]

View File

@ -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),
}

View File

@ -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!(),
}
}

View File

@ -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)
}

View File

@ -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)
)

View File

@ -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
View File

@ -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),
}
}

View File

@ -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> {

View File

@ -1,5 +1,7 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
pub mod db;
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 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],
}

View File

@ -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,
}
}