feat: new link command, which creates symlinks to the yanked files (#167)

This commit is contained in:
Collide 2023-09-25 01:48:37 +08:00 committed by sxyazi
parent f7fdda9d9b
commit 782f88b965
No known key found for this signature in database
10 changed files with 200 additions and 46 deletions

View file

@ -98,16 +98,26 @@ impl Executor {
"open" => cx.manager.open(exec.named.contains_key("interactive")),
"yank" => cx.manager.yank(exec.named.contains_key("cut")),
"paste" => {
let dest = cx.manager.cwd().to_owned();
let dest = cx.manager.cwd();
let (cut, src) = cx.manager.yanked();
let force = exec.named.contains_key("force");
if *cut {
cx.tasks.file_cut(src, dest, force)
} else {
cx.tasks.file_copy(src, dest, force, exec.named.contains_key("follow"))
cx.tasks.file_copy(src, dest, force)
}
}
"link" => {
let (cut, src) = cx.manager.yanked();
!cut
&& cx.tasks.file_link(
src,
cx.manager.cwd(),
exec.named.contains_key("relative"),
exec.named.contains_key("force"),
)
}
"remove" => {
let targets = cx.manager.selected().into_iter().map(|f| f.url_owned()).collect();
let force = exec.named.contains_key("force");

View file

@ -56,7 +56,6 @@
- paste: Paste the files.
- `--force`: Overwrite the destination file if it exists.
- `--follow`: Copy the file pointed to by a symbolic link, rather than the link itself. Only valid during copying.
- remove: Move the files to the trash/recycle bin.

View file

@ -51,8 +51,8 @@ keymap = [
{ on = [ "x" ], exec = "yank --cut", desc = "Cut the selected files" },
{ on = [ "p" ], exec = "paste", desc = "Paste the files" },
{ on = [ "P" ], exec = "paste --force", desc = "Paste the files (overwrite if the destination exists)" },
{ on = [ "k" ], exec = "paste --follow", desc = "Paste the files (follow the symlinks)" },
{ on = [ "K" ], exec = "paste --follow --force", desc = "Paste the files (overwrite + follow)" },
{ on = [ "n" ], exec = "link", desc = "Symlink the absolute path of files" },
{ on = [ "N" ], exec = "link --relative", desc = "Symlink the relative path of files" },
{ on = [ "d" ], exec = "remove", desc = "Move the files to the trash" },
{ on = [ "D" ], exec = "remove --permanently", desc = "Permanently delete the files" },
{ on = [ "a" ], exec = "create", desc = "Create a file or directory (ends with / for directories)" },

View file

@ -42,14 +42,11 @@ impl File {
pub fn url(&self) -> &Url { &self.url }
#[inline]
pub fn set_url(&mut self, url: Url) { self.url = url; }
pub fn url_mut(&mut self) -> &mut Url { &mut self.url }
#[inline]
pub fn url_owned(&self) -> Url { self.url.clone() }
#[inline]
pub fn url_os_str(&self) -> &OsStr { self.url.as_os_str() }
#[inline]
pub fn name(&self) -> Option<&OsStr> { self.url.file_name() }

View file

@ -222,7 +222,7 @@ impl Tab {
let mut it = self.selected().into_iter().peekable();
while let Some(f) = it.next() {
s.push(match type_ {
"path" => f.url_os_str(),
"path" => f.url().as_os_str(),
"dirname" => f.url().parent().map_or(OsStr::new(""), |p| p.as_os_str()),
"filename" => f.name().unwrap_or(OsStr::new("")),
"name_without_ext" => f.stem().unwrap_or(OsStr::new("")),
@ -356,7 +356,7 @@ impl Tab {
let selected: Vec<_> = self
.selected()
.into_iter()
.map(|f| (f.url_os_str().to_owned(), Default::default()))
.map(|f| (f.url().as_os_str().to_owned(), Default::default()))
.collect();
let mut exec = exec.to_owned();

View file

@ -197,7 +197,7 @@ impl Watcher {
let mut new = Vec::with_capacity(files.len());
for file in files {
let mut file = file.clone();
file.set_url(ori.join(file.url().strip_prefix(url).unwrap()));
*file.url_mut() = ori.join(file.url().strip_prefix(url).unwrap());
new.push(file);
}
new

View file

@ -8,7 +8,7 @@ use shared::{unique_path, Throttle, Url};
use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep};
use tracing::{info, trace};
use super::{workers::{File, FileOpDelete, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
use super::{workers::{File, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash, Precache, PrecacheOpMime, PrecacheOpSize, Process, ProcessOpOpen}, Running, TaskOp, TaskStage};
use crate::emit;
pub struct Scheduler {
@ -218,7 +218,7 @@ impl Scheduler {
});
}
pub(super) fn file_copy(&self, from: Url, mut to: Url, force: bool, follow: bool) {
pub(super) fn file_copy(&self, from: Url, mut to: Url, force: bool) {
let name = format!("Copy {:?} to {:?}", from, to);
let id = self.running.write().add(name);
@ -228,7 +228,26 @@ 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, cut: false, follow: true, retry: 0 }).await.ok();
}
.boxed()
});
}
pub(super) fn file_link(&self, from: Url, mut to: Url, relative: bool, force: bool) {
let name = format!("Link {from:?} to {to:?}");
let id = self.running.write().add(name);
let _ = self.todo.send_blocking({
let file = self.file.clone();
async move {
if !force {
to = unique_path(to).await;
}
file
.link(FileOpLink { id, from, to, meta: None, resolve: false, relative, delete: false })
.await
.ok();
}
.boxed()
});

View file

@ -155,7 +155,7 @@ impl Tasks {
false
}
pub fn file_cut(&self, src: &HashSet<Url>, dest: Url, force: bool) -> bool {
pub fn file_cut(&self, src: &HashSet<Url>, dest: &Url, force: bool) -> bool {
for u in src {
let to = dest.join(u.file_name().unwrap());
if force && u == &to {
@ -167,13 +167,25 @@ impl Tasks {
false
}
pub fn file_copy(&self, src: &HashSet<Url>, dest: Url, force: bool, follow: bool) -> bool {
pub fn file_copy(&self, src: &HashSet<Url>, dest: &Url, force: bool) -> bool {
for u in src {
let to = dest.join(u.file_name().unwrap());
if force && u == &to {
trace!("file_copy: same file, skipping {:?}", to);
} else {
self.scheduler.file_copy(u.clone(), to, force, follow);
self.scheduler.file_copy(u.clone(), to, force);
}
}
false
}
pub fn file_link(&self, src: &HashSet<Url>, dest: &Url, relative: bool, force: bool) -> bool {
for u in src {
let to = dest.join(u.file_name().unwrap());
if force && *u == to {
trace!("file_link: same file, skipping {:?}", to);
} else {
self.scheduler.file_link(u.clone(), to, relative, force);
}
}
false

View file

@ -1,9 +1,9 @@
use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};
use std::{borrow::Cow, collections::VecDeque, fs::Metadata, path::{Path, PathBuf}};
use anyhow::Result;
use config::TASKS;
use futures::{future::BoxFuture, FutureExt};
use shared::{calculate_size, copy_with_progress, Url};
use shared::{calculate_size, copy_with_progress, path_relative_to, Url};
use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::trace;
@ -36,11 +36,13 @@ pub(crate) struct FileOpPaste {
#[derive(Clone, Debug)]
pub(crate) struct FileOpLink {
pub id: usize,
pub from: Url,
pub to: Url,
pub cut: bool,
pub length: u64,
pub id: usize,
pub from: Url,
pub to: Url,
pub meta: Option<Metadata>,
pub resolve: bool,
pub relative: bool,
pub delete: bool,
}
#[derive(Clone, Debug)]
@ -114,33 +116,49 @@ impl File {
self.sch.send(TaskOp::Adv(task.id, 1, 0))?;
}
FileOp::Link(task) => {
let src = match fs::read_link(&task.from).await {
Ok(src) => src,
Err(e) if e.kind() == NotFound => {
self.log(task.id, format!("Link task partially done: {:?}", task))?;
return Ok(self.sch.send(TaskOp::Adv(task.id, 1, task.length))?);
let meta = task.meta.as_ref().unwrap();
let src = if task.resolve {
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))?;
return Ok(self.sch.send(TaskOp::Adv(task.id, 1, meta.len()))?);
}
Err(e) => Err(e)?,
}
Err(e) => Err(e)?,
} else {
Cow::Borrowed(task.from.as_path())
};
let src = if task.relative {
path_relative_to(&src, &fs::canonicalize(task.to.parent().unwrap()).await?)
} else {
src
};
match fs::remove_file(&task.to).await {
Err(e) if e.kind() != NotFound => Err(e)?,
_ => {
#[cfg(target_os = "windows")]
{
fs::symlink_file(src, &task.to).await?
}
#[cfg(not(target_os = "windows"))]
#[cfg(unix)]
{
fs::symlink(src, &task.to).await?
}
#[cfg(windows)]
{
if meta.is_dir() {
fs::symlink_dir(src, &task.to).await?
} else {
fs::symlink_file(src, &task.to).await?
}
}
}
}
if task.cut {
if task.delete {
fs::remove_file(&task.from).await.ok();
}
self.sch.send(TaskOp::Adv(task.id, 1, task.length))?;
self.sch.send(TaskOp::Adv(task.id, 1, meta.len()))?;
}
FileOp::Delete(task) => {
if let Err(e) = fs::remove_file(&task.target).await {
@ -192,7 +210,7 @@ impl File {
if meta.is_file() {
self.tx.send(FileOp::Paste(task)).await?;
} else if meta.is_symlink() {
self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?;
self.tx.send(FileOp::Link(task.to_link(meta))).await?;
}
return self.done(id);
}
@ -205,7 +223,7 @@ impl File {
let dest = root.join(src.components().skip(skip).collect::<PathBuf>());
match fs::create_dir(&dest).await {
Err(e) if e.kind() != AlreadyExists => {
self.log(task.id, format!("Create dir failed: {:?}, {e}", dest))?;
self.log(task.id, format!("Create dir failed: {dest:?}, {e}"))?;
continue;
}
_ => {}
@ -214,7 +232,7 @@ impl File {
let mut it = match fs::read_dir(&src).await {
Ok(it) => it,
Err(e) => {
self.log(task.id, format!("Read dir failed: {:?}, {e}", src))?;
self.log(task.id, format!("Read dir failed: {src:?}, {e}"))?;
continue;
}
};
@ -237,13 +255,24 @@ impl File {
if meta.is_file() {
self.tx.send(FileOp::Paste(task.clone())).await?;
} else if meta.is_symlink() {
self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?;
self.tx.send(FileOp::Link(task.to_link(meta))).await?;
}
}
}
self.done(task.id)
}
pub(crate) async fn link(&self, mut task: FileOpLink) -> Result<()> {
let id = task.id;
if task.meta.is_none() {
task.meta = Some(fs::symlink_metadata(&task.from).await?);
}
self.sch.send(TaskOp::New(id, task.meta.as_ref().unwrap().len()))?;
self.tx.send(FileOp::Link(task)).await?;
self.done(id)
}
pub(crate) async fn delete(&self, mut task: FileOpDelete) -> Result<()> {
let meta = fs::symlink_metadata(&task.target).await?;
if !meta.is_dir() {
@ -321,7 +350,15 @@ impl File {
}
impl FileOpPaste {
fn to_link(&self, length: u64) -> FileOpLink {
FileOpLink { id: self.id, from: self.from.clone(), to: self.to.clone(), cut: self.cut, length }
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,
}
}
}

View file

@ -1,4 +1,4 @@
use std::{env, path::{Path, PathBuf}};
use std::{borrow::Cow, env, path::{Component, Path, PathBuf}};
use tokio::fs;
@ -51,7 +51,7 @@ pub fn readable_size(size: u64) -> String {
}
pub async fn unique_path(mut p: Url) -> Url {
let Some(name) = p.file_name().map(|n| n.to_os_string()) else {
let Some(name) = p.file_name().map(|n| n.to_owned()) else {
return p;
};
@ -65,6 +65,51 @@ pub async fn unique_path(mut p: Url) -> Url {
p
}
// Parmaters
// * `path`: The absolute path(contains no `/./`) to get relative path.
// * `root`: The absolute path(contains no `/./`) to be compared.
//
// Return
// * Unix: The relative format to `root` of `path`.
// * Windows: The relative format to `root` of `path`; or `path` itself when
// `path` and `root` are both under different disk drives.
pub fn path_relative_to<'a>(path: &'a Path, root: &Path) -> Cow<'a, Path> {
assert!(path.is_absolute());
assert!(root.is_absolute());
let mut p_comps = path.components();
let mut r_comps = root.components();
// 1. Ensure that the two paths have the same prefix.
// 2. Strips any common prefix the two paths do have.
//
// NOTE:
// Prefixes are platform dependent,
// but different prefixes would for example indicate paths for different drives
// on Windows.
let (p_head, r_head) = loop {
use std::path::Component::*;
match (p_comps.next(), r_comps.next()) {
(Some(RootDir), Some(RootDir)) => (),
(Some(Prefix(a)), Some(Prefix(b))) if a == b => (),
(Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => {
return Cow::from(path);
}
(None, None) => break (None, None),
(a, b) if a != b => break (a, b),
_ => (),
}
};
let p_comps = p_head.into_iter().chain(p_comps);
let walk_up = r_head.into_iter().chain(r_comps).map(|_| Component::ParentDir);
let mut buf = PathBuf::new();
buf.extend(walk_up);
buf.extend(p_comps);
Cow::from(buf)
}
#[inline]
pub fn optional_bool(s: &str) -> Option<bool> {
if s == "true" {
@ -75,3 +120,38 @@ pub fn optional_bool(s: &str) -> Option<bool> {
None
}
}
#[cfg(test)]
mod tests {
use std::{borrow::Cow, path::Path};
use super::path_relative_to;
#[cfg(unix)]
#[test]
fn test_path_relative_to() {
fn assert(path: &str, root: &str, res: &str) {
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
}
assert("/a/b", "/a/b/c", "../");
assert("/a/b/c", "/a/b", "c");
assert("/a/b/c", "/a/b/d", "../c");
assert("/a", "/a/b/c", "../../");
assert("/a/a/b", "/a/b/b", "../../a/b");
}
#[cfg(windows)]
#[test]
fn test_path_relative_to() {
fn assert(path: &str, root: &str, res: &str) {
assert_eq!(path_relative_to(Path::new(path), Path::new(root)), Cow::Borrowed(Path::new(res)));
}
assert("C:\\a\\b", "C:\\a\\b\\c", "..\\");
assert("C:\\a\\b\\c", "C:\\a\\b", "c");
assert("C:\\a\\b\\c", "C:\\a\\b\\d", "..\\c");
assert("C:\\a", "C:\\a\\b\\c", "..\\..\\");
assert("C:\\a\\a\\b", "C:\\a\\b\\b", "..\\..\\a\\b");
}
}