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",
|
"rust-embed",
|
||||||
"rustc-hash 2.1.2",
|
"rustc-hash 2.1.2",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sha2 0.10.9",
|
||||||
"shlex 1.3.0",
|
"shlex 1.3.0",
|
||||||
"slotmap",
|
"slotmap",
|
||||||
"tar",
|
"tar",
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ slotmap = "1.1.1"
|
||||||
recently-used-xbel = "1.2.0"
|
recently-used-xbel = "1.2.0"
|
||||||
zip = "8"
|
zip = "8"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
|
sha2 = "0.10"
|
||||||
png = "0.18"
|
png = "0.18"
|
||||||
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
||||||
num_cpus = "1.17.0"
|
num_cpus = "1.17.0"
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,9 @@ item-created = Created: {$created}
|
||||||
item-modified = Modified: {$modified}
|
item-modified = Modified: {$modified}
|
||||||
item-accessed = Accessed: {$accessed}
|
item-accessed = Accessed: {$accessed}
|
||||||
calculating = Calculating...
|
calculating = Calculating...
|
||||||
|
checksum = {$kind} checksum
|
||||||
|
calculate = Calculate
|
||||||
|
error = Error
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
settings = Settings
|
settings = Settings
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use tokio::sync::mpsc;
|
||||||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||||
use crate::config::IconSizes;
|
use crate::config::IconSizes;
|
||||||
use crate::err_str;
|
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";
|
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?
|
//TODO: scan directory size on gvfs mounts?
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(items)
|
Ok(items)
|
||||||
|
|
|
||||||
189
src/tab.rs
189
src/tab.rs
|
|
@ -28,6 +28,7 @@ use jiff_icu::ConvertFrom;
|
||||||
use mime_guess::{Mime, mime};
|
use mime_guess::{Mime, mime};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::cmp::{Ordering, Reverse};
|
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,
|
overlaps_drag_rect: false,
|
||||||
dir_size,
|
dir_size,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -862,6 +864,7 @@ pub fn item_from_entry(
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size,
|
dir_size,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -918,6 +921,7 @@ pub fn item_from_trash_entry(
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1341,6 +1345,7 @@ pub fn scan_desktop(
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
cut: false,
|
||||||
|
checksums: ChecksumState::default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1762,6 +1767,9 @@ pub enum Message {
|
||||||
HighlightDeactivate(usize),
|
HighlightDeactivate(usize),
|
||||||
HighlightActivate(usize),
|
HighlightActivate(usize),
|
||||||
DirectorySize(PathBuf, DirSize),
|
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
|
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),
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ItemMetadata {
|
pub enum ItemMetadata {
|
||||||
Path {
|
Path {
|
||||||
|
|
@ -2240,6 +2265,7 @@ pub struct Item {
|
||||||
pub cut: bool,
|
pub cut: bool,
|
||||||
pub overlaps_drag_rect: bool,
|
pub overlaps_drag_rect: bool,
|
||||||
pub dir_size: DirSize,
|
pub dir_size: DirSize,
|
||||||
|
pub checksums: ChecksumState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
|
|
@ -2516,6 +2542,55 @@ impl Item {
|
||||||
}
|
}
|
||||||
column = column.push(details);
|
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()
|
if let Some(path) = self.path_opt()
|
||||||
&& self.selected
|
&& self.selected
|
||||||
{
|
{
|
||||||
|
|
@ -2713,6 +2788,32 @@ async fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64,
|
||||||
Ok(total)
|
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) {
|
fn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let mut found_home = false;
|
let mut found_home = false;
|
||||||
|
|
@ -3176,10 +3277,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rows_per_page(&self) -> usize {
|
fn rows_per_page(&self) -> usize {
|
||||||
let viewport_height = self
|
let viewport_height = self.item_view_size_opt.get().map_or(0.0, |s| s.height);
|
||||||
.item_view_size_opt
|
|
||||||
.get()
|
|
||||||
.map_or(0.0, |s| s.height);
|
|
||||||
let row_height = self
|
let row_height = self
|
||||||
.select_focus
|
.select_focus
|
||||||
.and_then(|i| self.items_opt.as_ref()?.get(i))
|
.and_then(|i| self.items_opt.as_ref()?.get(i))
|
||||||
|
|
@ -3891,8 +3989,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
Message::ItemPageDown => {
|
Message::ItemPageDown => {
|
||||||
self.dehighlight_all();
|
self.dehighlight_all();
|
||||||
if let Some((row, col)) =
|
if let Some((row, col)) = self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
||||||
self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
|
||||||
{
|
{
|
||||||
if self.select_focus.is_none() {
|
if self.select_focus.is_none() {
|
||||||
self.select_position(row, col, mod_shift);
|
self.select_position(row, col, mod_shift);
|
||||||
|
|
@ -3903,16 +4000,13 @@ impl Tab {
|
||||||
|
|
||||||
if !self.select_position(target_row, col, mod_shift) {
|
if !self.select_position(target_row, col, mod_shift) {
|
||||||
// Fall back to the last item at or before target_row
|
// Fall back to the last item at or before target_row
|
||||||
let best = self
|
let best = self.items_opt.as_ref().and_then(|items| {
|
||||||
.items_opt
|
items
|
||||||
.as_ref()
|
.iter()
|
||||||
.and_then(|items| {
|
.filter_map(|item| item.pos_opt.get())
|
||||||
items
|
.filter(|(r, _)| *r <= target_row)
|
||||||
.iter()
|
.max()
|
||||||
.filter_map(|item| item.pos_opt.get())
|
});
|
||||||
.filter(|(r, _)| *r <= target_row)
|
|
||||||
.max()
|
|
||||||
});
|
|
||||||
if let Some((best_row, best_col)) = best {
|
if let Some((best_row, best_col)) = best {
|
||||||
self.select_position(best_row, best_col, mod_shift);
|
self.select_position(best_row, best_col, mod_shift);
|
||||||
}
|
}
|
||||||
|
|
@ -3950,16 +4044,13 @@ impl Tab {
|
||||||
|
|
||||||
if !self.select_position(target_row, col, mod_shift) {
|
if !self.select_position(target_row, col, mod_shift) {
|
||||||
// Fall back to the first item at or after target_row
|
// Fall back to the first item at or after target_row
|
||||||
let best = self
|
let best = self.items_opt.as_ref().and_then(|items| {
|
||||||
.items_opt
|
items
|
||||||
.as_ref()
|
.iter()
|
||||||
.and_then(|items| {
|
.filter_map(|item| item.pos_opt.get())
|
||||||
items
|
.filter(|(r, _)| *r >= target_row)
|
||||||
.iter()
|
.min()
|
||||||
.filter_map(|item| item.pos_opt.get())
|
});
|
||||||
.filter(|(r, _)| *r >= target_row)
|
|
||||||
.min()
|
|
||||||
});
|
|
||||||
if let Some((best_row, best_col)) = best {
|
if let Some((best_row, best_col)) = best {
|
||||||
self.select_position(best_row, best_col, mod_shift);
|
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
|
// Scroll to top if needed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue