Compare commits
4 Commits
Author | SHA1 | Date |
---|---|---|
Rekai Nyangadzayi Musuka | f098430a78 | |
Rekai Nyangadzayi Musuka | bf2336386e | |
Rekai Nyangadzayi Musuka | a0357b3ee3 | |
Rekai Nyangadzayi Musuka | 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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue