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 {
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());
@ -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 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())
}

View file

@ -1,7 +1,7 @@
use std::{io, path::Path, sync::Arc};
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}};
@ -120,14 +120,14 @@ impl<'a> Provider for Local<'a> {
}
#[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
P: AsPath,
S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>,
{
#[cfg(unix)]
{
let original = original.as_path().as_os()?;
let original = original.as_strand().as_os()?;
tokio::fs::symlink(original, self.path).await
}
#[cfg(windows)]
@ -139,11 +139,11 @@ impl<'a> Provider for Local<'a> {
}
#[inline]
async fn symlink_dir<P>(&self, original: P) -> io::Result<()>
async fn symlink_dir<S>(&self, original: S) -> io::Result<()>
where
P: AsPath,
S: AsStrand,
{
let original = original.as_path().as_os()?;
let original = original.as_strand().as_os()?;
#[cfg(unix)]
{
@ -156,11 +156,11 @@ impl<'a> Provider for Local<'a> {
}
#[inline]
async fn symlink_file<P>(&self, original: P) -> io::Result<()>
async fn symlink_file<S>(&self, original: S) -> io::Result<()>
where
P: AsPath,
S: AsStrand,
{
let original = original.as_path().as_os()?;
let original = original.as_strand().as_os()?;
#[cfg(unix)]
{

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc};
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};
@ -155,21 +155,21 @@ pub trait Provider: Sized {
where
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
P: AsPath,
S: AsStrand,
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
P: AsPath,
S: AsStrand,
{
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
P: AsPath,
S: AsStrand,
{
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 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> {
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));
}
@ -108,8 +111,17 @@ impl File {
}
Ok(n) => self.ops.out(task.id, FileOutPasteDo::Adv(n)),
Err(e) if e.kind() == NotFound => {
warn!("Paste task partially done: {task:?}");
break;
let Ok(cha) = Self::cha(&task.from, task.follow, None).await else {
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)
// Attribute not found (os error 93)
@ -132,10 +144,6 @@ impl File {
}
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 {
ok_or_not_found!(
provider::read_link(&task.from).await,

View file

@ -79,13 +79,7 @@ impl Scheduler {
let mut ongoing = self.ongoing.lock();
let id = ongoing.add::<FileProgPaste>(format!("Cut {} to {}", from.display(), to.display()));
let Ok(prefixed) = to.try_starts_with(&from) else {
return self
.ops
.out(id, FileOutPaste::Fail("Path being cut has a different encoding".to_owned()));
};
if prefixed && !to.covariant(&from) {
if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
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 follow = !from.scheme().covariant(to.scheme());
self.send_micro(id, LOW, async move {
if !force {
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()
));
let Ok(prefixed) = to.try_starts_with(&from) else {
return self
.ops
.out(id, FileOutPaste::Fail("Path being copied has a different encoding".to_owned()));
};
if prefixed && !to.covariant(&from) {
if to.try_starts_with(&from).unwrap_or(false) && !to.covariant(&from) {
return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned()));
}
let file = self.file.clone();
let follow = follow || !from.scheme().covariant(to.scheme());
self.send_micro(id, LOW, async move {
if !force {
to = unique_name(to, must_be_dir(&from)).await?;
@ -160,14 +150,13 @@ impl Scheduler {
to.display()
));
let Ok(prefixed) = to.try_starts_with(&from) else {
return self.ops.out(
id,
FileOutHardlink::Fail("Path being hardlinked has a different encoding".to_owned()),
);
};
if !from.scheme().covariant(to.scheme()) {
return self
.ops
.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
.ops
.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 {
session: Arc<Session>,
@ -12,6 +12,10 @@ pub struct ReadDir {
done: bool,
}
impl Drop for ReadDir {
fn drop(&mut self) { Operator::from(&self.session).close(&self.handle).ok(); }
}
impl ReadDir {
pub(crate) fn new(session: &Arc<Session>, dir: SftpPath, handle: String) -> Self {
Self {
@ -49,7 +53,12 @@ impl ReadDir {
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,
Err(Error::Status(status)) if status.is_eof() => {
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 russh::{ChannelStream, client::Msg};
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};
@ -93,12 +93,7 @@ impl Session {
I: Into<Packet<'a>> + Serialize,
O: TryFrom<Packet<'static>, Error = Error> + 'static,
{
match timeout(Duration::from_secs(30), self.send_sync(input)?).await?? {
Packet::Status(status) if TypeId::of::<O>() != TypeId::of::<responses::Status>() => {
Err(Error::Status(status))
}
response => response.try_into(),
}
self.send_with_timeout(input, Duration::from_secs(45)).await
}
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))
}
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() }
}

View file

@ -7,12 +7,15 @@ use yazi_shared::url::{Url, UrlBuf};
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> {
let src = provider::open(from).await?;
let dist = provider::create(to).await?;
let mut reader = BufReader::with_capacity(524288, src);
let mut writer = BufWriter::with_capacity(524288, dist);
let mut reader = BufReader::with_capacity(BUF_SIZE, src);
let mut writer = BufWriter::with_capacity(BUF_SIZE, dist);
let written = tokio::io::copy(&mut reader, &mut writer).await?;
writer.flush().await?;
@ -33,32 +36,39 @@ pub(super) fn copy_with_progress_impl(
let (acc_, prog_tx_) = (acc.clone(), prog_tx.clone());
tokio::spawn(async move {
let (cha, mut src) = {
let f = provider::open(&*from).await?;
(f.metadata().await?, Some(f))
let init = async {
let src = provider::open(&*from).await?;
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 f = provider::create(&*to).await?;
f.set_len(cha.len).await?;
Some(f)
let (cha, mut src, mut dist) = match init.await {
Ok(r) => r,
Err(e) => {
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)
.map(|i| {
let acc_ = acc_.clone();
let (from, to) = (from.clone(), to.clone());
let (src, dist) = (src.take(), dist.take());
async move {
let offset = i * 5242880;
let take = cha.len.saturating_sub(offset).min(5242880);
let offset = i * PER_CHUNK;
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,
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,
None => Gate::default().write(true).open(&*to).await?,
});
@ -114,7 +124,6 @@ pub(super) fn copy_with_progress_impl(
}
done_tx.send(()).ok();
Ok::<_, io::Error>(())
});
tokio::spawn(async move {

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::sync::mpsc;
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};
@ -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
U: AsUrl,
P: AsPath,
S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>,
{
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
U: AsUrl,
P: AsPath,
S: AsStrand,
{
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
U: AsUrl,
P: AsPath,
S: AsStrand,
{
Providers::new(link.as_url()).await?.symlink_file(original).await
}

View file

@ -2,7 +2,7 @@ use std::io;
use tokio::sync::mpsc;
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)]
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
P: AsPath,
S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>,
{
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
P: AsPath,
S: AsStrand,
{
match self {
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
P: AsPath,
S: AsStrand,
{
match self {
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_fs::{CWD, provider::{DirReader, FileHolder, Provider}};
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 crate::provider::sftp::Conn;
@ -175,14 +175,14 @@ impl<'a> Provider for Sftp<'a> {
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
P: AsPath,
S: AsStrand,
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> {