diff --git a/src/dspfs/api.rs b/src/dspfs/api.rs new file mode 100644 index 0000000..3103a2f --- /dev/null +++ b/src/dspfs/api.rs @@ -0,0 +1,84 @@ +use crate::fs::hash::Hash; +use crate::user::PublicUser; + +use crate::fs::file::SimpleFile; +use anyhow::Result; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, HashSet}; +use std::ffi::OsString; +use std::fs::DirEntry; +use std::path::Path; +use std::time::SystemTime; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +pub struct LocalFile { + name: OsString, + modtime: SystemTime, + is_dir: bool, + file_size: u64, +} + +impl LocalFile { + pub fn from_direntry(direntry: DirEntry) -> Result { + let metadata = direntry.metadata()?; + + Ok(Self { + name: direntry.file_name(), + modtime: metadata.modified()?, + is_dir: metadata.is_dir(), + file_size: metadata.len(), + }) + } +} + +#[async_trait] +pub trait Api { + /// Equivalent of `git init`. Creates a new group with only you in it. + async fn init_group(&self, path: &Path) -> Result; + + /// Equivalent of `git add`. Adds a file to the group and makes it visible for others in the group. + async fn add_file(&self, guuid: Uuid, path: &Path) -> Result; + + /// Bootstraps a group. + async fn join_group(&self, guuid: Uuid, bootstrap_user: &PublicUser) -> Result<()>; + + /// Gives a flat list of all files and their hashes. + async fn list_files(&self, guuid: Uuid) -> Result>; + + /// Gets all users present in the group (that we know of). + async fn get_users(&self, guuid: Uuid) -> Result>; + + /// gives a list of files in your local filesystem, which you can share in the group + async fn get_available_files(&self, guuid: Uuid, path: &Path) -> Result>; + + /// gets a certain level of filetree as seen by another user + async fn get_files( + &self, + guuid: Uuid, + user: &PublicUser, + path: &Path, + ) -> Result>; + + /// requests a file from other users in the group + async fn request_file(&self, hash: Hash, to: &Path) -> Result<()>; + + /// Lists current download/uploads. + async fn status(&self) { + todo!() + } + + /// Refreshes internal state. + /// may do any of the following things: + /// * Re-index local files. + /// * Check for new users in the group. + /// * Check for new files from other users. + async fn refresh(&self); + + // TODO: + async fn add_folder(&self, _guuid: Uuid, path: &Path) -> Result<()> { + assert!(path.is_dir()); + todo!() + } +} diff --git a/src/dspfs/builder.rs b/src/dspfs/builder.rs index 06352bd..603d779 100644 --- a/src/dspfs/builder.rs +++ b/src/dspfs/builder.rs @@ -8,6 +8,8 @@ use ring::pkcs8::Document; use std::collections::HashMap; use tokio::net::ToSocketAddrs; +// TODO: Rethink + pub struct DspfsBuilder {} impl DspfsBuilder { diff --git a/src/dspfs/mod.rs b/src/dspfs/mod.rs index 772a9dc..968e45a 100644 --- a/src/dspfs/mod.rs +++ b/src/dspfs/mod.rs @@ -1,16 +1,24 @@ use crate::dspfs::builder::DspfsBuilder; use crate::dspfs::client::Client; use crate::dspfs::server::{Server, ServerHandle}; +use crate::fs::file::File; +use crate::fs::file::SimpleFile; use crate::fs::group::StoredGroup; +use crate::fs::hash::Hash; use crate::global_store::{SharedStore, Store}; use crate::user::{PrivateUser, PublicUser}; -use anyhow::Result; +use anyhow::{Context, Result}; +use api::Api; +use api::LocalFile; +use async_trait::async_trait; use log::*; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs; use std::mem; use std::path::Path; +use uuid::Uuid; +pub mod api; pub mod builder; pub mod client; pub mod server; @@ -53,36 +61,129 @@ impl Dspfs { Ok(()) } +} - pub async fn new_group(&mut self, path: impl AsRef) -> Result<()> { - // 1. create or find folder (mkdir -p) - // a) - // 2. create .dspfs folder inside of that folder - // 2.1: build file tree - // 2.2: schedule index - - // b) - // 2. Import the existing .dspfs folder - +#[async_trait] +impl Api for Dspfs { + async fn init_group(&self, path: &Path) -> Result { let group = StoredGroup::new(&path); - if group.dspfs_folder().exists() { - // Existing folder - todo!() - } else { + if !group.dspfs_folder().exists() { // New folder fs::create_dir_all(group.dspfs_folder())?; + let uuid = group.uuid; self.store.write().await.add_group(group)?; + + Ok(uuid) + } else { + Err(anyhow::anyhow!("Group already exists")) } + } - Ok(()) + async fn add_file(&self, guuid: Uuid, path: &Path) -> Result { + let mut group = self + .store + .read() + .await + .get_group(guuid)? + .context("There is no group with this uuid.")? + .reload(self.store.clone())?; + + let us = self.store.read().await.get_self_user()?.context("")?; + + let mut location = group.dspfs_root().to_path_buf(); + location.push(path); + + if !location.is_file() { + return Err(anyhow::anyhow!("Path does not point to a file")); + } + + let file = File::new(location).await.context("Indexing file failed")?; + + let simple_file = file.simplify(); + + group + .add_file(&us, file) + .await + .context("Adding file to group failed")?; + + Ok(simple_file) } -} -// -// #[async_trait] -// impl Notify for Dspfs { -// async fn file_added(&mut self, _file: &File) -> Result<()> { -// unimplemented!() -// } -// } + async fn join_group(&self, _guuid: Uuid, _bootstrap_user: &PublicUser) -> Result<()> { + unimplemented!("procrastination is life") + } + + async fn list_files(&self, guuid: Uuid) -> Result> { + let group = self + .store + .read() + .await + .get_group(guuid)? + .context("There is no group with this uuid.")? + .reload(self.store.clone())?; + + let f = group.list_files().await?.into_iter().map(|f| f.simplify()); + + Ok(f.collect()) + } + + async fn get_users(&self, guuid: Uuid) -> Result> { + let group = self + .store + .read() + .await + .get_group(guuid)? + .context("There is no group with this uuid.")?; + + Ok(group.users) + } + + async fn get_available_files(&self, guuid: Uuid, path: &Path) -> Result> { + let group = self + .store + .read() + .await + .get_group(guuid)? + .context("There is no group with this uuid.")?; + + let mut location = group.dspfs_root().to_path_buf(); + location.push(path); + + let set = location + .read_dir() + .context("Reading directory failed")? + .map(|i| i.map(LocalFile::from_direntry)) + .flatten() + .collect::>()?; + + Ok(set) + } + + async fn get_files( + &self, + guuid: Uuid, + user: &PublicUser, + path: &Path, + ) -> Result> { + let group = self + .store + .read() + .await + .get_group(guuid)? + .context("There is no group with this uuid.")? + .reload(self.store.clone())?; + + let _files = group.get_files_from_user(user, path).await; + + todo!() + } + + async fn request_file(&self, _hash: Hash, _to: &Path) -> Result<()> { + unimplemented!() + } + + async fn refresh(&self) { + unimplemented!() + } +} diff --git a/src/dspfs/server.rs b/src/dspfs/server.rs index 9877803..fcb04dc 100644 --- a/src/dspfs/server.rs +++ b/src/dspfs/server.rs @@ -219,7 +219,7 @@ pub mod tests { // Create group let mut group = StoredGroup::new(path.clone()); - group.users.push(us.public_user().clone()); + group.users.insert(us.public_user().clone()); let guuid = group.uuid; std::fs::create_dir_all(group.dspfs_folder()).unwrap(); diff --git a/src/fs/file.rs b/src/fs/file.rs index a73459d..1e3bb41 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -2,15 +2,23 @@ use crate::fs::hash::{Hash, HashingAlgorithm, BLOCK_HASHING_ALGORITHM}; use crate::user::PublicUser; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::BTreeSet; use std::path::PathBuf; use tokio::fs::File as tFile; use tokio::io::AsyncReadExt; +/// For documentation on fields, refer to the [File] struct +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] +pub struct SimpleFile { + pub path: PathBuf, + pub hash: Hash, + pub users: BTreeSet, + pub file_size: u64, +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct File { /// filename/location locally relative to group root. - /// If this variant is None, then this user is guaranteed not to be in the file's user list. pub path: PathBuf, /// Hash of the entire file @@ -23,6 +31,8 @@ pub struct File { /// If it does, the file will be recognized as a different file with a different hash pub(crate) block_size: u64, + pub file_size: u64, + /// Hashes of each block in the file. In the same order as the blocks appear in the file. blockhashes: Vec, @@ -30,7 +40,7 @@ pub struct File { /// we learn that someone has a file, either from them directly or from someone else. /// Only when we ask this user for the file and it turns out they don't have it anymore, /// do we remove him from this set. - users: HashSet, + users: BTreeSet, // TODO: // modtime } @@ -50,7 +60,18 @@ impl File { hashing_algorithm: BLOCK_HASHING_ALGORITHM, block_size: block_size(0), blockhashes: vec![block_hash], - users: HashSet::new(), + users: BTreeSet::new(), + file_size: 0, + } + } + + /// TODO: maybe avoid cloning everything here + pub fn simplify(&self) -> SimpleFile { + SimpleFile { + path: self.path.clone(), + hash: self.hash.clone(), + users: self.users.clone(), + file_size: self.file_size, } } @@ -76,6 +97,7 @@ impl File { // 5. Create File Ok(Self { path, + file_size, hash: file_hash, hashing_algorithm: BLOCK_HASHING_ALGORITHM, block_size, diff --git a/src/fs/group/heed.rs b/src/fs/group/heed.rs index 6c722b3..0402357 100644 --- a/src/fs/group/heed.rs +++ b/src/fs/group/heed.rs @@ -70,6 +70,29 @@ impl GroupStore for HeedGroupStore { .context("error getting hash from db")?) } + fn list_files(&self) -> Result> { + let rtxn = self.env.read_txn()?; + + let files = self + .files + .iter(&rtxn)? + .map(|i| i.map(|(_hash, file)| file)) + .flatten() + .collect(); + + Ok(files) + } + + fn get_filetree(&self, user: &PublicUser) -> Result { + let rtxn = self.env.read_txn()?; + + Ok(self + .filetrees + .get(&rtxn, user) + .context("error getting filetree from db")? + .context("no filetree found for this user")?) + } + fn delete_file(&mut self, user: &PublicUser, file: &File) -> Result<()> { let mut wtxn = self.env.write_txn()?; diff --git a/src/fs/group/mod.rs b/src/fs/group/mod.rs index 0972954..f245c2b 100644 --- a/src/fs/group/mod.rs +++ b/src/fs/group/mod.rs @@ -1,7 +1,8 @@ mod heed; mod store; -use crate::fs::file::File; +use crate::fs::file::{File, SimpleFile}; +use crate::fs::filetree::FileTree; use crate::fs::group::heed::HeedGroupStore; use crate::fs::group::store::{GroupStore, SharedGroupStore}; use crate::fs::hash::Hash; @@ -9,6 +10,7 @@ use crate::global_store::{SharedStore, Store}; use crate::user::PublicUser; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeSet, HashSet}; use std::io::SeekFrom; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; @@ -28,7 +30,7 @@ use uuid::Uuid; #[derive(Clone, Serialize, Deserialize)] pub struct StoredGroup { pub uuid: Uuid, - pub users: Vec, + pub users: BTreeSet, pub location: PathBuf, } @@ -39,7 +41,7 @@ impl StoredGroup { pub fn new(path: impl AsRef) -> Self { Self { uuid: Uuid::new_v4(), - users: Vec::new(), + users: BTreeSet::new(), location: path.as_ref().to_path_buf(), } } @@ -53,6 +55,10 @@ impl StoredGroup { pub fn dspfs_folder(&self) -> PathBuf { self.location.join(".dspfs") } + + pub fn dspfs_root(&self) -> &Path { + self.location.as_path() + } } /// A Group is a structure which represents a folder on your computer which is shared through DSPFS. @@ -208,6 +214,37 @@ impl Group { Ok(Some(buffer)) } + + pub async fn list_files(&self) -> Result> { + self.group_store.read().await.list_files() + } + + /// Returns all files in the directory pointed to by path, from a certain user. + /// This function only returns the files directly in that folder, and not recursively. + pub async fn get_files_from_user( + &self, + user: &PublicUser, + path: &Path, + ) -> Result> { + let filetree = self.group_store.read().await.get_filetree(user)?; + + let dir = filetree.find(path).context("no file exists at this path")?; + + Ok(match dir { + FileTree::Leaf { file, .. } => { + let mut hs = HashSet::new(); + hs.insert(file.simplify()); + hs + } + FileTree::Node { children, .. } => children + .iter() + .map(|node| match node { + FileTree::Leaf { file, .. } => file.simplify(), + FileTree::Node { .. } => todo!(), + }) + .collect(), + }) + } } impl Deref for Group { diff --git a/src/fs/group/store.rs b/src/fs/group/store.rs index 051f0ef..3d37eda 100644 --- a/src/fs/group/store.rs +++ b/src/fs/group/store.rs @@ -1,4 +1,5 @@ use crate::fs::file::File; +use crate::fs::filetree::FileTree; use crate::fs::hash::Hash; use crate::user::PublicUser; use anyhow::Result; @@ -24,6 +25,12 @@ pub trait GroupStore: Send + Sync { /// Gets a specific file given a filehash fn get_file(&self, hash: Hash) -> Result>; + /// Gets the list of all files + fn list_files(&self) -> Result>; + + /// Gets the file tree of a specific user + fn get_filetree(&self, user: &PublicUser) -> Result; + /// Changes a user's file from old to new. fn update_file(&mut self, user: &PublicUser, old: &File, new: File) -> Result<()> { self.delete_file(user, old)?; diff --git a/src/user/mod.rs b/src/user/mod.rs index 37686e1..bb38e44 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -8,7 +8,9 @@ use ring::signature::{Ed25519KeyPair, KeyPair, UnparsedPublicKey, ED25519}; use serde::export::TryFrom; use zerocopy::{AsBytes, LayoutVerified}; -#[derive(serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, Debug, AsBytes)] +#[derive( + serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, Debug, AsBytes, Ord, PartialOrd, +)] #[repr(packed)] pub struct PublicKey(pub(self) [u8; 32]); diff --git a/src/user/public.rs b/src/user/public.rs index 844311f..9d4eea0 100644 --- a/src/user/public.rs +++ b/src/user/public.rs @@ -4,7 +4,7 @@ use std::net::IpAddr; type SymmetricKey = u8; -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Ord, PartialOrd)] pub struct PublicUser { // ed25519 public key public_key: PublicKey,