From ef23ff77e1442cb348901f8d6720341fb0da0685 Mon Sep 17 00:00:00 2001 From: Nathan Rowe Date: Thu, 15 Aug 2024 12:32:12 -0500 Subject: [PATCH] Enable extracting zip files to current folder --- Cargo.lock | 255 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + i18n/en/cosmic_files.ftl | 9 ++ src/app.rs | 15 +++ src/menu.rs | 15 +++ src/operation.rs | 64 ++++++++++ 6 files changed, 360 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4c0f753..5e1524f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -256,6 +267,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -787,6 +807,27 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.12.4" @@ -898,6 +939,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clipboard-win" version = "5.4.0" @@ -1058,6 +1109,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1172,6 +1229,7 @@ dependencies = [ "shlex", "slotmap", "smol_str", + "tar", "tempfile", "test-log", "tokio", @@ -1180,6 +1238,7 @@ dependencies = [ "vergen", "xdg", "xdg-mime", + "zip", ] [[package]] @@ -1244,6 +1303,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1395,6 +1469,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "deranged" version = "0.3.11" @@ -1415,6 +1495,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "derive_setters" version = "0.1.6" @@ -1435,6 +1526,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -2523,6 +2615,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "humantime" version = "2.1.0" @@ -3106,6 +3207,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -3475,6 +3585,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" @@ -3542,6 +3658,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mac-notification-sys" version = "0.6.1" @@ -4263,6 +4389,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5266,6 +5402,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "svg_fmt" version = "0.4.3" @@ -5359,6 +5501,17 @@ dependencies = [ "slotmap", ] +[[package]] +name = "tar" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.15" @@ -6906,6 +7059,17 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", +] + [[package]] name = "xcursor" version = "0.3.6" @@ -7199,6 +7363,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "zerovec" version = "0.10.4" @@ -7221,6 +7405,77 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "zip" +version = "2.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index f89032c..e25b637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ rayon = "1" regex = "1" serde = { version = "1", features = ["serde_derive"] } shlex = { version = "1.3" } +tar = "0.4.41" tokio = { version = "1", features = ["sync"] } trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "delete-info" } xdg = { version = "2.5.2", optional = true } @@ -46,6 +47,7 @@ i18n-embed = { version = "0.14", features = [ i18n-embed-fl = "0.7" rust-embed = "8" slotmap = "1.0.7" +zip = "2.1.6" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic.git" diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 27335b2..a157052 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -83,6 +83,14 @@ copied = Copied {$items} {$items -> } from {$from} to {$to} emptying-trash = Emptying {trash} emptied-trash = Emptied {trash} +extracting = Extracting {$items} {$items -> + [one] item + *[other] items + } from {$from} to {$to} +extracted = Extracted {$items} {$items -> + [one] item + *[other] items + } from {$from} to {$to} moving = Moving {$items} {$items -> [one] item *[other] items @@ -131,6 +139,7 @@ dark = Dark light = Light # Context menu +extract-here = Extract add-to-sidebar = Add to sidebar new-file = New file... new-folder = New folder... diff --git a/src/app.rs b/src/app.rs index deabffd..630d643 100644 --- a/src/app.rs +++ b/src/app.rs @@ -71,6 +71,7 @@ pub enum Action { Cut, EditHistory, EditLocation, + ExtractHere, HistoryNext, HistoryPrevious, ItemDown, @@ -119,6 +120,7 @@ impl Action { Action::Cut => Message::Cut(entity_opt), Action::EditHistory => Message::ToggleContextPage(ContextPage::EditHistory), Action::EditLocation => Message::EditLocation(entity_opt), + Action::ExtractHere => Message::ExtractHere(entity_opt), Action::HistoryNext => Message::TabMessage(entity_opt, tab::Message::GoNext), Action::HistoryPrevious => Message::TabMessage(entity_opt, tab::Message::GoPrevious), Action::ItemDown => Message::TabMessage(entity_opt, tab::Message::ItemDown), @@ -216,6 +218,7 @@ pub enum Message { DialogPush(DialogPage), DialogUpdate(DialogPage), EditLocation(Option), + ExtractHere(Option), Key(Modifiers, Key), LaunchUrl(String), MaybeExit, @@ -1333,6 +1336,18 @@ impl Application for App { )); } } + Message::ExtractHere(entity_opt) => { + let paths = self.selected_paths(entity_opt); + if let Some(current_path) = paths.get(0) { + if let Some(destination) = current_path.parent().zip(current_path.file_stem()) { + let destination_path = destination.0.to_path_buf(); + self.operation(Operation::Extract { + paths, + to: destination_path, + }); + } + } + } Message::Key(modifiers, key) => { let entity = self.tab_model.active(); for (key_bind, action) in self.key_binds.iter() { diff --git a/src/menu.rs b/src/menu.rs index 0b3af53..68b08ba 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -9,6 +9,7 @@ use cosmic::{ widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar}, Element, }; +use mime_guess::Mime; use std::collections::HashMap; use crate::{ @@ -79,6 +80,7 @@ pub fn context_menu<'a>( let mut selected_dir = 0; let mut selected = 0; + let mut selected_types: Vec = vec![]; tab.items_opt().map(|items| { for item in items.iter() { if item.selected { @@ -86,9 +88,12 @@ pub fn context_menu<'a>( if item.metadata.is_dir() { selected_dir += 1; } + selected_types.push(item.mime.clone()); } } }); + selected_types.sort_unstable(); + selected_types.dedup(); let mut children: Vec> = Vec::new(); match tab.location { @@ -119,6 +124,16 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("rename"), Action::Rename).into()); children.push(menu_item(fl!("cut"), Action::Cut).into()); children.push(menu_item(fl!("copy"), Action::Copy).into()); + + let supported_archive_types = ["application/x-tar", "application/zip"] + .iter() + .filter_map(|mime_type| mime_type.parse::().ok()) + .collect::>(); + selected_types.retain(|t| !supported_archive_types.contains(t)); + if selected_types.is_empty() { + children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into()); + } + //TODO: Print? children.push(container(horizontal_rule(1)).padding([0, 8]).into()); children.push(menu_item(fl!("show-details"), Action::Properties).into()); diff --git a/src/operation.rs b/src/operation.rs index 26bb63d..37bdd42 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,8 +1,11 @@ use cosmic::iced::futures::{channel::mpsc::Sender, executor, SinkExt}; +use mime_guess::MimeGuess; use std::{ borrow::Cow, fs, + io::{self, Error}, path::{Path, PathBuf}, + process, sync::{ atomic::{self, AtomicU64}, Arc, @@ -138,6 +141,11 @@ pub enum Operation { }, /// Empty the trash EmptyTrash, + /// Uncompress files + Extract { + paths: Vec, + to: PathBuf, + }, /// Move items Move { paths: Vec, @@ -246,6 +254,12 @@ impl Operation { to = fl!("trash") ), Self::EmptyTrash => fl!("emptying-trash"), + Self::Extract { paths, to } => fl!( + "extracting", + items = paths.len(), + from = paths_parent_name(paths), + to = file_name(to) + ), Self::Move { paths, to } => fl!( "moving", items = paths.len(), @@ -284,6 +298,12 @@ impl Operation { to = fl!("trash") ), Self::EmptyTrash => fl!("emptied-trash"), + Self::Extract { paths, to } => fl!( + "extracted", + items = paths.len(), + from = paths_parent_name(paths), + to = file_name(to) + ), Self::Move { paths, to } => fl!( "moved", items = paths.len(), @@ -473,6 +493,50 @@ impl Operation { .send(Message::PendingProgress(id, 100.0)) .await; } + Self::Extract { paths, to } => { + for path in paths { + let to = to.to_owned(); + + tokio::task::spawn_blocking(move || -> Result<(), String> { + if let Some(file_stem) = path.file_stem() { + let mut new_dir = to.join(file_stem); + if new_dir.exists() { + let mut extensionless_path = path.to_owned(); + extensionless_path.set_extension(""); + if let Some(new_dir_parent) = new_dir.parent() { + new_dir = copy_unique_path(&extensionless_path, new_dir_parent); + } + } + + if let Some(mime) = mime_guess::from_path(&path).first() { + match mime.essence_str() { + "application/x-tar" => { + return fs::File::open(path) + .map(io::BufReader::new) + .map(tar::Archive::new) + .and_then(|mut archive| archive.unpack(new_dir)) + .map_err(err_str) + } + "application/zip" => { + return fs::File::open(path) + .map(io::BufReader::new) + .map(zip::ZipArchive::new) + .map_err(err_str)? + .and_then(|mut archive| archive.extract(new_dir)) + .map_err(err_str) + } + _ => Err(format!("unsupported mime type {:?}", mime))?, + } + } + } + + Ok(()) + }) + .await + .map_err(err_str)? + .map_err(err_str)?; + } + } Self::Move { paths, to } => { let msg_tx = msg_tx.clone(); tokio::task::spawn_blocking(move || {