Compare commits

7 Commits
dev ... main

Author SHA1 Message Date
56575b62ed feat: add barebones README
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-29 23:34:33 -05:00
cb7a72cece chore: fix merge issues 2021-03-01 23:06:18 -06:00
864258cc01 Merge branch 'dev' 2021-03-01 22:57:22 -06:00
ce6bcd40f2 chore: remove --verbose from CI build & test config 2021-03-01 22:06:23 -06:00
2874d290eb fix: ensure archive doctests pass 2021-03-01 22:04:57 -06:00
1a6dac03d5 chore: remove unnecessary commands from CI config 2021-03-01 21:38:23 -06:00
de801870dd chore: add CI build config 2021-03-01 21:36:06 -06:00
12 changed files with 120 additions and 720 deletions

View File

@@ -14,7 +14,6 @@ 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]

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# Save Sync
[![Build Status](https://ci.paoda.moe/api/badges/paoda/save-sync/status.svg)](https://ci.paoda.moe/paoda/save-sync)
TODO: Fill out this README and resume development of this project

View File

@@ -1,6 +1,5 @@
use crate::utils::{self, ProjectDirError};
use log::debug;
use save_sync::db::{establish_connection, query::GameSaveQuery, Database};
use log::{debug, info, trace, warn};
use save_sync::game::{GameFile, GameSaveLocation};
use std::path::{Path, PathBuf};
use thiserror::Error;
@@ -8,6 +7,7 @@ use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct Archive {
tracked_games: Vec<GameSaveLocation>,
data_root: PathBuf,
config_root: PathBuf,
}
@@ -31,6 +31,7 @@ impl Archive {
debug!("Created default Archive with: {:?} and {:?}", data, config);
Ok(Self {
tracked_games: Vec::new(),
data_root: data,
config_root: config,
})
@@ -57,13 +58,12 @@ 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().expect("Failed to create an Archive");
/// let mut archive = Archive::try_default().unwrap();
/// 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::write_game_save(&game_save_loc)?;
self.tracked_games.push(game_save_loc);
Ok(())
}
@@ -97,7 +97,7 @@ impl Archive {
/// # Examples
/// ```
/// # use client::archive::Archive;
/// let mut archive = Archive::try_default().expect("Failed to create an Archive");
/// let mut archive = Archive::try_default().unwrap();
/// 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::write_game_save(&game_save_loc)?;
self.tracked_games.push(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)?)
}
}
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)?)
// TODO: Remove backup copy of game save location on disk
Ok(())
}
}
@@ -208,8 +208,6 @@ 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)]
@@ -218,12 +216,4 @@ 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::info;
use log::{debug, info};
use std::path::Path;
fn main() {
dotenv().ok();
env_logger::init();
@@ -32,142 +32,29 @@ 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_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),
("track", Some(sub_m)) => track_path(sub_m),
_ => eprintln!("No valid subcommand / argument provided"),
}
}
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");
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");
if let Some(f_name) = matches.value_of("friendly") {
info!("Name {} present for {:?}", f_name, path_str);
info!("Name {} present for {:?}", f_name, path);
archive
.track_game_with_friendly(path_str, f_name)
.track_game_with_friendly(path, f_name)
.expect("Archive failed to track Game Save Location")
} else {
info!("No friendly name present for {:?}", path_str);
info!("No friendly name present for {:?}", path);
archive
.track_game(path_str)
.track_game(path)
.expect("Archive failed to track Game Save Location");
}
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!(),
}
info!("Now Tracking: {:?}", path);
}

View File

@@ -1,6 +1,27 @@
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 NOT NULL,
original_path TEXT NOT NULL UNIQUE, -- TEXT assumes a Unicode Encoding
file_hash BLOB NOT NULL UNIQUE,
id INTEGER PRIMARY KEY,
original_path BLOB NOT NULL, -- TEXT assumes a Unicode Encoding
file_hash INTEGER NOT NULL, -- u64, but that's not
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 NOT NULL,
friendly_name TEXT UNIQUE, -- This can be null
original_path TEXT NOT NULL UNIQUE,
uuid BLOB NOT NULL UNIQUE
id INTEGER PRIMARY KEY,
friendly_name TEXT -- This can be null
original_PATH BLOB NOT NULL,
uuid BLOB NOT NULL
)

410
src/db.rs
View File

@@ -1,16 +1,7 @@
use super::game::{GameFile, GameSaveLocation};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel_migrations::embed_migrations;
use dotenv::dotenv;
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");
use std::env;
/// Establishes a DB Connection with a Sqlite Database
///
@@ -30,402 +21,5 @@ 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");
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),
}
SqliteConnection::establish(&db_url).expect(&format!("Error connecting to {}", db_url))
}

View File

@@ -14,8 +14,8 @@ const XXHASH64_SEED: u64 = 1337;
pub struct GameSaveLocation {
pub friendly_name: Option<String>,
pub original_path: PathBuf,
pub files: Vec<GameFile>,
pub uuid: Uuid,
files: Vec<GameFile>,
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/save01.sav";
/// let path = "/home/user/Documents/some_company/some_game/saves";
/// match GameFile::new(path) {
/// Ok(_) => { /* Do something with the file */ }
/// Err(err) => { eprintln!("Error while attempting to calculate the hash of {}", path)}
@@ -99,6 +99,19 @@ 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,7 +1,5 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
pub mod db;
pub mod game;

View File

@@ -1,116 +1,8 @@
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 DbGameFile {
pub struct GameFile {
pub id: i32,
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],
pub original_path: PathBuf,
pub file_hash: u64,
}

View File

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