Skip to content

Commit daf6898

Browse files
authored
Merge e66ef7e into c6c6566
2 parents c6c6566 + e66ef7e commit daf6898

File tree

8 files changed

+500
-244
lines changed

8 files changed

+500
-244
lines changed

src-tauri/Cargo.lock

Lines changed: 255 additions & 164 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ fuzzy-matcher = "*"
2222
rayon = "1.7.0"
2323
dirs = "5.0.1"
2424
notify = "6.0.1"
25+
tokio = { version = "1.28.2", features = ["full"] }
2526

2627
[features]
2728
# this feature is used for production builds or when `devPath` points to the filesystem

src-tauri/src/filesystem/cache.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use crate::{AppState, CachedPath, StateSafe, VolumeCache};
2+
use std::{fs};
3+
use std::io::Write;
4+
use std::path::{Path, PathBuf};
5+
use std::sync::{Arc, MutexGuard};
6+
use std::time::Duration;
7+
use notify::{Event};
8+
use notify::event::{CreateKind, ModifyKind, RenameMode};
9+
use tokio::time;
10+
use crate::filesystem::{DIRECTORY, FILE};
11+
12+
pub const CACHE_FILE_PATH: &str = "./system_cache.json";
13+
14+
/// Handles filesystem events, currently intended for cache invalidation.
15+
pub struct FsEventHandler {
16+
state_mux: StateSafe,
17+
mountpoint: PathBuf,
18+
}
19+
20+
impl FsEventHandler {
21+
pub fn new(state_mux: StateSafe, mountpoint: PathBuf) -> Self {
22+
Self { state_mux, mountpoint }
23+
}
24+
25+
/// Gets the current volume from the cache
26+
fn get_from_cache<'a>(&self, state: &'a mut AppState) -> &'a mut VolumeCache {
27+
let mountpoint = self.mountpoint.to_string_lossy().to_string();
28+
29+
state.system_cache.get_mut(&mountpoint)
30+
.unwrap_or_else(|| panic!("Failed to find mountpoint '{:?}' in cache.", self.mountpoint))
31+
}
32+
33+
pub fn handle_create(&self, kind: CreateKind, path: &Path) {
34+
let state = &mut self.state_mux.lock().unwrap();
35+
let current_volume = self.get_from_cache(state);
36+
37+
let filename = path.file_name().unwrap().to_string_lossy().to_string();
38+
let file_type = match kind {
39+
CreateKind::File => FILE,
40+
CreateKind::Folder => DIRECTORY,
41+
_ => return, // Other options are weird lol
42+
}.to_string();
43+
44+
let file_path = path.to_string_lossy().to_string();
45+
current_volume.entry(filename).or_insert(vec![CachedPath{file_path, file_type}]);
46+
}
47+
48+
pub fn handle_delete(&self, path: &Path) {
49+
let state = &mut self.state_mux.lock().unwrap();
50+
let current_volume = self.get_from_cache(state);
51+
52+
let filename = path.file_name().unwrap().to_string_lossy().to_string();
53+
current_volume.remove(&filename);
54+
}
55+
56+
/// Removes file from cache, when `handle_rename_to` is called a new file is added to the cache in place.
57+
pub fn handle_rename_from(&mut self, old_path: &Path) {
58+
let state = &mut self.state_mux.lock().unwrap();
59+
let current_volume = self.get_from_cache(state);
60+
61+
let old_path_string= old_path.to_string_lossy().to_string();
62+
let old_filename = old_path.file_name().unwrap().to_string_lossy().to_string();
63+
64+
let empty_vec = &mut Vec::new();
65+
let cached_paths = current_volume.get_mut(&old_filename).unwrap_or(empty_vec);
66+
67+
// 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
68+
if cached_paths.len() <= 1 {
69+
current_volume.remove(&old_filename);
70+
return;
71+
}
72+
73+
cached_paths.retain(|path| path.file_path != old_path_string);
74+
}
75+
76+
/// Adds new file name & path to cache.
77+
pub fn handle_rename_to(&self, new_path: &Path) {
78+
let state = &mut self.state_mux.lock().unwrap();
79+
let current_volume = self.get_from_cache(state);
80+
81+
let filename = new_path.file_name().unwrap().to_string_lossy().to_string();
82+
let file_type = if new_path.is_dir() { DIRECTORY } else { FILE };
83+
84+
let path_string = new_path.to_string_lossy().to_string();
85+
current_volume.entry(filename).or_insert(vec![CachedPath{file_path: path_string, file_type: String::from(file_type)}]);
86+
}
87+
88+
pub fn handle_event(&mut self, event: Event) {
89+
let paths = event.paths;
90+
91+
match event.kind {
92+
notify::EventKind::Modify(modify_kind) => {
93+
if modify_kind == ModifyKind::Name(RenameMode::From) {
94+
self.handle_rename_from(&paths[0]);
95+
} else if modify_kind == ModifyKind::Name(RenameMode::To) {
96+
self.handle_rename_to(&paths[0]);
97+
}
98+
},
99+
notify::EventKind::Create(kind) => self.handle_create(kind, &paths[0]),
100+
notify::EventKind::Remove(_) => self.handle_delete(&paths[0]),
101+
_ => (),
102+
}
103+
}
104+
}
105+
106+
/// Starts a constant interval loop where the cache is updated every ~30 seconds.
107+
pub fn run_cache_interval(state_mux: &StateSafe) {
108+
let state_clone = Arc::clone(state_mux);
109+
110+
tokio::spawn(async move { // We use tokio spawn because async closures with std spawn is unstable
111+
let mut interval = time::interval(Duration::from_secs(30));
112+
interval.tick().await; // Wait 30 seconds before doing first re-cache
113+
114+
loop {
115+
interval.tick().await;
116+
117+
let guard = &mut state_clone.lock().unwrap();
118+
save_to_cache(guard);
119+
}
120+
});
121+
}
122+
123+
/// This takes in an Arc<Mutex<AppState>> and calls `save_to_cache` after locking it.
124+
pub fn save_system_cache(state_mux: &StateSafe) {
125+
let state = &mut state_mux.lock().unwrap();
126+
save_to_cache(state);
127+
}
128+
129+
/// Gets the cache from the state (in memory), encodes and saves it to the cache file path.
130+
/// This needs optimising.
131+
fn save_to_cache(state: &mut MutexGuard<AppState>) {
132+
let serialized_cache = serde_json::to_string(&state.system_cache).unwrap();
133+
134+
let mut file = fs::OpenOptions::new()
135+
.write(true)
136+
.truncate(true)
137+
.open(CACHE_FILE_PATH)
138+
.unwrap();
139+
140+
file.write_all(serialized_cache.as_bytes()).unwrap();
141+
}
142+
143+
/// Reads and decodes the cache file and stores it in memory for quick access.
144+
/// Returns false if the cache was unable to deserialize.
145+
pub fn load_system_cache(state_mux: &StateSafe) -> bool {
146+
let state = &mut state_mux.lock().unwrap();
147+
let file_contents = fs::read_to_string(CACHE_FILE_PATH).unwrap();
148+
149+
let deserialize_result = serde_json::from_str(&file_contents);
150+
if let Ok(system_cache) = deserialize_result {
151+
state.system_cache = system_cache;
152+
return true;
153+
}
154+
155+
false
156+
}

src-tauri/src/filesystem/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
pub mod cache;
2+
pub mod volume;
3+
4+
use std::fs::{read_dir};
5+
use crate::filesystem::volume::DirectoryChild;
6+
7+
pub const DIRECTORY: &str = "directory";
8+
pub const FILE: &str = "file";
9+
10+
pub const fn bytes_to_gb(bytes: u64) -> u16 { (bytes / (1e+9 as u64)) as u16 }
11+
12+
/// Searches and returns the files in a given directory. This is not recursive.
13+
#[tauri::command]
14+
pub fn open_directory(path: String) -> Vec<DirectoryChild> {
15+
let mut dir_children = Vec::new();
16+
17+
let Ok(directory) = read_dir(path) else {
18+
return dir_children;
19+
};
20+
21+
for entry in directory {
22+
let entry = entry.unwrap();
23+
24+
let file_name = entry.file_name().to_str().unwrap().to_string();
25+
let entry_is_file = entry.file_type().unwrap().is_file();
26+
let entry = entry.path().to_str().unwrap().to_string();
27+
28+
if entry_is_file {
29+
dir_children.push(DirectoryChild::File(file_name, entry));
30+
continue;
31+
}
32+
33+
dir_children.push(DirectoryChild::Directory(file_name, entry));
34+
}
35+
36+
dir_children
37+
}
Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@ use crate::{CachedPath, StateSafe};
22
use rayon::prelude::*;
33
use serde::{Deserialize, Serialize};
44
use std::collections::HashMap;
5-
use std::fs;
6-
use std::fs::{read_dir, File};
7-
use std::io::Write;
8-
use std::path::PathBuf;
5+
use std::{fs, thread};
6+
use std::fs::{File};
7+
use std::path::{PathBuf};
98
use std::sync::{Arc, Mutex};
109
use sysinfo::{Disk, DiskExt, System, SystemExt};
11-
use tauri::State;
10+
use tauri::{State};
1211
use walkdir::WalkDir;
13-
14-
const CACHE_FILE_PATH: &str = "./system_cache.json";
15-
16-
const fn bytes_to_gb(bytes: u64) -> u16 {
17-
(bytes / (1e+9 as u64)) as u16
18-
}
12+
use notify::{Watcher, RecursiveMode};
13+
use tokio::task::block_in_place;
14+
use crate::filesystem::{bytes_to_gb, DIRECTORY, FILE};
15+
use crate::filesystem::cache::{CACHE_FILE_PATH, FsEventHandler, load_system_cache, run_cache_interval, save_system_cache};
1916

2017
#[derive(Serialize)]
2118
pub struct Volume {
@@ -94,7 +91,7 @@ impl Volume {
9491
true => "Local Volume",
9592
false => volume_name,
9693
}
97-
.to_string()
94+
.to_string()
9895
};
9996

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

133130
let walkdir_filetype = entry.file_type();
134-
let file_type = if walkdir_filetype.is_dir() {
135-
"directory"
136-
} else {
137-
"file"
138-
}
139-
.to_string();
131+
let file_type = if walkdir_filetype.is_dir() { DIRECTORY } else { FILE }
132+
.to_string();
140133

141134
let cache_guard = &mut system_cache.lock().unwrap();
142135
cache_guard
@@ -148,6 +141,27 @@ impl Volume {
148141
});
149142
});
150143
}
144+
145+
fn watch_changes(&self, state_mux: &StateSafe) {
146+
let mut fs_event_manager = FsEventHandler::new(state_mux.clone(), self.mountpoint.clone());
147+
148+
let mut watcher = notify::recommended_watcher(move |res| {
149+
match res {
150+
Ok(event) => fs_event_manager.handle_event(event),
151+
Err(e) => panic!("Failed to handle event: {:?}", e),
152+
}
153+
}).unwrap();
154+
155+
let path = self.mountpoint.clone();
156+
157+
thread::spawn(move || {
158+
watcher.watch(&path, RecursiveMode::Recursive).unwrap();
159+
160+
block_in_place(|| loop {
161+
thread::park();
162+
})
163+
});
164+
}
151165
}
152166

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

159-
/// Gets the cache from the state (in memory), encodes and saves it to the cache file path.
160-
/// This needs optimising.
161-
pub fn save_system_cache(state_mux: &StateSafe) {
162-
let state = &mut state_mux.lock().unwrap();
163-
let serialized_cache = serde_json::to_string(&state.system_cache).unwrap();
164-
165-
let mut file = fs::OpenOptions::new()
166-
.write(true)
167-
.open(CACHE_FILE_PATH)
168-
.unwrap();
169-
file.write_all(serialized_cache.as_bytes()).unwrap();
170-
}
171-
172-
/// Reads and decodes the cache file and stores it in memory for quick access.
173-
pub fn load_system_cache(state_mux: &StateSafe) {
174-
let state = &mut state_mux.lock().unwrap();
175-
let file_contents = fs::read_to_string(CACHE_FILE_PATH).unwrap();
176-
state.system_cache = serde_json::from_str(&file_contents).unwrap();
177-
}
178-
179173
/// Gets list of volumes and returns them.
180174
/// If there is a cache stored on volume it is loaded.
181175
/// If there is no cache stored on volume, one is created as well as stored in memory.
@@ -186,9 +180,9 @@ pub fn get_volumes(state_mux: State<StateSafe>) -> Vec<Volume> {
186180
let mut sys = System::new_all();
187181
sys.refresh_all();
188182

189-
let cache_exists = fs::metadata(CACHE_FILE_PATH).is_ok();
183+
let mut cache_exists = fs::metadata(CACHE_FILE_PATH).is_ok();
190184
if cache_exists {
191-
load_system_cache(&state_mux);
185+
cache_exists = load_system_cache(&state_mux);
192186
} else {
193187
File::create(CACHE_FILE_PATH).unwrap();
194188
}
@@ -200,37 +194,12 @@ pub fn get_volumes(state_mux: State<StateSafe>) -> Vec<Volume> {
200194
volume.create_cache(&state_mux);
201195
}
202196

197+
volume.watch_changes(&state_mux);
203198
volumes.push(volume);
204199
}
205200

206201
save_system_cache(&state_mux);
202+
run_cache_interval(&state_mux);
207203

208204
volumes
209205
}
210-
211-
/// Searches and returns the files in a given directory. This is not recursive.
212-
#[tauri::command]
213-
pub fn open_directory(path: String) -> Vec<DirectoryChild> {
214-
let mut dir_children = Vec::new();
215-
216-
let Ok(directory) = read_dir(path) else {
217-
return dir_children;
218-
};
219-
220-
for entry in directory {
221-
let entry = entry.unwrap();
222-
223-
let file_name = entry.file_name().to_str().unwrap().to_string();
224-
let entry_is_file = entry.file_type().unwrap().is_file();
225-
let entry = entry.path().to_str().unwrap().to_string();
226-
227-
if entry_is_file {
228-
dir_children.push(DirectoryChild::File(file_name, entry));
229-
continue;
230-
}
231-
232-
dir_children.push(DirectoryChild::Directory(file_name, entry));
233-
}
234-
235-
dir_children
236-
}

src-tauri/src/main.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
mod filesystem;
55
mod search;
66

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

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

28-
fn main() {
29+
#[tokio::main]
30+
async fn main() {
2931
tauri::Builder::default()
3032
.invoke_handler(tauri::generate_handler![
3133
get_volumes,
@@ -35,5 +37,4 @@ fn main() {
3537
.manage(Arc::new(Mutex::new(AppState::default())))
3638
.run(tauri::generate_context!())
3739
.expect("error while running tauri application");
38-
}
39-
40+
}

0 commit comments

Comments
 (0)