From 001efa8510ec057d52ad08248a9a6af799c1da8a Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Mon, 1 Mar 2021 18:58:11 -0600 Subject: [PATCH] chore: work on project structure --- .env.example | 1 + .gitignore | 2 + Cargo.toml | 3 + client/Cargo.toml | 4 +- client/src/archive.rs | 192 +++++++++++++++++++----------------------- client/src/main.rs | 60 +++++++++++++ server/Cargo.toml | 3 + server/src/main.rs | 31 +++++++ src/game.rs | 89 ++++++++++++++++++++ src/lib.rs | 2 + 10 files changed, 281 insertions(+), 106 deletions(-) create mode 100644 .env.example create mode 100644 client/src/main.rs create mode 100644 server/src/main.rs create mode 100644 src/game.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c0a1380 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +RUST_LOG = \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96ef6c0..9b25202 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target Cargo.lock +/.env +/.vscode diff --git a/Cargo.toml b/Cargo.toml index acf47a9..1517063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,8 @@ members = ["server", "client"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +uuid = { version = "^0.8", features = ["v4"] } +twox-hash = "^1.6" +thiserror = "^1.0" [dev-dependencies] diff --git a/client/Cargo.toml b/client/Cargo.toml index afb67a7..de252f8 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -12,6 +12,8 @@ thiserror = "^1.0" walkdir = "^2.0" directories = "^3.0" log = "^0.4" +clap = "^2.33" +env_logger = "^0.8" +dotenv = "^0.15" [dev-dependencies] -vfs = "0.4.0" diff --git a/client/src/archive.rs b/client/src/archive.rs index d941016..143773a 100644 --- a/client/src/archive.rs +++ b/client/src/archive.rs @@ -1,14 +1,15 @@ use crate::utils::{self, ProjectDirError}; use log::{debug, info, trace, warn}; +use save_sync::game::{GameFile, GameSaveLocation}; use std::path::{Path, PathBuf}; use thiserror::Error; use walkdir::WalkDir; #[derive(Debug, Clone)] pub struct Archive { + tracked_games: Vec, data_root: PathBuf, config_root: PathBuf, - tracked_files: Vec, } impl Archive { @@ -23,20 +24,24 @@ impl Archive { /// let archive_res = Archive::try_default(); /// ``` pub fn try_default() -> Result { - let root = utils::get_project_dirs()?; - let data = root.data_dir().to_path_buf(); - let config = root.config_dir().to_path_buf(); + let user = utils::get_project_dirs()?; + let data = user.data_dir().to_path_buf(); + let config = user.config_dir().to_path_buf(); debug!("Created default Archive with: {:?} and {:?}", data, config); Ok(Self { + tracked_games: Vec::new(), data_root: data, config_root: config, - tracked_files: Vec::new(), }) } - /// Returns a new instance of Archive + /// Creates a new instance of Archive + /// + /// # Arguments + /// * `data_root` - Path to the user application data folder + /// * `config_root` - Path to the user configuration data folder. /// /// # Examples /// ``` @@ -53,149 +58,126 @@ impl Archive { ); Self { + tracked_games: Vec::new(), data_root: data_root.to_path_buf(), config_root: config_root.to_path_buf(), - tracked_files: Vec::new(), } } - /// Adds a file to the list of tracked files + /// Adds a path and it's contents to the list of tracked game files /// - /// Will fail if: - /// * `path` is not a file - /// * `path` is already tracked + /// TODO: Add note here about how GameSaveLocation, GameFile and tracking individual files rather than directories work + /// + /// # Arguments + /// * `path` - The path which will be tracked along with any children it may have /// /// # Examples /// ``` /// # use crate::client::archive::Archive; - /// let mut archive = Archive::try_default().unwrap(); - /// let track_result = archive.track_file("/home/user/Documents/game/0001.sav"); + /// let archive = Archive::try_default() + /// archive.track_game("/home/user/Documents/generic_company/generic_game/save_folder") /// ``` - pub fn track_file>(&mut self, path: P) -> Result<(), ArchiveAddError> { - let path = path.as_ref(); - - if path.is_file() { - if !self.tracked_files.iter().any(|buf| buf == path) { - self.tracked_files.push(path.to_path_buf()); - trace!("Added {:?} to list of tracked files", path); - Ok(()) - } else { - warn!("{:?} is already a tracked file", path); - Err(ArchiveAddError::AlreadyTracked(path.to_path_buf())) - } - } else { - warn!("{:?} was not tracked since it is not a file", path); - Err(ArchiveAddError::InvalidFilePath(path.to_path_buf())) - } + pub fn track_game>(&mut self, path: P) -> Result<(), GameTrackError> { + let game_save_loc = self.get_game_save_files(path, None)?; + self.tracked_games.push(game_save_loc); + Ok(()) } - /// Recursively adds all files in a directory to the list of tracked files - /// - /// Will fail if: - /// * `path` is not a directory - /// * The recursive directory search prematurely fails - /// - pub fn track_directory>(&mut self, path: &P) -> Result<(), ArchiveAddError> { + pub fn track_game_with_friendly

(&mut self, path: P, name: &str) -> Result<(), GameTrackError> + where + P: AsRef, + { + let game_save_loc = self.get_game_save_files(path, Some(name))?; + self.tracked_games.push(game_save_loc); + Ok(()) + } + + fn get_game_save_files

( + &mut self, + path: P, + friendly_name: Option<&str>, + ) -> Result + where + P: AsRef, + { + use GameTrackError::*; + let path = path.as_ref(); + let mut game_files: Vec = Vec::new(); if path.is_dir() { for maybe_entry in WalkDir::new(path) { match maybe_entry { Ok(entry) => { - if let Err(add_error) = self.track_file(entry.path()) { - match add_error { - ArchiveAddError::AlreadyTracked(_) => {} - ArchiveAddError::InvalidFilePath(_) => {} - err => return Err(err), - } - } + let game_file = GameFile::new(entry.path())?; + game_files.push(game_file); } Err(err) => { - warn!("WalkDir failed while recursively scanning {:?}", path); - return Err(ArchiveAddError::WalkDirError(err)); + let io_err: std::io::Error = err.into(); + return Err(io_err.into()); } - }; + } } - - Ok(()) + } else if path.is_file() { + // We've been requested to track an individual file. + todo!("Implement the ability to track a single file instead of a directory") } else { - warn!( - "{:?} is not a directory, so the contents were ignored", - path - ); - Err(ArchiveAddError::InvalidDirectoryPath(path.to_path_buf())) + return Err(UnknownFileSystemObject(path.to_path_buf())); } + + // FIXME: There most likely is a function that does this (check clippy) + let friendly_name = friendly_name.map(|s| s.to_owned()); + Ok(GameSaveLocation::new(game_files, friendly_name)) } - /// Removes a file from the list of tracked files + /// Removes a game from the list of traked games /// /// Will fail if: - /// * The file to be removed was not tracked - /// * the path provided is not a file + /// * 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 crate::client::archive::Archive; /// let mut archive = Archive::try_default().unwrap(); - /// archive.track_file("/home/user/Documents/game/0001.sav").unwrap(); - /// archive.drop_file("/home/user/Documents/game/0001.sav").unwrap(); + /// let drop_res = archive.drop_game("/home/user/Documents/generic_company/generic_game/save_folder"); /// ``` - pub fn drop_file>(&mut self, path: P) -> Result<(), ArchiveDropError> { - let path = path.as_ref(); - - if path.is_file() { - match self.tracked_files.iter().position(|buf| path == buf) { - Some(index) => { - self.tracked_files.remove(index); - Ok(()) - } - None => return Err(ArchiveDropError::FileNotTracked(path.to_path_buf())), - } - } else { - Err(ArchiveDropError::InvalidFilePath(path.to_path_buf())) - } + pub fn drop_game>(&mut self, path: P) -> Result<(), GameDropError> { + unimplemented!() } -} -impl Archive { - fn add_file(path: &Path) -> Result { - // Create Local Copy of file + /// 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 crate::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> { unimplemented!() } } #[derive(Error, Debug)] -pub enum ArchiveError { +pub enum GameTrackError { #[error(transparent)] - IOError(#[from] std::io::Error), + 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? } #[derive(Error, Debug)] -enum LocalBackupError {} - -#[derive(Error, Debug)] -pub enum ArchiveDropError { - #[error("{0:?} was not a file")] - InvalidFilePath(PathBuf), - #[error("{0:?} was not a directory")] - InvalidDirectoryPath(PathBuf), - #[error("{0:?} is not tracked by save-sync")] - FileNotTracked(PathBuf), -} - -#[derive(Error, Debug)] -pub enum ArchiveAddError { - #[error("{0:?} was not a file")] - InvalidFilePath(PathBuf), - #[error("{0:?} was not a directory")] - InvalidDirectoryPath(PathBuf), - #[error("{0:?} is already tracked")] - AlreadyTracked(PathBuf), - #[error(transparent)] - WalkDirError(#[from] walkdir::Error), -} - -#[cfg(test)] -mod tests { - use super::*; +pub enum GameDropError { + #[error("Unable to find Game with the name {0}")] + UnknownFriendlyName(String), + #[error("Unable to find game with the path {0:?}")] + UnknownPath(PathBuf), } diff --git a/client/src/main.rs b/client/src/main.rs new file mode 100644 index 0000000..a62840b --- /dev/null +++ b/client/src/main.rs @@ -0,0 +1,60 @@ +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; +fn main() { + dotenv().ok(); + env_logger::init(); + + let app = App::new("Save Sync") + .version(crate_version!()) + .author(crate_authors!()) + .about(crate_description!()); + let m = app + .subcommand( + SubCommand::with_name("track") + .arg( + Arg::with_name("path") + .value_name("PATH") + .takes_value(true) + .required(true) + .index(1) + .help("The file / directory which will be tracked"), + ) + .arg( + Arg::with_name("friendly") + .short("f") + .long("friendly") + .value_name("NAME") + .takes_value(true) + .help("A friendly name for a tracked file / directory"), + ), + ) + .get_matches(); + + match m.subcommand() { + ("track", Some(sub_m)) => track_path(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"); + + if let Some(f_name) = matches.value_of("friendly") { + info!("Name {} present for {:?}", f_name, path); + archive + .track_game_with_friendly(path, f_name) + .expect("Archive failed to track Game Save Location") + } else { + info!("No friendly name present for {:?}", path); + archive + .track_game(path) + .expect("Archive failed to track Game Save Location"); + } + + info!("Now Tracking: {:?}", path); +} diff --git a/server/Cargo.toml b/server/Cargo.toml index ec2b192..a943b34 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,3 +7,6 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +actix-web = "^3.3" +env_logger = "^0.8" +dotenv = "^0.15" diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..cab0d71 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,31 @@ +use actix_web::{get, web, App, HttpServer, Responder}; +use actix_web::{middleware::Logger, HttpRequest}; +use dotenv::dotenv; +use std::io; +use web::Path as WebPath; + +#[actix_web::main] +async fn main() -> io::Result<()> { + dotenv().ok(); + env_logger::init(); + + HttpServer::new(|| { + App::new() + .wrap(Logger::default()) + .service(index) + .service(test) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} + +#[get("/{id}/{name}/index.html")] +async fn index(WebPath((id, name)): WebPath<(u32, String)>) -> impl Responder { + format!("Hello {}! id:{}", name, id) +} + +#[get("/test")] +async fn test(_req: HttpRequest) -> impl Responder { + "This is a Test Response" +} diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..7f47da2 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,89 @@ +use std::fs::File; +use std::hash::Hasher; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use thiserror::Error; +use twox_hash::XxHash64; +use uuid::Uuid; + +// TODO: Change this seed +const XXHASH64_SEED: u64 = 1337; + +#[derive(Debug, Clone)] +pub struct GameSaveLocation { + pub friendly_name: Option, + files: Vec, + uuid: Uuid, +} + +impl GameSaveLocation { + pub fn new(files: Vec, friendly_name: Option) -> Self { + Self { + friendly_name, + files, + uuid: Uuid::new_v4(), + } + } +} + +#[derive(Debug, Clone)] +pub struct GameFile { + pub original_path: PathBuf, + pub hash: u64, +} + +impl GameFile { + pub fn new>(path: P) -> std::io::Result { + let path = path.as_ref(); + let file = File::open(path)?; + + Ok(Self { + original_path: path.to_path_buf(), + hash: Self::calculate_hash(file)?, + }) + } + + fn calculate_hash(mut buf: impl Read) -> std::io::Result { + let mut hash_writer = HashWriter(XxHash64::with_seed(XXHASH64_SEED)); + std::io::copy(&mut buf, &mut hash_writer)?; + Ok(hash_writer.0.finish()) + } +} + +#[derive(Error, Debug)] +pub enum GameFileError { + #[error(transparent)] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Default)] +pub struct BackupPath { + inner: Option, +} + +impl BackupPath { + pub fn new>(path: P) -> Self { + Self { + inner: Some(path.as_ref().to_path_buf()), + } + } +} + +impl BackupPath {} + +struct HashWriter(T); + +impl Write for HashWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.write(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + self.write(buf).map(|_| ()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 31e1bb2..bfbe1ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +pub mod game; + #[cfg(test)] mod tests { #[test]