From 7a90e6209319bf0ea54697111c40a9ff8864aa94 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sat, 28 Feb 2026 12:31:55 -0700 Subject: [PATCH 1/2] fix: preserve modified time when creating/extracting zip files --- Cargo.lock | 1 + Cargo.toml | 1 + src/archive.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++ src/operation/mod.rs | 44 ++++++++++++++++++++++++++++------------ 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8db9972..7275e8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,6 +1461,7 @@ dependencies = [ "dirs 6.0.0", "env_logger", "fastrand 2.3.0", + "filetime", "flate2", "fork", "gio", diff --git a/Cargo.toml b/Cargo.toml index 3786a9c..d622c8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ md-5 = "0.10.6" png = "0.18" jxl-oxide = { version = "0.12.5", features = ["image"] } num_cpus = "1.17.0" +filetime = "0.2" # Completion-based IO runtime to enable io_uring / IOCP file IO support. [dependencies.compio] diff --git a/src/archive.rs b/src/archive.rs index 7374941..5786ff7 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -2,12 +2,15 @@ use crate::{ mime_icon::mime_for_path, operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk}, }; +use chrono::TimeZone; +use chrono::{Datelike, Timelike}; use cosmic::iced::futures; use std::{ collections::HashSet, fs, io::{self, Read, Write}, path::{Path, PathBuf}, + time::SystemTime, }; use zip::result::ZipError; @@ -143,6 +146,7 @@ fn zip_extract>( let mut target_dirs = HashSet::new(); #[cfg(unix)] let mut files_by_unix_mode = Vec::with_capacity(total_files); + let mut files_by_last_modified = Vec::with_capacity(total_files); for i in 0..total_files { futures::executor::block_on(async { @@ -158,12 +162,17 @@ fn zip_extract>( None => archive.by_index(i), Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()), }?; + let filepath = file .enclosed_name() .ok_or(ZipError::InvalidArchive("Invalid file path".into()))?; let outpath = directory.as_ref().join(filepath); + if let Some(last_modified) = file.last_modified() { + files_by_last_modified.push((outpath.clone(), last_modified)); + } + if file.is_dir() { make_writable_dir_all(&outpath, &mut target_dirs)?; @@ -262,8 +271,47 @@ fn zip_extract>( } } + for (path, last_modified) in files_by_last_modified { + if let Some(modified) = zip_date_time_to_system_time(last_modified) { + let file_time = filetime::FileTime::from_system_time(modified); + filetime::set_file_mtime(&path, file_time)?; + } + } + // Flush files to disk futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await }); Ok(()) } + +fn zip_date_time_to_system_time(date_time: zip::DateTime) -> Option { + let date = chrono::NaiveDate::from_ymd_opt( + date_time.year() as i32, + date_time.month() as u32, + date_time.day() as u32, + )?; + let time = chrono::NaiveTime::from_hms_opt( + date_time.hour() as u32, + date_time.minute() as u32, + date_time.second() as u32, + )?; + let naive = chrono::NaiveDateTime::new(date, time); + chrono::Local + .from_local_datetime(&naive) + .latest() + .map(SystemTime::from) +} + +pub fn system_time_to_zip_date_time(system_time: SystemTime) -> Option { + let date_time: chrono::DateTime = system_time.into(); + + zip::DateTime::from_date_and_time( + date_time.year() as u16, + date_time.month() as u8, + date_time.day() as u8, + date_time.hour() as u8, + date_time.minute() as u8, + date_time.second() as u8, + ) + .ok() +} diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 8b926af..3aad6fc 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -1,5 +1,6 @@ use crate::{ app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID}, + archive, config::IconSizes, fl, spawn_detached::spawn_detached, @@ -184,7 +185,13 @@ async fn copy_or_move( let msg_tx = msg_tx.clone(); context = context.on_replace(move |op, conflict_count| { let msg_tx = msg_tx.clone(); - Box::pin(handle_replace(msg_tx, op.from.clone(), op.to.clone(), true, conflict_count)) + Box::pin(handle_replace( + msg_tx, + op.from.clone(), + op.to.clone(), + true, + conflict_count, + )) }); } @@ -748,24 +755,35 @@ impl Operation { .map_err(|e| OperationError::from_err(e, &controller))? .to_str() { + let mut file = fs::File::open(path).map_err(|e| { + OperationError::from_err(e, &controller) + })?; + let metadata = file.metadata().map_err(|e| { + OperationError::from_err(e, &controller) + })?; + + if let Ok(modified) = metadata.modified() { + if let Some(last_modified) = + archive::system_time_to_zip_date_time(modified) + { + zip_options = + zip_options.last_modified_time(last_modified); + } + } + + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let mode = metadata.mode(); + zip_options = zip_options.unix_permissions(mode); + } + if path.is_file() { - let mut file = fs::File::open(path).map_err(|e| { - OperationError::from_err(e, &controller) - })?; - let metadata = file.metadata().map_err(|e| { - OperationError::from_err(e, &controller) - })?; let total = metadata.len(); if total >= 4 * 1024 * 1024 * 1024 { // The large file option must be enabled for files above 4 GiB zip_options = zip_options.large_file(true); } - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - let mode = metadata.mode(); - zip_options = zip_options.unix_permissions(mode); - } archive .start_file(relative_path, zip_options) .map_err(|e| { From 1b50c18e1d7b0ab11bb0fd879d0d2d980f9c3d47 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sat, 28 Feb 2026 14:31:45 -0700 Subject: [PATCH 2/2] fix: change 'Compress' to 'Compress...' in context menu --- i18n/en/cosmic_files.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 361756a..654e9ab 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -316,7 +316,7 @@ type-to-search-select = Selects the first matching file or folder # Context menu add-to-sidebar = Add to sidebar clear-recents-history = Clear Recents history -compress = Compress +compress = Compress... copy-to = Copy to... delete-permanently = Delete permanently eject = Eject