chore: work on project structure
This commit is contained in:
		
							
								
								
									
										1
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| RUST_LOG = | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,4 @@ | ||||
| /target | ||||
| Cargo.lock | ||||
| /.env | ||||
| /.vscode | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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<GameSaveLocation>, | ||||
|     data_root: PathBuf, | ||||
|     config_root: PathBuf, | ||||
|     tracked_files: Vec<PathBuf>, | ||||
| } | ||||
|  | ||||
| impl Archive { | ||||
| @@ -23,20 +24,24 @@ impl Archive { | ||||
|     /// let archive_res = Archive::try_default(); | ||||
|     /// ``` | ||||
|     pub fn try_default() -> Result<Self, ProjectDirError> { | ||||
|         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<P: AsRef<Path>>(&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<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); | ||||
|         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<P: AsRef<Path>>(&mut self, path: &P) -> Result<(), ArchiveAddError> { | ||||
|     pub fn track_game_with_friendly<P>(&mut self, path: P, name: &str) -> Result<(), GameTrackError> | ||||
|     where | ||||
|         P: AsRef<Path>, | ||||
|     { | ||||
|         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<P>( | ||||
|         &mut self, | ||||
|         path: P, | ||||
|         friendly_name: Option<&str>, | ||||
|     ) -> Result<GameSaveLocation, GameTrackError> | ||||
|     where | ||||
|         P: AsRef<Path>, | ||||
|     { | ||||
|         use GameTrackError::*; | ||||
|  | ||||
|         let path = path.as_ref(); | ||||
|         let mut game_files: Vec<GameFile> = 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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&mut self, path: P) -> Result<(), GameDropError> { | ||||
|         unimplemented!() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Archive { | ||||
|     fn add_file(path: &Path) -> Result<PathBuf, ArchiveError> { | ||||
|         // 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), | ||||
| } | ||||
|   | ||||
							
								
								
									
										60
									
								
								client/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								client/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
							
								
								
									
										31
									
								
								server/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								server/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
| } | ||||
							
								
								
									
										89
									
								
								src/game.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/game.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<String>, | ||||
|     files: Vec<GameFile>, | ||||
|     uuid: Uuid, | ||||
| } | ||||
|  | ||||
| impl GameSaveLocation { | ||||
|     pub fn new(files: Vec<GameFile>, friendly_name: Option<String>) -> 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<P: AsRef<Path>>(path: P) -> std::io::Result<Self> { | ||||
|         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<u64> { | ||||
|         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<PathBuf>, | ||||
| } | ||||
|  | ||||
| impl BackupPath { | ||||
|     pub fn new<P: AsRef<Path>>(path: P) -> Self { | ||||
|         Self { | ||||
|             inner: Some(path.as_ref().to_path_buf()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl BackupPath {} | ||||
|  | ||||
| struct HashWriter<T: Hasher>(T); | ||||
|  | ||||
| impl<T: Hasher> Write for HashWriter<T> { | ||||
|     fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||||
|         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(|_| ()) | ||||
|     } | ||||
| } | ||||
| @@ -1,3 +1,5 @@ | ||||
| pub mod game; | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     #[test] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user