Merge pull request #1662 from jasonrhansen/compress-dates

Preserve modified time when creating/extracting zip files
This commit is contained in:
Levi Portenier 2026-03-06 16:44:21 -07:00 committed by GitHub
commit 747e2a91e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 82 additions and 14 deletions

View file

@ -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<R: io::Read + io::Seek, P: AsRef<Path>>(
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<R: io::Read + io::Seek, P: AsRef<Path>>(
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<R: io::Read + io::Seek, P: AsRef<Path>>(
}
}
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<SystemTime> {
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<zip::DateTime> {
let date_time: chrono::DateTime<chrono::Local> = 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()
}

View file

@ -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| {