Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 255 additions & 164 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ fuzzy-matcher = "*"
rayon = "1.7.0"
dirs = "5.0.1"
notify = "6.0.1"
tokio = { version = "1.28.2", features = ["full"] }

[features]
# this feature is used for production builds or when `devPath` points to the filesystem
Expand Down
156 changes: 156 additions & 0 deletions src-tauri/src/filesystem/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use crate::{AppState, CachedPath, StateSafe, VolumeCache};
use std::{fs};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, MutexGuard};
use std::time::Duration;
use notify::{Event};
use notify::event::{CreateKind, ModifyKind, RenameMode};
use tokio::time;
use crate::filesystem::{DIRECTORY, FILE};

pub const CACHE_FILE_PATH: &str = "./system_cache.json";

/// Handles filesystem events, currently intended for cache invalidation.
pub struct FsEventHandler {
state_mux: StateSafe,
mountpoint: PathBuf,
}

impl FsEventHandler {
pub fn new(state_mux: StateSafe, mountpoint: PathBuf) -> Self {
Self { state_mux, mountpoint }
}

/// Gets the current volume from the cache
fn get_from_cache<'a>(&self, state: &'a mut AppState) -> &'a mut VolumeCache {
let mountpoint = self.mountpoint.to_string_lossy().to_string();

state.system_cache.get_mut(&mountpoint)
.unwrap_or_else(|| panic!("Failed to find mountpoint '{:?}' in cache.", self.mountpoint))
}

pub fn handle_create(&self, kind: CreateKind, path: &Path) {
let state = &mut self.state_mux.lock().unwrap();
let current_volume = self.get_from_cache(state);

let filename = path.file_name().unwrap().to_string_lossy().to_string();
let file_type = match kind {
CreateKind::File => FILE,
CreateKind::Folder => DIRECTORY,
_ => return, // Other options are weird lol
}.to_string();

let file_path = path.to_string_lossy().to_string();
current_volume.entry(filename).or_insert(vec![CachedPath{file_path, file_type}]);
}

pub fn handle_delete(&self, path: &Path) {
let state = &mut self.state_mux.lock().unwrap();
let current_volume = self.get_from_cache(state);

let filename = path.file_name().unwrap().to_string_lossy().to_string();
current_volume.remove(&filename);
}

/// Removes file from cache, when `handle_rename_to` is called a new file is added to the cache in place.
pub fn handle_rename_from(&mut self, old_path: &Path) {
let state = &mut self.state_mux.lock().unwrap();
let current_volume = self.get_from_cache(state);

let old_path_string= old_path.to_string_lossy().to_string();
let old_filename = old_path.file_name().unwrap().to_string_lossy().to_string();

let empty_vec = &mut Vec::new();
let cached_paths = current_volume.get_mut(&old_filename).unwrap_or(empty_vec);

// If there is only one item in the cached paths, this means it can only be the renamed file and therefore it should be removed from the hashmap
if cached_paths.len() <= 1 {
current_volume.remove(&old_filename);
return;
}

cached_paths.retain(|path| path.file_path != old_path_string);
}

/// Adds new file name & path to cache.
pub fn handle_rename_to(&self, new_path: &Path) {
let state = &mut self.state_mux.lock().unwrap();
let current_volume = self.get_from_cache(state);

let filename = new_path.file_name().unwrap().to_string_lossy().to_string();
let file_type = if new_path.is_dir() { DIRECTORY } else { FILE };

let path_string = new_path.to_string_lossy().to_string();
current_volume.entry(filename).or_insert(vec![CachedPath{file_path: path_string, file_type: String::from(file_type)}]);
}

pub fn handle_event(&mut self, event: Event) {
let paths = event.paths;

match event.kind {
notify::EventKind::Modify(modify_kind) => {
if modify_kind == ModifyKind::Name(RenameMode::From) {
self.handle_rename_from(&paths[0]);
} else if modify_kind == ModifyKind::Name(RenameMode::To) {
self.handle_rename_to(&paths[0]);
}
},
notify::EventKind::Create(kind) => self.handle_create(kind, &paths[0]),
notify::EventKind::Remove(_) => self.handle_delete(&paths[0]),
_ => (),
}
}
}

/// Starts a constant interval loop where the cache is updated every ~30 seconds.
pub fn run_cache_interval(state_mux: &StateSafe) {
let state_clone = Arc::clone(state_mux);

tokio::spawn(async move { // We use tokio spawn because async closures with std spawn is unstable
let mut interval = time::interval(Duration::from_secs(30));
interval.tick().await; // Wait 30 seconds before doing first re-cache

loop {
interval.tick().await;

let guard = &mut state_clone.lock().unwrap();
save_to_cache(guard);
}
});
}

/// This takes in an Arc<Mutex<AppState>> and calls `save_to_cache` after locking it.
pub fn save_system_cache(state_mux: &StateSafe) {
let state = &mut state_mux.lock().unwrap();
save_to_cache(state);
}

/// Gets the cache from the state (in memory), encodes and saves it to the cache file path.
/// This needs optimising.
fn save_to_cache(state: &mut MutexGuard<AppState>) {
let serialized_cache = serde_json::to_string(&state.system_cache).unwrap();

let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(CACHE_FILE_PATH)
.unwrap();

file.write_all(serialized_cache.as_bytes()).unwrap();
}

/// Reads and decodes the cache file and stores it in memory for quick access.
/// Returns false if the cache was unable to deserialize.
pub fn load_system_cache(state_mux: &StateSafe) -> bool {
let state = &mut state_mux.lock().unwrap();
let file_contents = fs::read_to_string(CACHE_FILE_PATH).unwrap();

let deserialize_result = serde_json::from_str(&file_contents);
if let Ok(system_cache) = deserialize_result {
state.system_cache = system_cache;
return true;
}

false
}
37 changes: 37 additions & 0 deletions src-tauri/src/filesystem/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
pub mod cache;
pub mod volume;

use std::fs::{read_dir};
use crate::filesystem::volume::DirectoryChild;

pub const DIRECTORY: &str = "directory";
pub const FILE: &str = "file";

pub const fn bytes_to_gb(bytes: u64) -> u16 { (bytes / (1e+9 as u64)) as u16 }

/// Searches and returns the files in a given directory. This is not recursive.
#[tauri::command]
pub fn open_directory(path: String) -> Vec<DirectoryChild> {
let mut dir_children = Vec::new();

let Ok(directory) = read_dir(path) else {
return dir_children;
};

for entry in directory {
let entry = entry.unwrap();

let file_name = entry.file_name().to_str().unwrap().to_string();
let entry_is_file = entry.file_type().unwrap().is_file();
let entry = entry.path().to_str().unwrap().to_string();

if entry_is_file {
dir_children.push(DirectoryChild::File(file_name, entry));
continue;
}

dir_children.push(DirectoryChild::Directory(file_name, entry));
}

dir_children
}
103 changes: 36 additions & 67 deletions src-tauri/src/filesystem.rs → src-tauri/src/filesystem/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ use crate::{CachedPath, StateSafe};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::fs::{read_dir, File};
use std::io::Write;
use std::path::PathBuf;
use std::{fs, thread};
use std::fs::{File};
use std::path::{PathBuf};
use std::sync::{Arc, Mutex};
use sysinfo::{Disk, DiskExt, System, SystemExt};
use tauri::State;
use tauri::{State};
use walkdir::WalkDir;

const CACHE_FILE_PATH: &str = "./system_cache.json";

const fn bytes_to_gb(bytes: u64) -> u16 {
(bytes / (1e+9 as u64)) as u16
}
use notify::{Watcher, RecursiveMode};
use tokio::task::block_in_place;
use crate::filesystem::{bytes_to_gb, DIRECTORY, FILE};
use crate::filesystem::cache::{CACHE_FILE_PATH, FsEventHandler, load_system_cache, run_cache_interval, save_system_cache};

#[derive(Serialize)]
pub struct Volume {
Expand Down Expand Up @@ -94,7 +91,7 @@ impl Volume {
true => "Local Volume",
false => volume_name,
}
.to_string()
.to_string()
};

let mountpoint = disk.mount_point().to_path_buf();
Expand Down Expand Up @@ -131,12 +128,8 @@ impl Volume {
let file_path = entry.path().to_string_lossy().to_string();

let walkdir_filetype = entry.file_type();
let file_type = if walkdir_filetype.is_dir() {
"directory"
} else {
"file"
}
.to_string();
let file_type = if walkdir_filetype.is_dir() { DIRECTORY } else { FILE }
.to_string();

let cache_guard = &mut system_cache.lock().unwrap();
cache_guard
Expand All @@ -148,6 +141,27 @@ impl Volume {
});
});
}

fn watch_changes(&self, state_mux: &StateSafe) {
let mut fs_event_manager = FsEventHandler::new(state_mux.clone(), self.mountpoint.clone());

let mut watcher = notify::recommended_watcher(move |res| {
match res {
Ok(event) => fs_event_manager.handle_event(event),
Err(e) => panic!("Failed to handle event: {:?}", e),
}
}).unwrap();

let path = self.mountpoint.clone();

thread::spawn(move || {
watcher.watch(&path, RecursiveMode::Recursive).unwrap();

block_in_place(|| loop {
thread::park();
})
});
}
}

#[derive(Serialize, Deserialize, Clone)]
Expand All @@ -156,26 +170,6 @@ pub enum DirectoryChild {
Directory(String, String),
}

/// Gets the cache from the state (in memory), encodes and saves it to the cache file path.
/// This needs optimising.
pub fn save_system_cache(state_mux: &StateSafe) {
let state = &mut state_mux.lock().unwrap();
let serialized_cache = serde_json::to_string(&state.system_cache).unwrap();

let mut file = fs::OpenOptions::new()
.write(true)
.open(CACHE_FILE_PATH)
.unwrap();
file.write_all(serialized_cache.as_bytes()).unwrap();
}

/// Reads and decodes the cache file and stores it in memory for quick access.
pub fn load_system_cache(state_mux: &StateSafe) {
let state = &mut state_mux.lock().unwrap();
let file_contents = fs::read_to_string(CACHE_FILE_PATH).unwrap();
state.system_cache = serde_json::from_str(&file_contents).unwrap();
}

/// Gets list of volumes and returns them.
/// If there is a cache stored on volume it is loaded.
/// If there is no cache stored on volume, one is created as well as stored in memory.
Expand All @@ -186,9 +180,9 @@ pub fn get_volumes(state_mux: State<StateSafe>) -> Vec<Volume> {
let mut sys = System::new_all();
sys.refresh_all();

let cache_exists = fs::metadata(CACHE_FILE_PATH).is_ok();
let mut cache_exists = fs::metadata(CACHE_FILE_PATH).is_ok();
if cache_exists {
load_system_cache(&state_mux);
cache_exists = load_system_cache(&state_mux);
} else {
File::create(CACHE_FILE_PATH).unwrap();
}
Expand All @@ -200,37 +194,12 @@ pub fn get_volumes(state_mux: State<StateSafe>) -> Vec<Volume> {
volume.create_cache(&state_mux);
}

volume.watch_changes(&state_mux);
volumes.push(volume);
}

save_system_cache(&state_mux);
run_cache_interval(&state_mux);

volumes
}

/// Searches and returns the files in a given directory. This is not recursive.
#[tauri::command]
pub fn open_directory(path: String) -> Vec<DirectoryChild> {
let mut dir_children = Vec::new();

let Ok(directory) = read_dir(path) else {
return dir_children;
};

for entry in directory {
let entry = entry.unwrap();

let file_name = entry.file_name().to_str().unwrap().to_string();
let entry_is_file = entry.file_type().unwrap().is_file();
let entry = entry.path().to_str().unwrap().to_string();

if entry_is_file {
dir_children.push(DirectoryChild::File(file_name, entry));
continue;
}

dir_children.push(DirectoryChild::Directory(file_name, entry));
}

dir_children
}
9 changes: 5 additions & 4 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
mod filesystem;
mod search;

use filesystem::{get_volumes, open_directory};
use filesystem::open_directory;
use filesystem::volume::get_volumes;
use search::search_directory;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
Expand All @@ -25,7 +26,8 @@ pub struct AppState {

pub type StateSafe = Arc<Mutex<AppState>>;

fn main() {
#[tokio::main]
async fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_volumes,
Expand All @@ -35,5 +37,4 @@ fn main() {
.manage(Arc::new(Mutex::new(AppState::default())))
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

}
Loading