Skip to content

Commit aaa5885

Browse files
authored
Add cache support to similar music files (#558)
* Simplify cache code * Better saving/loading. Add support for loading/saving json files in release mode * Broken files cache * Finally same music cache
1 parent db3b1f5 commit aaa5885

17 files changed

+528
-427
lines changed

Changelog.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
## Version 4.0.0 - ?
22
- Multithreading support for collecting files to check(2/3x speedup on 4 thread processor and SSD) - [#502](https://github.com/qarmin/czkawka/pull/502), [#504](https://github.com/qarmin/czkawka/pull/504)
3-
- Add Polish, German and Italian translation - [#469](https://github.com/qarmin/czkawka/pull/469), [#508](https://github.com/qarmin/czkawka/pull/508), [5be](https://github.com/qarmin/czkawka/commit/5be801e76395855f07ab1da43cdbb8bd0b843834)
3+
- Add multiple translations - Polish, Italian, French, German, Russian ... - [#469](https://github.com/qarmin/czkawka/pull/469), [#508](https://github.com/qarmin/czkawka/pull/508), [5be](https://github.com/qarmin/czkawka/commit/5be801e76395855f07ab1da43cdbb8bd0b843834)
44
- Add support for finding similar videos - [#460](https://github.com/qarmin/czkawka/pull/460)
5-
- GUI code refactoring(could fix some bugs) - [#462](https://github.com/qarmin/czkawka/pull/462)
5+
- GUI code refactoring and search code unification - [#462](https://github.com/qarmin/czkawka/pull/462), [#531](https://github.com/qarmin/czkawka/pull/531)
66
- Fixed crash when trying to hard/symlink 0 files - [#462](https://github.com/qarmin/czkawka/pull/462)
77
- GTK 4 compatibility improvements for future change of toolkit - [#467](https://github.com/qarmin/czkawka/pull/467), [#468](https://github.com/qarmin/czkawka/pull/468), [#473](https://github.com/qarmin/czkawka/pull/473), [#474](https://github.com/qarmin/czkawka/pull/474), [#503](https://github.com/qarmin/czkawka/pull/503), [#505](https://github.com/qarmin/czkawka/pull/505)
88
- Change minimal supported OS to Ubuntu 20.04(needed by GTK) - [#468](https://github.com/qarmin/czkawka/pull/468)
@@ -22,6 +22,7 @@
2222
- Image compare performance and usability improvements - [#529](https://github.com/qarmin/czkawka/pull/529), [#528](https://github.com/qarmin/czkawka/pull/528), [#530](https://github.com/qarmin/czkawka/pull/530), [#525](https://github.com/qarmin/czkawka/pull/525)
2323
- Reorganize(unify) saving/loading data from file - [#524](https://github.com/qarmin/czkawka/pull/524)
2424
- Add "reference folders" - [#516](https://github.com/qarmin/czkawka/pull/516)
25+
- Add cache for similar music files - [#558](https://github.com/qarmin/czkawka/pull/558)
2526

2627
## Version 3.3.1 - 22.11.2021r
2728
- Fix crash when moving buttons [#457](https://github.com/qarmin/czkawka/pull/457)

czkawka_core/src/broken_files.rs

+68-115
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::collections::BTreeMap;
2-
use std::fs::{File, Metadata, OpenOptions};
2+
use std::fs::{File, Metadata};
33
use std::io::prelude::*;
44
use std::io::{BufReader, BufWriter};
55
use std::path::{Path, PathBuf};
@@ -10,10 +10,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
1010
use std::{fs, mem, panic, thread};
1111

1212
use crossbeam_channel::Receiver;
13-
use directories_next::ProjectDirs;
1413
use rayon::prelude::*;
14+
use serde::{Deserialize, Serialize};
1515

16-
use crate::common::Common;
16+
use crate::common::{open_cache_folder, Common};
1717
use crate::common_directory::Directories;
1818
use crate::common_extensions::Extensions;
1919
use crate::common_items::ExcludedItems;
@@ -23,8 +23,6 @@ use crate::fl;
2323
use crate::localizer::generate_translation_hashmap;
2424
use crate::similar_images::{AUDIO_FILES_EXTENSIONS, IMAGE_RS_BROKEN_FILES_EXTENSIONS, ZIP_FILES_EXTENSIONS};
2525

26-
const CACHE_FILE_NAME: &str = "cache_broken_files.txt";
27-
2826
#[derive(Debug)]
2927
pub struct ProgressData {
3028
pub current_stage: u8,
@@ -39,7 +37,7 @@ pub enum DeleteMethod {
3937
Delete,
4038
}
4139

42-
#[derive(Clone)]
40+
#[derive(Clone, Serialize, Deserialize)]
4341
pub struct FileEntry {
4442
pub path: PathBuf,
4543
pub modified_date: u64,
@@ -48,7 +46,7 @@ pub struct FileEntry {
4846
pub error_string: String,
4947
}
5048

51-
#[derive(Copy, Clone, PartialEq, Eq)]
49+
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
5250
pub enum TypeOfFile {
5351
Unknown = -1,
5452
Image = 0,
@@ -82,6 +80,8 @@ pub struct BrokenFiles {
8280
delete_method: DeleteMethod,
8381
stopped_search: bool,
8482
use_cache: bool,
83+
delete_outdated_cache: bool, // TODO add this to GUI
84+
save_also_as_json: bool,
8585
}
8686

8787
impl BrokenFiles {
@@ -98,6 +98,8 @@ impl BrokenFiles {
9898
stopped_search: false,
9999
broken_files: Default::default(),
100100
use_cache: true,
101+
delete_outdated_cache: true,
102+
save_also_as_json: false,
101103
}
102104
}
103105

@@ -135,6 +137,10 @@ impl BrokenFiles {
135137
self.delete_method = delete_method;
136138
}
137139

140+
pub fn set_save_also_as_json(&mut self, save_also_as_json: bool) {
141+
self.save_also_as_json = save_also_as_json;
142+
}
143+
138144
pub fn set_use_cache(&mut self, use_cache: bool) {
139145
self.use_cache = use_cache;
140146
}
@@ -350,7 +356,7 @@ impl BrokenFiles {
350356
let mut non_cached_files_to_check: BTreeMap<String, FileEntry> = Default::default();
351357

352358
if self.use_cache {
353-
loaded_hash_map = match load_cache_from_file(&mut self.text_messages) {
359+
loaded_hash_map = match load_cache_from_file(&mut self.text_messages, self.delete_outdated_cache) {
354360
Some(t) => t,
355361
None => Default::default(),
356362
};
@@ -501,7 +507,7 @@ impl BrokenFiles {
501507
for (_name, file_entry) in loaded_hash_map {
502508
all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry);
503509
}
504-
save_cache_to_file(&all_results, &mut self.text_messages);
510+
save_cache_to_file(&all_results, &mut self.text_messages, self.save_also_as_json);
505511
}
506512

507513
self.information.number_of_broken_files = self.broken_files.len();
@@ -620,137 +626,84 @@ impl PrintResults for BrokenFiles {
620626
}
621627
}
622628

623-
fn save_cache_to_file(hashmap_file_entry: &BTreeMap<String, FileEntry>, text_messages: &mut Messages) {
624-
if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") {
625-
// Lin: /home/username/.cache/czkawka
626-
// Win: C:\Users\Username\AppData\Local\Qarmin\Czkawka\cache
627-
// Mac: /Users/Username/Library/Caches/pl.Qarmin.Czkawka
628-
629-
let cache_dir = PathBuf::from(proj_dirs.cache_dir());
630-
if cache_dir.exists() {
631-
if !cache_dir.is_dir() {
632-
text_messages.messages.push(format!("Config dir {} is a file!", cache_dir.display()));
633-
return;
634-
}
635-
} else if let Err(e) = fs::create_dir_all(&cache_dir) {
636-
text_messages.messages.push(format!("Cannot create config dir {}, reason {}", cache_dir.display(), e));
637-
return;
629+
fn save_cache_to_file(old_hashmap: &BTreeMap<String, FileEntry>, text_messages: &mut Messages, save_also_as_json: bool) {
630+
let mut hashmap: BTreeMap<String, FileEntry> = Default::default();
631+
for (path, fe) in old_hashmap {
632+
if fe.size > 1024 {
633+
hashmap.insert(path.clone(), fe.clone());
638634
}
639-
let cache_file = cache_dir.join(CACHE_FILE_NAME);
640-
let file_handler = match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file) {
641-
Ok(t) => t,
642-
Err(e) => {
635+
}
636+
let hashmap = &hashmap;
637+
638+
if let Some(((file_handler, cache_file), (file_handler_json, cache_file_json))) = open_cache_folder(&get_cache_file(), true, save_also_as_json, &mut text_messages.warnings) {
639+
{
640+
let writer = BufWriter::new(file_handler.unwrap()); // Unwrap because cannot fail here
641+
if let Err(e) = bincode::serialize_into(writer, hashmap) {
643642
text_messages
644-
.messages
645-
.push(format!("Cannot create or open cache file {}, reason {}", cache_file.display(), e));
643+
.warnings
644+
.push(format!("Cannot write data to cache file {}, reason {}", cache_file.display(), e));
646645
return;
647646
}
648-
};
649-
let mut writer = BufWriter::new(file_handler);
650-
651-
for file_entry in hashmap_file_entry.values() {
652-
// Only save to cache files which have more than 1KB
653-
if file_entry.size > 1024 {
654-
let string: String = format!(
655-
"{}//{}//{}//{}",
656-
file_entry.path.display(),
657-
file_entry.size,
658-
file_entry.modified_date,
659-
file_entry.error_string
660-
);
661-
662-
if let Err(e) = writeln!(writer, "{}", string) {
647+
}
648+
if save_also_as_json {
649+
if let Some(file_handler_json) = file_handler_json {
650+
let writer = BufWriter::new(file_handler_json);
651+
if let Err(e) = serde_json::to_writer(writer, hashmap) {
663652
text_messages
664-
.messages
665-
.push(format!("Failed to save some data to cache file {}, reason {}", cache_file.display(), e));
653+
.warnings
654+
.push(format!("Cannot write data to cache file {}, reason {}", cache_file_json.display(), e));
666655
return;
667-
};
656+
}
668657
}
669658
}
659+
660+
text_messages.messages.push(format!("Properly saved to file {} cache entries.", hashmap.len()));
670661
}
671662
}
672663

673-
fn load_cache_from_file(text_messages: &mut Messages) -> Option<BTreeMap<String, FileEntry>> {
674-
if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") {
675-
let cache_dir = PathBuf::from(proj_dirs.cache_dir());
676-
let cache_file = cache_dir.join(CACHE_FILE_NAME);
677-
// TODO add before checking if cache exists(if not just return) but if exists then enable error
678-
let file_handler = match OpenOptions::new().read(true).open(&cache_file) {
679-
Ok(t) => t,
680-
Err(_inspected) => {
681-
// text_messages.messages.push(format!("Cannot find or open cache file {}", cache_file.display())); // This shouldn't be write to output
682-
return None;
683-
}
684-
};
685-
686-
let reader = BufReader::new(file_handler);
687-
688-
let mut hashmap_loaded_entries: BTreeMap<String, FileEntry> = Default::default();
689-
690-
// Read the file line by line using the lines() iterator from std::io::BufRead.
691-
for (index, line) in reader.lines().enumerate() {
692-
let line = match line {
664+
fn load_cache_from_file(text_messages: &mut Messages, delete_outdated_cache: bool) -> Option<BTreeMap<String, FileEntry>> {
665+
if let Some(((file_handler, cache_file), (file_handler_json, cache_file_json))) = open_cache_folder(&get_cache_file(), false, true, &mut text_messages.warnings) {
666+
let mut hashmap_loaded_entries: BTreeMap<String, FileEntry>;
667+
if let Some(file_handler) = file_handler {
668+
let reader = BufReader::new(file_handler);
669+
hashmap_loaded_entries = match bincode::deserialize_from(reader) {
693670
Ok(t) => t,
694671
Err(e) => {
695672
text_messages
696673
.warnings
697-
.push(format!("Failed to load line number {} from cache file {}, reason {}", index + 1, cache_file.display(), e));
674+
.push(format!("Failed to load data from cache file {}, reason {}", cache_file.display(), e));
675+
return None;
676+
}
677+
};
678+
} else {
679+
let reader = BufReader::new(file_handler_json.unwrap()); // Unwrap cannot fail, because at least one file must be valid
680+
hashmap_loaded_entries = match serde_json::from_reader(reader) {
681+
Ok(t) => t,
682+
Err(e) => {
683+
text_messages
684+
.warnings
685+
.push(format!("Failed to load data from cache file {}, reason {}", cache_file_json.display(), e));
698686
return None;
699687
}
700688
};
701-
let uuu = line.split("//").collect::<Vec<&str>>();
702-
if uuu.len() != 4 {
703-
text_messages
704-
.warnings
705-
.push(format!("Found invalid data in line {} - ({}) in cache file {}", index + 1, line, cache_file.display()));
706-
continue;
707-
}
708-
// Don't load cache data if destination file not exists
709-
if Path::new(uuu[0]).exists() {
710-
hashmap_loaded_entries.insert(
711-
uuu[0].to_string(),
712-
FileEntry {
713-
path: PathBuf::from(uuu[0]),
714-
size: match uuu[1].parse::<u64>() {
715-
Ok(t) => t,
716-
Err(e) => {
717-
text_messages.warnings.push(format!(
718-
"Found invalid size value in line {} - ({}) in cache file {}, reason {}",
719-
index + 1,
720-
line,
721-
cache_file.display(),
722-
e
723-
));
724-
continue;
725-
}
726-
},
727-
modified_date: match uuu[2].parse::<u64>() {
728-
Ok(t) => t,
729-
Err(e) => {
730-
text_messages.warnings.push(format!(
731-
"Found invalid modified date value in line {} - ({}) in cache file {}, reason {}",
732-
index + 1,
733-
line,
734-
cache_file.display(),
735-
e
736-
));
737-
continue;
738-
}
739-
},
740-
type_of_file: check_extension_avaibility(&uuu[0].to_lowercase()),
741-
error_string: uuu[3].to_string(),
742-
},
743-
);
744-
}
745689
}
746690

691+
// Don't load cache data if destination file not exists
692+
if delete_outdated_cache {
693+
hashmap_loaded_entries.retain(|src_path, _file_entry| Path::new(src_path).exists());
694+
}
695+
696+
text_messages.messages.push(format!("Properly loaded {} cache entries.", hashmap_loaded_entries.len()));
697+
747698
return Some(hashmap_loaded_entries);
748699
}
749-
750-
text_messages.messages.push("Cannot find or open system config dir to save cache file".to_string());
751700
None
752701
}
753702

703+
fn get_cache_file() -> String {
704+
"cache_broken_files.bin".to_string()
705+
}
706+
754707
fn check_extension_avaibility(file_name_lowercase: &str) -> TypeOfFile {
755708
if IMAGE_RS_BROKEN_FILES_EXTENSIONS.iter().any(|e| file_name_lowercase.ends_with(e)) {
756709
TypeOfFile::Image

czkawka_core/src/common.rs

+58-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
use directories_next::ProjectDirs;
12
use image::{DynamicImage, ImageBuffer, Rgb};
23
use imagepipe::{ImageSource, Pipeline};
34
use std::ffi::OsString;
45
use std::fs;
5-
use std::fs::OpenOptions;
6+
use std::fs::{File, OpenOptions};
67
use std::io::BufReader;
78
use std::path::{Path, PathBuf};
89
use std::time::SystemTime;
@@ -11,6 +12,62 @@ use std::time::SystemTime;
1112
1213
pub struct Common();
1314

15+
pub fn open_cache_folder(cache_file_name: &str, save_to_cache: bool, use_json: bool, warnings: &mut Vec<String>) -> Option<((Option<File>, PathBuf), (Option<File>, PathBuf))> {
16+
if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") {
17+
let cache_dir = PathBuf::from(proj_dirs.cache_dir());
18+
let cache_file = cache_dir.join(cache_file_name);
19+
let cache_file_json = cache_dir.join(cache_file_name.replace(".bin", ".json"));
20+
21+
let mut file_handler_default = None;
22+
let mut file_handler_json = None;
23+
24+
if save_to_cache {
25+
if cache_dir.exists() {
26+
if !cache_dir.is_dir() {
27+
warnings.push(format!("Config dir {} is a file!", cache_dir.display()));
28+
return None;
29+
}
30+
} else if let Err(e) = fs::create_dir_all(&cache_dir) {
31+
warnings.push(format!("Cannot create config dir {}, reason {}", cache_dir.display(), e));
32+
return None;
33+
}
34+
35+
file_handler_default = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file) {
36+
Ok(t) => t,
37+
Err(e) => {
38+
warnings.push(format!("Cannot create or open cache file {}, reason {}", cache_file.display(), e));
39+
return None;
40+
}
41+
});
42+
if use_json {
43+
file_handler_json = Some(match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file_json) {
44+
Ok(t) => t,
45+
Err(e) => {
46+
warnings.push(format!("Cannot create or open cache file {}, reason {}", cache_file_json.display(), e));
47+
return None;
48+
}
49+
});
50+
}
51+
} else {
52+
if let Ok(t) = OpenOptions::new().read(true).open(&cache_file) {
53+
file_handler_default = Some(t);
54+
} else {
55+
if use_json {
56+
file_handler_json = Some(match OpenOptions::new().read(true).open(&cache_file_json) {
57+
Ok(t) => t,
58+
Err(_) => return None,
59+
});
60+
} else {
61+
// messages.push(format!("Cannot find or open cache file {}", cache_file.display())); // No error or warning
62+
return None;
63+
}
64+
}
65+
};
66+
return Some(((file_handler_default, cache_file), (file_handler_json, cache_file_json)));
67+
}
68+
None
69+
}
70+
1471
pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug) -> Option<DynamicImage> {
1572
let file_handler = match OpenOptions::new().read(true).open(&path) {
1673
Ok(t) => t,

0 commit comments

Comments
 (0)