Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new command hardlink #1268

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"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","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","nlink"],"language":"en","version":"0.2","flagWords":[]}
{"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","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking"],"version":"0.2"}
64 changes: 32 additions & 32 deletions yazi-config/preset/keymap.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,30 @@ keymap = [
{ on = "<C-r>", run = "select_all --state=none", desc = "Inverse selection of all files" },

# Operation
{ on = "o", run = "open", desc = "Open the selected files" },
{ on = "O", run = "open --interactive", desc = "Open the selected files interactively" },
{ on = "<Enter>", run = "open", desc = "Open the selected files" },
{ on = "<S-Enter>", run = "open --interactive", desc = "Open the selected files interactively" },
{ on = "y", run = "yank", desc = "Copy the selected files" },
{ on = "Y", run = "unyank", desc = "Cancel the yank status of files" },
{ on = "o", run = "open", desc = "Open selected files" },
{ on = "O", run = "open --interactive", desc = "Open selected files interactively" },
{ on = "<Enter>", run = "open", desc = "Open selected files" },
{ on = "<S-Enter>", run = "open --interactive", desc = "Open selected files interactively" },
{ on = "y", run = "yank", desc = "Copy selected files" },
{ on = "x", run = "yank --cut", desc = "Cut the selected files" },
{ on = "X", run = "unyank", desc = "Cancel the yank status of files" },
{ on = "p", run = "paste", desc = "Paste the files" },
{ on = "P", run = "paste --force", desc = "Paste the files (overwrite if the destination exists)" },
{ on = "-", run = "link", desc = "Symlink the absolute path of files" },
{ on = "_", run = "link --relative", desc = "Symlink the relative path of files" },
{ on = "d", run = "remove", desc = "Move the files to the trash" },
{ on = "D", run = "remove --permanently", desc = "Permanently delete the files" },
{ on = "a", run = "create", desc = "Create a file or directory (ends with / for directories)" },
{ on = "r", run = "rename --cursor=before_ext", desc = "Rename a file or directory" },
{ on = "Y", run = "unyank", desc = "Cancel the yank status" },
{ on = "X", run = "unyank", desc = "Cancel the yank status" },
{ on = "p", run = "paste", desc = "Paste yanked files" },
{ on = "P", run = "paste --force", desc = "Paste yanked files (overwrite if the destination exists)" },
{ on = "-", run = "link", desc = "Symlink the absolute path of yanked files" },
{ on = "_", run = "link --relative", desc = "Symlink the relative path of yanked files" },
{ on = "d", run = "remove", desc = "Trash selected files" },
{ on = "D", run = "remove --permanently", desc = "Permanently delete selected files" },
{ on = "a", run = "create", desc = "Create a file (ends with / for directories)" },
{ on = "r", run = "rename --cursor=before_ext", desc = "Rename selected file(s)" },
{ on = ";", run = "shell --interactive", desc = "Run a shell command" },
{ on = ":", run = "shell --block --interactive", desc = "Run a shell command (block the UI until the command finishes)" },
{ on = ":", run = "shell --block --interactive", desc = "Run a shell command (block until finishes)" },
{ on = ".", run = "hidden toggle", desc = "Toggle the visibility of hidden files" },
{ on = "s", run = "search fd", desc = "Search files by name using fd" },
{ on = "S", run = "search rg", desc = "Search files by content using ripgrep" },
{ on = "<C-s>", run = "search none", desc = "Cancel the ongoing search" },
{ on = "z", run = "plugin zoxide", desc = "Jump to a directory using zoxide" },
{ on = "Z", run = "plugin fzf", desc = "Jump to a directory, or reveal a file using fzf" },
{ on = "Z", run = "plugin fzf", desc = "Jump to a directory or reveal a file using fzf" },

# Linemode
{ on = [ "m", "s" ], run = "linemode size", desc = "Set linemode to size" },
Expand All @@ -92,19 +92,19 @@ keymap = [
{ on = [ "m", "n" ], run = "linemode none", desc = "Set linemode to none" },

# Copy
{ on = [ "c", "c" ], run = "copy path", desc = "Copy the absolute path" },
{ on = [ "c", "d" ], run = "copy dirname", desc = "Copy the path of the parent directory" },
{ on = [ "c", "f" ], run = "copy filename", desc = "Copy the name of the file" },
{ on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy the name of the file without the extension" },
{ on = [ "c", "c" ], run = "copy path", desc = "Copy the file path" },
{ on = [ "c", "d" ], run = "copy dirname", desc = "Copy the directory path" },
{ on = [ "c", "f" ], run = "copy filename", desc = "Copy the filename" },
{ on = [ "c", "n" ], run = "copy name_without_ext", desc = "Copy the filename without extension" },

# Filter
{ on = "f", run = "filter --smart", desc = "Filter the files" },
{ on = "f", run = "filter --smart", desc = "Filter files" },

# Find
{ on = "/", run = "find --smart", desc = "Find next file" },
{ on = "?", run = "find --previous --smart", desc = "Find previous file" },
{ on = "n", run = "find_arrow", desc = "Go to next found file" },
{ on = "N", run = "find_arrow --previous", desc = "Go to previous found file" },
{ on = "n", run = "find_arrow", desc = "Go to the next found" },
{ on = "N", run = "find_arrow --previous", desc = "Go to the previous found" },

# Sorting
{ on = [ ",", "m" ], run = "sort modified --reverse=no", desc = "Sort by modified time" },
Expand All @@ -121,7 +121,7 @@ keymap = [
{ on = [ ",", "S" ], run = "sort size --reverse", desc = "Sort by size (reverse)" },

# Tabs
{ on = "t", run = "tab_create --current", desc = "Create a new tab using the current path" },
{ on = "t", run = "tab_create --current", desc = "Create a new tab with CWD" },

{ on = "1", run = "tab_switch 0", desc = "Switch to the first tab" },
{ on = "2", run = "tab_switch 1", desc = "Switch to the second tab" },
Expand All @@ -136,11 +136,11 @@ keymap = [
{ on = "[", run = "tab_switch -1 --relative", desc = "Switch to the previous tab" },
{ on = "]", run = "tab_switch 1 --relative", desc = "Switch to the next tab" },

{ on = "{", run = "tab_swap -1", desc = "Swap the current tab with the previous tab" },
{ on = "}", run = "tab_swap 1", desc = "Swap the current tab with the next tab" },
{ on = "{", run = "tab_swap -1", desc = "Swap current tab with previous tab" },
{ on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" },

# Tasks
{ on = "w", run = "tasks_show", desc = "Show the tasks manager" },
{ on = "w", run = "tasks_show", desc = "Show task manager" },

# Goto
{ on = [ "g", "h" ], run = "cd ~", desc = "Go to the home directory" },
Expand All @@ -155,10 +155,10 @@ keymap = [
[tasks]

keymap = [
{ on = "<Esc>", run = "close", desc = "Hide the task manager" },
{ on = "<C-[>", run = "close", desc = "Hide the task manager" },
{ on = "<C-c>", run = "close", desc = "Hide the task manager" },
{ on = "w", run = "close", desc = "Hide the task manager" },
{ on = "<Esc>", run = "close", desc = "Close task manager" },
{ on = "<C-[>", run = "close", desc = "Close task manager" },
{ on = "<C-c>", run = "close", desc = "Close task manager" },
{ on = "w", run = "close", desc = "Close task manager" },

{ on = "k", run = "arrow -1", desc = "Move cursor up" },
{ on = "j", run = "arrow 1", desc = "Move cursor down" },
Expand Down
23 changes: 23 additions & 0 deletions yazi-core/src/manager/commands/hardlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use yazi_shared::event::Cmd;

use crate::{manager::Manager, tasks::Tasks};

pub struct Opt {
force: bool,
follow: bool,
}

impl From<Cmd> for Opt {
fn from(c: Cmd) -> Self { Self { force: c.bool("force"), follow: c.bool("follow") } }
}

impl Manager {
pub fn hardlink(&mut self, opt: impl Into<Opt>, tasks: &Tasks) {
if self.yanked.cut {
return;
}

let opt = opt.into() as Opt;
tasks.file_hardlink(&self.yanked, self.cwd(), opt.force, opt.follow);
}
}
1 change: 1 addition & 0 deletions yazi-core/src/manager/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod bulk_rename;
mod close;
mod create;
mod hardlink;
mod hover;
mod link;
mod open;
Expand Down
11 changes: 11 additions & 0 deletions yazi-core/src/tasks/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ impl Tasks {
}
}

pub fn file_hardlink(&self, src: &HashSet<Url>, dest: &Url, force: bool, follow: bool) {
for u in src {
let to = dest.join(u.file_name().unwrap());
if force && *u == to {
debug!("file_hardlink: same file, skipping {:?}", to);
} else {
self.scheduler.file_hardlink(u.clone(), to, force, follow);
}
}
}

pub fn file_remove(&self, targets: Vec<Url>, permanently: bool) {
for u in targets {
if permanently {
Expand Down
1 change: 1 addition & 0 deletions yazi-fm/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl<'a> Executor<'a> {
on!(MANAGER, unyank);
on!(MANAGER, paste, &self.app.cx.tasks);
on!(MANAGER, link, &self.app.cx.tasks);
on!(MANAGER, hardlink, &self.app.cx.tasks);
on!(MANAGER, remove, &self.app.cx.tasks);
on!(MANAGER, remove_do, &self.app.cx.tasks);
on!(MANAGER, create);
Expand Down
118 changes: 95 additions & 23 deletions yazi-scheduler/src/file/file.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::{borrow::Cow, collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};

use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, FutureExt};
use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn;
use yazi_config::TASKS;
use yazi_shared::fs::{calculate_size, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, Url};

use super::{FileOp, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash};
use super::{FileOp, FileOpDelete, FileOpHardlink, FileOpLink, FileOpPaste, FileOpTrash};
use crate::{TaskOp, TaskProg, LOW, NORMAL};

pub struct File {
Expand Down Expand Up @@ -39,7 +38,7 @@ impl File {
}
Ok(n) => self.prog.send(TaskProg::Adv(task.id, 0, n))?,
Err(e) if e.kind() == NotFound => {
warn!("Paste task partially done: {:?}", task);
warn!("Paste task partially done: {task:?}");
break;
}
// Operation not permitted (os error 1)
Expand All @@ -65,7 +64,7 @@ impl File {
match fs::read_link(&task.from).await {
Ok(p) => Cow::Owned(p),
Err(e) if e.kind() == NotFound => {
self.log(task.id, format!("Link task partially done: {:?}", task))?;
warn!("Link task partially done: {task:?}");
return Ok(self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?);
}
Err(e) => Err(e)?,
Expand All @@ -83,14 +82,14 @@ impl File {
ok_or_not_found(fs::remove_file(&task.to).await)?;
#[cfg(unix)]
{
fs::symlink(src, &task.to).await?
fs::symlink(src, &task.to).await?;
}
#[cfg(windows)]
{
if meta.is_dir() {
fs::symlink_dir(src, &task.to).await?
fs::symlink_dir(src, &task.to).await?;
} else {
fs::symlink_file(src, &task.to).await?
fs::symlink_file(src, &task.to).await?;
}
}

Expand All @@ -99,6 +98,26 @@ impl File {
}
self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?;
}
FileOp::Hardlink(task) => {
let meta = task.meta.as_ref().unwrap();
let src = if !task.follow {
Cow::Borrowed(task.from.as_path())
} else if let Ok(p) = fs::canonicalize(&task.from).await {
Cow::Owned(p)
} else {
Cow::Borrowed(task.from.as_path())
};

ok_or_not_found(fs::remove_file(&task.to).await)?;
match fs::hard_link(src, &task.to).await {
Err(e) if e.kind() == NotFound => {
warn!("Hardlink task partially done: {task:?}");
}
v => v?,
}

self.prog.send(TaskProg::Adv(task.id, 1, meta.len()))?;
}
FileOp::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await {
if e.kind() != NotFound && maybe_exists(&task.target).await {
Expand Down Expand Up @@ -206,6 +225,61 @@ impl File {
self.succ(id)
}

pub async fn hardlink(&self, mut task: FileOpHardlink) -> Result<()> {
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()))?;
self.queue(FileOp::Hardlink(task), NORMAL).await?;
return self.succ(id);
}

macro_rules! continue_unless_ok {
($result:expr) => {
match $result {
Ok(v) => v,
Err(e) => {
self.prog.send(TaskProg::New(task.id, 0))?;
self.fail(task.id, format!("An error occurred while hardlinking: {e}"))?;
continue;
}
}
};
}

let root = &task.to;
let skip = task.from.components().count();
let mut dirs = VecDeque::from([task.from.clone()]);

while let Some(src) = dirs.pop_front() {
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
continue_unless_ok!(match fs::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => Err(e),
_ => Ok(()),
});

let mut it = continue_unless_ok!(fs::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().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(from);
continue;
}

let to = dest.join(from.file_name().unwrap());
self.prog.send(TaskProg::New(task.id, meta.len()))?;
self.queue(FileOp::Hardlink(task.spawn(from, to, meta)), NORMAL).await?;
}
}
self.succ(task.id)
}

pub async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
let meta = fs::symlink_metadata(&task.target).await?;
if !meta.is_dir() {
Expand Down Expand Up @@ -246,6 +320,7 @@ impl File {
self.succ(id)
}

#[inline]
async fn metadata(path: &Path, follow: bool) -> io::Result<Metadata> {
if !follow {
return fs::symlink_metadata(path).await;
Expand All @@ -255,24 +330,21 @@ impl File {
if meta.is_ok() { meta } else { fs::symlink_metadata(path).await }
}

pub(crate) fn remove_empty_dirs(dir: &Path) -> BoxFuture<()> {
async move {
let mut it = match fs::read_dir(dir).await {
Ok(it) => it,
Err(_) => return,
};

while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
let path = entry.path();
Self::remove_empty_dirs(&path).await;
fs::remove_dir(path).await.ok();
}
pub(crate) async fn remove_empty_dirs(dir: &Path) {
let mut it = match fs::read_dir(dir).await {
Ok(it) => it,
Err(_) => return,
};

while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
let path = entry.path();
Box::pin(Self::remove_empty_dirs(&path)).await;
fs::remove_dir(path).await.ok();
}

fs::remove_dir(dir).await.ok();
}
.boxed()

fs::remove_dir(dir).await.ok();
}
}

Expand Down
Loading