fix: always follow symlinks when pasting remote files (#3406)

This commit is contained in:
三咲雅 misaki masa 2025-12-05 23:22:47 +08:00 committed by sxyazi
parent 205b7fe11f
commit c569263a50
No known key found for this signature in database
11 changed files with 120 additions and 93 deletions

View file

@ -21,7 +21,7 @@ fn path_relative_to_impl<'a>(from: PathCow<'_>, to: PathCow<'a>) -> Result<PathC
} }
if from == to { if from == to {
return Ok(PathDyn::with_str(to.kind(), ".").into()); return Ok(PathDyn::with_str(from.kind(), ".").into());
} }
let (mut f_it, mut t_it) = (from.components(), to.components()); let (mut f_it, mut t_it) = (from.components(), to.components());
@ -41,7 +41,7 @@ fn path_relative_to_impl<'a>(from: PathCow<'_>, to: PathCow<'a>) -> Result<PathC
let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir); let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir);
let rest = t_head.into_iter().chain(t_it); let rest = t_head.into_iter().chain(t_it);
let buf = PathBufDyn::from_components(to.kind(), dots.chain(rest))?; let buf = PathBufDyn::from_components(from.kind(), dots.chain(rest))?;
Ok(buf.into()) Ok(buf.into())
} }

View file

@ -1,7 +1,7 @@
use std::{io, path::Path, sync::Arc}; use std::{io, path::Path, sync::Arc};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow}}; use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}}; use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Provider}};
@ -120,14 +120,14 @@ impl<'a> Provider for Local<'a> {
} }
#[inline] #[inline]
async fn symlink<P, F>(&self, original: P, _is_dir: F) -> io::Result<()> async fn symlink<S, F>(&self, original: S, _is_dir: F) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
#[cfg(unix)] #[cfg(unix)]
{ {
let original = original.as_path().as_os()?; let original = original.as_strand().as_os()?;
tokio::fs::symlink(original, self.path).await tokio::fs::symlink(original, self.path).await
} }
#[cfg(windows)] #[cfg(windows)]
@ -139,11 +139,11 @@ impl<'a> Provider for Local<'a> {
} }
#[inline] #[inline]
async fn symlink_dir<P>(&self, original: P) -> io::Result<()> async fn symlink_dir<S>(&self, original: S) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
{ {
let original = original.as_path().as_os()?; let original = original.as_strand().as_os()?;
#[cfg(unix)] #[cfg(unix)]
{ {
@ -156,11 +156,11 @@ impl<'a> Provider for Local<'a> {
} }
#[inline] #[inline]
async fn symlink_file<P>(&self, original: P) -> io::Result<()> async fn symlink_file<S>(&self, original: S) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
{ {
let original = original.as_path().as_os()?; let original = original.as_strand().as_os()?;
#[cfg(unix)] #[cfg(unix)]
{ {

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc};
use yazi_macro::ok_or_not_found; use yazi_macro::ok_or_not_found;
use yazi_shared::{path::{AsPath, PathBufDyn}, strand::StrandCow, url::{AsUrl, Url, UrlBuf}}; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}};
use crate::{cha::{Cha, ChaType}, provider::Attrs}; use crate::{cha::{Cha, ChaType}, provider::Attrs};
@ -155,21 +155,21 @@ pub trait Provider: Sized {
where where
P: AsPath; P: AsPath;
fn symlink<P, F>(&self, original: P, _is_dir: F) -> impl Future<Output = io::Result<()>> fn symlink<S, F>(&self, original: S, _is_dir: F) -> impl Future<Output = io::Result<()>>
where where
P: AsPath, S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>; F: AsyncFnOnce() -> io::Result<bool>;
fn symlink_dir<P>(&self, original: P) -> impl Future<Output = io::Result<()>> fn symlink_dir<S>(&self, original: S) -> impl Future<Output = io::Result<()>>
where where
P: AsPath, S: AsStrand,
{ {
self.symlink(original, async || Ok(true)) self.symlink(original, async || Ok(true))
} }
fn symlink_file<P>(&self, original: P) -> impl Future<Output = io::Result<()>> fn symlink_file<S>(&self, original: S) -> impl Future<Output = io::Result<()>>
where where
P: AsPath, S: AsStrand,
{ {
self.symlink(original, async || Ok(false)) self.symlink(original, async || Ok(false))
} }

View file

@ -1,4 +1,4 @@
use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}, time::Duration}; use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}};
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
@ -26,7 +26,10 @@ impl File {
} }
pub(crate) async fn paste(&self, mut task: FileInPaste) -> Result<(), FileOutPaste> { pub(crate) async fn paste(&self, mut task: FileInPaste) -> Result<(), FileOutPaste> {
if task.cut && ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok() { if task.cut
&& !task.follow
&& ok_or_not_found(provider::rename(&task.from, &task.to).await).is_ok()
{
return Ok(self.ops.out(task.id, FileOutPaste::Succ)); return Ok(self.ops.out(task.id, FileOutPaste::Succ));
} }
@ -108,8 +111,17 @@ impl File {
} }
Ok(n) => self.ops.out(task.id, FileOutPasteDo::Adv(n)), Ok(n) => self.ops.out(task.id, FileOutPasteDo::Adv(n)),
Err(e) if e.kind() == NotFound => { Err(e) if e.kind() == NotFound => {
warn!("Paste task partially done: {task:?}"); let Ok(cha) = Self::cha(&task.from, task.follow, None).await else {
break; warn!("Paste task partially done: {task:?}");
break;
};
if cha.is_orphan() || (cha.is_link() && !task.follow) {
task.cha = Some(cha);
return Ok(self.queue(task.into_link(), NORMAL));
}
Err(e)?
} }
// Operation not permitted (os error 1) // Operation not permitted (os error 1)
// Attribute not found (os error 93) // Attribute not found (os error 93)
@ -132,10 +144,6 @@ impl File {
} }
pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> {
if !task.from.scheme().covariant(task.to.scheme()) {
Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))?
}
let mut src: PathCow = if task.resolve { let mut src: PathCow = if task.resolve {
ok_or_not_found!( ok_or_not_found!(
provider::read_link(&task.from).await, provider::read_link(&task.from).await,

View file

@ -79,13 +79,7 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock(); let mut ongoing = self.ongoing.lock();
let id = ongoing.add::<FileProgPaste>(format!("Cut {} to {}", from.display(), to.display())); let id = ongoing.add::<FileProgPaste>(format!("Cut {} to {}", from.display(), to.display()));
let Ok(prefixed) = to.try_starts_with(&from) else { if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
return self
.ops
.out(id, FileOutPaste::Fail("Path being cut has a different encoding".to_owned()));
};
if prefixed && !to.covariant(&from) {
return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned())); return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned()));
} }
@ -103,11 +97,12 @@ impl Scheduler {
}); });
let file = self.file.clone(); let file = self.file.clone();
let follow = !from.scheme().covariant(to.scheme());
self.send_micro(id, LOW, async move { self.send_micro(id, LOW, async move {
if !force { if !force {
to = unique_name(to, must_be_dir(&from)).await?; to = unique_name(to, must_be_dir(&from)).await?;
} }
file.paste(FileInPaste { id, from, to, cha: None, cut: true, follow: false, retry: 0 }).await file.paste(FileInPaste { id, from, to, cha: None, cut: true, follow, retry: 0 }).await
}); });
} }
@ -118,17 +113,12 @@ impl Scheduler {
to.display() to.display()
)); ));
let Ok(prefixed) = to.try_starts_with(&from) else { if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
return self
.ops
.out(id, FileOutPaste::Fail("Path being copied has a different encoding".to_owned()));
};
if prefixed && !to.covariant(&from) {
return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned())); return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned()));
} }
let file = self.file.clone(); let file = self.file.clone();
let follow = follow || !from.scheme().covariant(to.scheme());
self.send_micro(id, LOW, async move { self.send_micro(id, LOW, async move {
if !force { if !force {
to = unique_name(to, must_be_dir(&from)).await?; to = unique_name(to, must_be_dir(&from)).await?;
@ -160,14 +150,13 @@ impl Scheduler {
to.display() to.display()
)); ));
let Ok(prefixed) = to.try_starts_with(&from) else { if !from.scheme().covariant(to.scheme()) {
return self.ops.out( return self
id, .ops
FileOutHardlink::Fail("Path being hardlinked has a different encoding".to_owned()), .out(id, FileOutHardlink::Fail("Cannot hardlink cross filesystem".to_owned()));
); }
};
if prefixed && !to.covariant(&from) { if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
return self return self
.ops .ops
.out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned())); .out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned()));

View file

@ -1,6 +1,6 @@
use std::{mem, sync::Arc}; use std::{mem, sync::Arc, time::Duration};
use crate::{Error, Session, SftpPath, fs::DirEntry, requests, responses}; use crate::{Error, Operator, Session, SftpPath, fs::DirEntry, requests, responses};
pub struct ReadDir { pub struct ReadDir {
session: Arc<Session>, session: Arc<Session>,
@ -12,6 +12,10 @@ pub struct ReadDir {
done: bool, done: bool,
} }
impl Drop for ReadDir {
fn drop(&mut self) { Operator::from(&self.session).close(&self.handle).ok(); }
}
impl ReadDir { impl ReadDir {
pub(crate) fn new(session: &Arc<Session>, dir: SftpPath, handle: String) -> Self { pub(crate) fn new(session: &Arc<Session>, dir: SftpPath, handle: String) -> Self {
Self { Self {
@ -49,7 +53,12 @@ impl ReadDir {
return Ok(()); return Ok(());
} }
self.name = match self.session.send(requests::ReadDir::new(&self.handle)).await { let result = self
.session
.send_with_timeout(requests::ReadDir::new(&self.handle), Duration::from_mins(5))
.await;
self.name = match result {
Ok(resp) => resp, Ok(resp) => resp,
Err(Error::Status(status)) if status.is_eof() => { Err(Error::Status(status)) if status.is_eof() => {
self.done = true; self.done = true;

View file

@ -3,7 +3,7 @@ use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc, t
use parking_lot::Mutex; use parking_lot::Mutex;
use russh::{ChannelStream, client::Msg}; use russh::{ChannelStream, client::Msg};
use serde::Serialize; use serde::Serialize;
use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}, time::timeout}; use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}};
use crate::{Error, Id, Packet, Receiver, responses}; use crate::{Error, Id, Packet, Receiver, responses};
@ -93,12 +93,7 @@ impl Session {
I: Into<Packet<'a>> + Serialize, I: Into<Packet<'a>> + Serialize,
O: TryFrom<Packet<'static>, Error = Error> + 'static, O: TryFrom<Packet<'static>, Error = Error> + 'static,
{ {
match timeout(Duration::from_secs(30), self.send_sync(input)?).await?? { self.send_with_timeout(input, Duration::from_secs(45)).await
Packet::Status(status) if TypeId::of::<O>() != TypeId::of::<responses::Status>() => {
Err(Error::Status(status))
}
response => response.try_into(),
}
} }
pub fn send_sync<'a, I>(self: &Arc<Self>, input: I) -> Result<Receiver, Error> pub fn send_sync<'a, I>(self: &Arc<Self>, input: I) -> Result<Receiver, Error>
@ -118,5 +113,22 @@ impl Session {
Ok(Receiver::new(self, id, rx)) Ok(Receiver::new(self, id, rx))
} }
pub async fn send_with_timeout<'a, I, O>(
self: &Arc<Self>,
input: I,
timeout: Duration,
) -> Result<O, Error>
where
I: Into<Packet<'a>> + Serialize,
O: TryFrom<Packet<'static>, Error = Error> + 'static,
{
match tokio::time::timeout(timeout, self.send_sync(input)?).await?? {
Packet::Status(status) if TypeId::of::<O>() != TypeId::of::<responses::Status>() => {
Err(Error::Status(status))
}
response => response.try_into(),
}
}
pub fn is_closed(self: &Arc<Self>) -> bool { self.tx.is_closed() } pub fn is_closed(self: &Arc<Self>) -> bool { self.tx.is_closed() }
} }

View file

@ -7,12 +7,15 @@ use yazi_shared::url::{Url, UrlBuf};
use crate::provider::{self, Gate}; use crate::provider::{self, Gate};
const BUF_SIZE: usize = 512 * 1024;
const PER_CHUNK: u64 = 8 * 1024 * 1024;
pub(super) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: Attrs) -> io::Result<u64> { pub(super) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: Attrs) -> io::Result<u64> {
let src = provider::open(from).await?; let src = provider::open(from).await?;
let dist = provider::create(to).await?; let dist = provider::create(to).await?;
let mut reader = BufReader::with_capacity(524288, src); let mut reader = BufReader::with_capacity(BUF_SIZE, src);
let mut writer = BufWriter::with_capacity(524288, dist); let mut writer = BufWriter::with_capacity(BUF_SIZE, dist);
let written = tokio::io::copy(&mut reader, &mut writer).await?; let written = tokio::io::copy(&mut reader, &mut writer).await?;
writer.flush().await?; writer.flush().await?;
@ -33,32 +36,39 @@ pub(super) fn copy_with_progress_impl(
let (acc_, prog_tx_) = (acc.clone(), prog_tx.clone()); let (acc_, prog_tx_) = (acc.clone(), prog_tx.clone());
tokio::spawn(async move { tokio::spawn(async move {
let (cha, mut src) = { let init = async {
let f = provider::open(&*from).await?; let src = provider::open(&*from).await?;
(f.metadata().await?, Some(f)) let cha = src.metadata().await?;
let dist = provider::create(&*to).await?;
dist.set_len(cha.len).await?;
Ok((cha, Some(src), Some(dist)))
}; };
let mut dist = { let (cha, mut src, mut dist) = match init.await {
let f = provider::create(&*to).await?; Ok(r) => r,
f.set_len(cha.len).await?; Err(e) => {
Some(f) prog_tx_.send(Err(e)).await.ok();
done_tx.send(()).ok();
return;
}
}; };
let chunks = (cha.len + 5242880 - 1) / 5242880; let chunks = (cha.len + PER_CHUNK - 1) / PER_CHUNK;
let mut result = futures::stream::iter(0..chunks) let mut result = futures::stream::iter(0..chunks)
.map(|i| { .map(|i| {
let acc_ = acc_.clone(); let acc_ = acc_.clone();
let (from, to) = (from.clone(), to.clone()); let (from, to) = (from.clone(), to.clone());
let (src, dist) = (src.take(), dist.take()); let (src, dist) = (src.take(), dist.take());
async move { async move {
let offset = i * 5242880; let offset = i * PER_CHUNK;
let take = cha.len.saturating_sub(offset).min(5242880); let take = cha.len.saturating_sub(offset).min(PER_CHUNK);
let mut src = BufReader::with_capacity(524288, match src { let mut src = BufReader::with_capacity(BUF_SIZE, match src {
Some(f) => f, Some(f) => f,
None => provider::open(&*from).await?, None => provider::open(&*from).await?,
}); });
let mut dist = BufWriter::with_capacity(524288, match dist { let mut dist = BufWriter::with_capacity(BUF_SIZE, match dist {
Some(f) => f, Some(f) => f,
None => Gate::default().write(true).open(&*to).await?, None => Gate::default().write(true).open(&*to).await?,
}); });
@ -114,7 +124,6 @@ pub(super) fn copy_with_progress_impl(
} }
done_tx.send(()).ok(); done_tx.send(()).ok();
Ok::<_, io::Error>(())
}); });
tokio::spawn(async move { tokio::spawn(async move {

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}}; use yazi_fs::{cha::Cha, provider::{Attrs, Provider, local::Local}};
use yazi_shared::{path::{AsPath, PathBufDyn}, url::{AsUrl, UrlBuf, UrlCow}}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, UrlBuf, UrlCow}};
use super::{Providers, ReadDir, RwFile}; use super::{Providers, ReadDir, RwFile};
@ -201,27 +201,27 @@ where
} }
} }
pub async fn symlink<U, P, F>(link: U, original: P, is_dir: F) -> io::Result<()> pub async fn symlink<U, S, F>(link: U, original: S, is_dir: F) -> io::Result<()>
where where
U: AsUrl, U: AsUrl,
P: AsPath, S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
Providers::new(link.as_url()).await?.symlink(original, is_dir).await Providers::new(link.as_url()).await?.symlink(original, is_dir).await
} }
pub async fn symlink_dir<U, P>(link: U, original: P) -> io::Result<()> pub async fn symlink_dir<U, S>(link: U, original: S) -> io::Result<()>
where where
U: AsUrl, U: AsUrl,
P: AsPath, S: AsStrand,
{ {
Providers::new(link.as_url()).await?.symlink_dir(original).await Providers::new(link.as_url()).await?.symlink_dir(original).await
} }
pub async fn symlink_file<U, P>(link: U, original: P) -> io::Result<()> pub async fn symlink_file<U, S>(link: U, original: S) -> io::Result<()>
where where
U: AsUrl, U: AsUrl,
P: AsPath, S: AsStrand,
{ {
Providers::new(link.as_url()).await?.symlink_file(original).await Providers::new(link.as_url()).await?.symlink_file(original).await
} }

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_fs::{cha::Cha, provider::{Attrs, Provider}}; use yazi_fs::{cha::Cha, provider::{Attrs, Provider}};
use yazi_shared::{path::{AsPath, PathBufDyn}, url::{Url, UrlBuf, UrlCow}}; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}};
#[derive(Clone)] #[derive(Clone)]
pub(super) enum Providers<'a> { pub(super) enum Providers<'a> {
@ -161,9 +161,9 @@ impl<'a> Provider for Providers<'a> {
} }
} }
async fn symlink<P, F>(&self, original: P, is_dir: F) -> io::Result<()> async fn symlink<S, F>(&self, original: S, is_dir: F) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
match self { match self {
@ -172,9 +172,9 @@ impl<'a> Provider for Providers<'a> {
} }
} }
async fn symlink_dir<P>(&self, original: P) -> io::Result<()> async fn symlink_dir<S>(&self, original: S) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
{ {
match self { match self {
Self::Local(p) => p.symlink_dir(original).await, Self::Local(p) => p.symlink_dir(original).await,
@ -182,9 +182,9 @@ impl<'a> Provider for Providers<'a> {
} }
} }
async fn symlink_file<P>(&self, original: P) -> io::Result<()> async fn symlink_file<S>(&self, original: S) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
{ {
match self { match self {
Self::Local(p) => p.symlink_file(original).await, Self::Local(p) => p.symlink_file(original).await,

View file

@ -4,7 +4,7 @@ use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver};
use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_config::vfs::{ServiceSftp, Vfs};
use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}}; use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}};
use yazi_sftp::fs::{Attrs, Flags}; use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow, UrlLike}}; use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}};
use super::Cha; use super::Cha;
use crate::provider::sftp::Conn; use crate::provider::sftp::Conn;
@ -175,14 +175,14 @@ impl<'a> Provider for Sftp<'a> {
Ok(()) Ok(())
} }
async fn symlink<P, F>(&self, original: P, _is_dir: F) -> io::Result<()> async fn symlink<S, F>(&self, original: S, _is_dir: F) -> io::Result<()>
where where
P: AsPath, S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
let original = original.as_path().as_unix()?; let original = original.as_strand().encoded_bytes();
Ok(self.op().await?.symlink(&original, self.path).await?) Ok(self.op().await?.symlink(original, self.path).await?)
} }
async fn symlink_metadata(&self) -> io::Result<yazi_fs::cha::Cha> { async fn symlink_metadata(&self) -> io::Result<yazi_fs::cha::Cha> {