chore: work on project structure

This commit is contained in:
Rekai Nyangadzayi Musuka 2021-03-01 18:58:11 -06:00
parent 914b9b066d
commit 001efa8510
10 changed files with 281 additions and 106 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
RUST_LOG =

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
/target
Cargo.lock
/.env
/.vscode

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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(|_| ())
}
}

View File

@ -1,3 +1,5 @@
pub mod game;
#[cfg(test)]
mod tests {
#[test]