feat: add SHA256 checksums to file properties
This commit is contained in:
parent
cd427c746c
commit
4d6788c9fd
5 changed files with 170 additions and 27 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1425,6 +1425,7 @@ dependencies = [
|
|||
"rust-embed",
|
||||
"rustc-hash 2.1.2",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"shlex 1.3.0",
|
||||
"slotmap",
|
||||
"tar",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ slotmap = "1.1.1"
|
|||
recently-used-xbel = "1.2.0"
|
||||
zip = "8"
|
||||
md-5 = "0.10.6"
|
||||
sha2 = "0.10"
|
||||
png = "0.18"
|
||||
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
||||
num_cpus = "1.17.0"
|
||||
|
|
|
|||
|
|
@ -303,6 +303,9 @@ item-created = Created: {$created}
|
|||
item-modified = Modified: {$modified}
|
||||
item-accessed = Accessed: {$accessed}
|
||||
calculating = Calculating...
|
||||
checksum = {$kind} checksum
|
||||
calculate = Calculate
|
||||
error = Error
|
||||
|
||||
## Settings
|
||||
settings = Settings
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use tokio::sync::mpsc;
|
|||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||
use crate::config::IconSizes;
|
||||
use crate::err_str;
|
||||
use crate::tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location};
|
||||
use crate::tab::{self, ChecksumState, DirSize, ItemMetadata, ItemThumbnail, Location};
|
||||
|
||||
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
|
||||
|
||||
|
|
@ -216,6 +216,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
|||
//TODO: scan directory size on gvfs mounts?
|
||||
dir_size: DirSize::NotDirectory,
|
||||
cut: false,
|
||||
checksums: ChecksumState::default(),
|
||||
});
|
||||
}
|
||||
Ok(items)
|
||||
|
|
|
|||
189
src/tab.rs
189
src/tab.rs
|
|
@ -28,6 +28,7 @@ use jiff_icu::ConvertFrom;
|
|||
use mime_guess::{Mime, mime};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::Cell;
|
||||
use std::cmp::{Ordering, Reverse};
|
||||
|
|
@ -731,6 +732,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
|||
overlaps_drag_rect: false,
|
||||
dir_size,
|
||||
cut: false,
|
||||
checksums: ChecksumState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -862,6 +864,7 @@ pub fn item_from_entry(
|
|||
overlaps_drag_rect: false,
|
||||
dir_size,
|
||||
cut: false,
|
||||
checksums: ChecksumState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -918,6 +921,7 @@ pub fn item_from_trash_entry(
|
|||
overlaps_drag_rect: false,
|
||||
dir_size: DirSize::NotDirectory,
|
||||
cut: false,
|
||||
checksums: ChecksumState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1341,6 +1345,7 @@ pub fn scan_desktop(
|
|||
overlaps_drag_rect: false,
|
||||
dir_size: DirSize::NotDirectory,
|
||||
cut: false,
|
||||
checksums: ChecksumState::default(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1762,6 +1767,9 @@ pub enum Message {
|
|||
HighlightDeactivate(usize),
|
||||
HighlightActivate(usize),
|
||||
DirectorySize(PathBuf, DirSize),
|
||||
Checksums(PathBuf, ChecksumState),
|
||||
CalculateChecksums(PathBuf),
|
||||
CopyChecksum(String),
|
||||
ImageDecoded(PathBuf, u32, u32, Vec<u8>, Option<(u32, u32)>, u64), // path, width, height, pixels, display_size, generation
|
||||
}
|
||||
|
||||
|
|
@ -1789,6 +1797,23 @@ pub enum DirSize {
|
|||
Error(String),
|
||||
}
|
||||
|
||||
/// Checksums computed for a file. Only SHA256 is currently exposed; see
|
||||
/// [`calculate_checksums`] for how to add more.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FileChecksums {
|
||||
pub sha256: String,
|
||||
}
|
||||
|
||||
/// State of checksum computation for a file.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ChecksumState {
|
||||
#[default]
|
||||
NotCalculated,
|
||||
Calculating,
|
||||
Calculated(FileChecksums),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ItemMetadata {
|
||||
Path {
|
||||
|
|
@ -2240,6 +2265,7 @@ pub struct Item {
|
|||
pub cut: bool,
|
||||
pub overlaps_drag_rect: bool,
|
||||
pub dir_size: DirSize,
|
||||
pub checksums: ChecksumState,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
|
|
@ -2516,6 +2542,55 @@ impl Item {
|
|||
}
|
||||
column = column.push(details);
|
||||
|
||||
if let Some(metadata) = self.file_metadata()
|
||||
&& !metadata.is_dir()
|
||||
&& let Some(path) = self.path_opt()
|
||||
{
|
||||
let control: Element<'_, Message> = match &self.checksums {
|
||||
ChecksumState::NotCalculated => widget::button::standard(fl!("calculate"))
|
||||
.on_press(Message::CalculateChecksums(path.clone()))
|
||||
.into(),
|
||||
ChecksumState::Calculating => widget::row::with_capacity(2)
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(space_xxxs)
|
||||
.push(widget::indeterminate_circular().size(16.0))
|
||||
.push(widget::text::body(fl!("calculating")))
|
||||
.into(),
|
||||
ChecksumState::Calculated(checksums) => {
|
||||
let value = checksums.sha256.clone();
|
||||
// Middle-ellipsize the digest to fit, full value on hover.
|
||||
let value_text = widget::tooltip(
|
||||
widget::text::body(value.clone())
|
||||
.font(cosmic::font::mono())
|
||||
.width(Length::Fill)
|
||||
.wrapping(text::Wrapping::None)
|
||||
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
|
||||
1,
|
||||
))),
|
||||
widget::text::body(value.clone()),
|
||||
widget::tooltip::Position::Bottom,
|
||||
);
|
||||
let copy_button = widget::button::icon(
|
||||
widget::icon::from_name("edit-copy-symbolic").size(16),
|
||||
)
|
||||
.on_press(Message::CopyChecksum(value.clone()))
|
||||
.tooltip(fl!("copy"));
|
||||
widget::row::with_capacity(2)
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(space_xxxs)
|
||||
.push(value_text)
|
||||
.push(copy_button)
|
||||
.into()
|
||||
}
|
||||
ChecksumState::Error(err) => {
|
||||
widget::text::body(format!("{}: {}", fl!("error"), err)).into()
|
||||
}
|
||||
};
|
||||
settings.push(
|
||||
widget::settings::item::builder(fl!("checksum", kind = "SHA256")).control(control),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(path) = self.path_opt()
|
||||
&& self.selected
|
||||
{
|
||||
|
|
@ -2713,6 +2788,32 @@ async fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64,
|
|||
Ok(total)
|
||||
}
|
||||
|
||||
/// Calculate file checksums in a single pass over the file. To add another
|
||||
/// digest, hash it alongside `sha256_hasher` in the loop below and add a field
|
||||
/// to [`FileChecksums`]; the file is only read once.
|
||||
async fn calculate_checksums(path: &Path) -> Result<FileChecksums, String> {
|
||||
let path = path.to_path_buf();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut file = File::open(&path).map_err(|e| e.to_string())?;
|
||||
let mut sha256_hasher = Sha256::new();
|
||||
|
||||
let mut buffer = [0u8; 8192];
|
||||
loop {
|
||||
let bytes_read = file.read(&mut buffer).map_err(|e| e.to_string())?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
sha256_hasher.update(&buffer[..bytes_read]);
|
||||
}
|
||||
|
||||
Ok(FileChecksums {
|
||||
sha256: format!("{:x}", sha256_hasher.finalize()),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
fn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {
|
||||
let path = path.as_ref();
|
||||
let mut found_home = false;
|
||||
|
|
@ -3176,10 +3277,7 @@ impl Tab {
|
|||
}
|
||||
|
||||
fn rows_per_page(&self) -> usize {
|
||||
let viewport_height = self
|
||||
.item_view_size_opt
|
||||
.get()
|
||||
.map_or(0.0, |s| s.height);
|
||||
let viewport_height = self.item_view_size_opt.get().map_or(0.0, |s| s.height);
|
||||
let row_height = self
|
||||
.select_focus
|
||||
.and_then(|i| self.items_opt.as_ref()?.get(i))
|
||||
|
|
@ -3891,8 +3989,7 @@ impl Tab {
|
|||
}
|
||||
Message::ItemPageDown => {
|
||||
self.dehighlight_all();
|
||||
if let Some((row, col)) =
|
||||
self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
||||
if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
||||
{
|
||||
if self.select_focus.is_none() {
|
||||
self.select_position(row, col, mod_shift);
|
||||
|
|
@ -3903,16 +4000,13 @@ impl Tab {
|
|||
|
||||
if !self.select_position(target_row, col, mod_shift) {
|
||||
// Fall back to the last item at or before target_row
|
||||
let best = self
|
||||
.items_opt
|
||||
.as_ref()
|
||||
.and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.pos_opt.get())
|
||||
.filter(|(r, _)| *r <= target_row)
|
||||
.max()
|
||||
});
|
||||
let best = self.items_opt.as_ref().and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.pos_opt.get())
|
||||
.filter(|(r, _)| *r <= target_row)
|
||||
.max()
|
||||
});
|
||||
if let Some((best_row, best_col)) = best {
|
||||
self.select_position(best_row, best_col, mod_shift);
|
||||
}
|
||||
|
|
@ -3950,16 +4044,13 @@ impl Tab {
|
|||
|
||||
if !self.select_position(target_row, col, mod_shift) {
|
||||
// Fall back to the first item at or after target_row
|
||||
let best = self
|
||||
.items_opt
|
||||
.as_ref()
|
||||
.and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.pos_opt.get())
|
||||
.filter(|(r, _)| *r >= target_row)
|
||||
.min()
|
||||
});
|
||||
let best = self.items_opt.as_ref().and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.pos_opt.get())
|
||||
.filter(|(r, _)| *r >= target_row)
|
||||
.min()
|
||||
});
|
||||
if let Some((best_row, best_col)) = best {
|
||||
self.select_position(best_row, best_col, mod_shift);
|
||||
}
|
||||
|
|
@ -4662,6 +4753,52 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
}
|
||||
Message::Checksums(path, checksum_state) => {
|
||||
let location = Location::Path(path);
|
||||
if let Some(ref mut item) = self.parent_item_opt
|
||||
&& item.location_opt.as_ref() == Some(&location)
|
||||
{
|
||||
item.checksums = checksum_state.clone();
|
||||
}
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
if item.location_opt.as_ref() == Some(&location) {
|
||||
item.checksums = checksum_state;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::CalculateChecksums(path) => {
|
||||
let location = Location::Path(path.clone());
|
||||
if let Some(ref mut item) = self.parent_item_opt
|
||||
&& item.location_opt.as_ref() == Some(&location)
|
||||
{
|
||||
item.checksums = ChecksumState::Calculating;
|
||||
}
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
if item.location_opt.as_ref() == Some(&location) {
|
||||
item.checksums = ChecksumState::Calculating;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
commands.push(Command::Iced(
|
||||
cosmic::Task::future(async move {
|
||||
match calculate_checksums(&path).await {
|
||||
Ok(checksums) => {
|
||||
Message::Checksums(path, ChecksumState::Calculated(checksums))
|
||||
}
|
||||
Err(err) => Message::Checksums(path, ChecksumState::Error(err)),
|
||||
}
|
||||
})
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Message::CopyChecksum(value) => {
|
||||
commands.push(Command::Iced(cosmic::iced::clipboard::write(value).into()));
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to top if needed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue