From 80000cfd868dafff518bc0b2f547941833a67fbc Mon Sep 17 00:00:00 2001 From: Rolv Apneseth Date: Sat, 20 Apr 2024 05:23:29 +0000 Subject: [PATCH] feat: preserve files' modified at timestamp while copying (#926) Co-authored-by: sxyazi --- Cargo.lock | 1 + cspell.json | 2 +- yazi-scheduler/src/file/file.rs | 41 ++++++++++++--------------------- yazi-scheduler/src/file/op.rs | 29 +++++++++++++++++++++++ yazi-scheduler/src/scheduler.rs | 10 ++++++-- yazi-shared/Cargo.toml | 1 + yazi-shared/src/fs/fns.rs | 19 +++++++++++---- 7 files changed, 69 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96bfa28d..1608b935 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2910,6 +2910,7 @@ dependencies = [ "bitflags 2.5.0", "crossterm", "dirs", + "filetime", "futures", "libc", "parking_lot", diff --git a/cspell.json b/cspell.json index 1d39b6a6..e69845bf 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"language":"en","version":"0.2","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset"]} \ No newline at end of file +{"version":"0.2","flagWords":[],"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime"]} \ No newline at end of file diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 64f9d3f0..8b495e26 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -31,7 +31,7 @@ impl File { _ => {} } - let mut it = copy_with_progress(&task.from, &task.to); + let mut it = copy_with_progress(&task.from, &task.to, task.meta.as_ref().unwrap()); while let Some(res) = it.recv().await { match res { Ok(0) => { @@ -142,7 +142,11 @@ impl File { } } - let meta = Self::metadata(&task.from, task.follow).await?; + if task.meta.is_none() { + task.meta = Some(Self::metadata(&task.from, task.follow).await?); + } + + let meta = task.meta.as_ref().unwrap(); if !meta.is_dir() { let id = task.id; self.prog.send(TaskProg::New(id, meta.len()))?; @@ -150,7 +154,7 @@ impl File { if meta.is_file() { self.queue(FileOp::Paste(task), LOW).await?; } else if meta.is_symlink() { - self.queue(FileOp::Link(task.to_link(meta)), NORMAL).await?; + self.queue(FileOp::Link(task.into()), NORMAL).await?; } return self.succ(id); } @@ -168,9 +172,9 @@ impl File { }; } - let root = task.to.clone(); + let root = &task.to; let skip = task.from.components().count(); - let mut dirs = VecDeque::from([task.from]); + let mut dirs = VecDeque::from([task.from.clone()]); while let Some(src) = dirs.pop_front() { let dest = root.join(src.components().skip(skip).collect::()); @@ -181,22 +185,21 @@ impl File { let mut it = continue_unless_ok!(fs::read_dir(&src).await); while let Ok(Some(entry)) = it.next_entry().await { - let src = Url::from(entry.path()); - let meta = continue_unless_ok!(Self::metadata(&src, task.follow).await); + let from = Url::from(entry.path()); + let meta = continue_unless_ok!(Self::metadata(&from, task.follow).await); if meta.is_dir() { - dirs.push_back(src); + dirs.push_back(from); continue; } - task.to = dest.join(src.file_name().unwrap()); - task.from = src; + let to = dest.join(from.file_name().unwrap()); self.prog.send(TaskProg::New(task.id, meta.len()))?; if meta.is_file() { - self.queue(FileOp::Paste(task.clone()), LOW).await?; + self.queue(FileOp::Paste(task.spawn(from, to, meta)), LOW).await?; } else if meta.is_symlink() { - self.queue(FileOp::Link(task.to_link(meta)), NORMAL).await?; + self.queue(FileOp::Link(task.spawn(from, to, meta).into()), NORMAL).await?; } } } @@ -303,17 +306,3 @@ impl File { self.macro_.send(op.into(), priority).await.map_err(|_| anyhow!("Failed to send task")) } } - -impl FileOpPaste { - fn to_link(&self, meta: Metadata) -> FileOpLink { - FileOpLink { - id: self.id, - from: self.from.clone(), - to: self.to.clone(), - meta: Some(meta), - resolve: true, - relative: false, - delete: self.cut, - } - } -} diff --git a/yazi-scheduler/src/file/op.rs b/yazi-scheduler/src/file/op.rs index cb80abfe..3eabec69 100644 --- a/yazi-scheduler/src/file/op.rs +++ b/yazi-scheduler/src/file/op.rs @@ -26,11 +26,26 @@ pub struct FileOpPaste { pub id: usize, pub from: Url, pub to: Url, + pub meta: Option, pub cut: bool, pub follow: bool, pub retry: u8, } +impl FileOpPaste { + pub(super) fn spawn(&self, from: Url, to: Url, meta: Metadata) -> Self { + Self { + id: self.id, + from, + to, + meta: Some(meta), + cut: self.cut, + follow: self.follow, + retry: self.retry, + } + } +} + #[derive(Clone, Debug)] pub struct FileOpLink { pub id: usize, @@ -42,6 +57,20 @@ pub struct FileOpLink { pub delete: bool, } +impl From for FileOpLink { + fn from(value: FileOpPaste) -> Self { + Self { + id: value.id, + from: value.from, + to: value.to, + meta: value.meta, + resolve: true, + relative: false, + delete: value.cut, + } + } +} + #[derive(Clone, Debug)] pub struct FileOpDelete { pub id: usize, diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 33eb2391..db83c69c 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -99,7 +99,10 @@ impl Scheduler { if !force { to = unique_path(to).await; } - file.paste(FileOpPaste { id, from, to, cut: true, follow: false, retry: 0 }).await.ok(); + file + .paste(FileOpPaste { id, from, to, meta: None, cut: true, follow: false, retry: 0 }) + .await + .ok(); } .boxed(), LOW, @@ -121,7 +124,10 @@ impl Scheduler { if !force { to = unique_path(to).await; } - file.paste(FileOpPaste { id, from, to, cut: false, follow, retry: 0 }).await.ok(); + file + .paste(FileOpPaste { id, from, to, meta: None, cut: false, follow, retry: 0 }) + .await + .ok(); } .boxed(), LOW, diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 061177b7..f1fdf534 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1.0.82" bitflags = "2.5.0" crossterm = "0.27.0" dirs = "5.0.1" +filetime = "0.2.23" futures = "0.3.30" parking_lot = "0.12.1" percent-encoding = "2.3.1" diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 9811a5bd..97934573 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,6 +1,7 @@ -use std::{collections::VecDeque, path::{Path, PathBuf}}; +use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}}; use anyhow::Result; +use filetime::{set_file_mtime, FileTime}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; pub async fn accessible(path: &Path) -> bool { @@ -34,16 +35,24 @@ pub async fn calculate_size(path: &Path) -> u64 { total } -pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver> { +pub fn copy_with_progress( + from: &Path, + to: &Path, + meta: &Metadata, +) -> mpsc::Receiver> { let (tx, rx) = mpsc::channel(1); let (tick_tx, mut tick_rx) = oneshot::channel(); tokio::spawn({ - let (from, to) = (from.to_path_buf(), to.to_path_buf()); + let (from, to) = (from.to_owned(), to.to_owned()); + let mtime = FileTime::from_last_modification_time(meta); async move { - _ = match fs::copy(from, to).await { - Ok(len) => tick_tx.send(Ok(len)), + _ = match fs::copy(&from, &to).await { + Ok(len) => { + set_file_mtime(to, mtime).ok(); + tick_tx.send(Ok(len)) + } Err(e) => tick_tx.send(Err(e)), }; }