feat(s3): add S3 bucket browsing as a VFS provider

Implement a read-only S3 provider built on the `object_store` crate,
allowing users to browse S3 buckets natively within yazi.

- Add `ServiceS3` config (region, endpoint, credentials, force_path_style, allow_http)
- Add `S3` variant to `Url` enum and `SchemeKind`
- Implement read_dir with paginated listing, metadata, and streaming read
- Write operations return `Unsupported` for now
This commit is contained in:
Xie Zejian 2026-04-01 14:07:03 +08:00 committed by distsystem
parent 4857d46918
commit 558622e512
36 changed files with 1731 additions and 22 deletions

804
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,15 +7,28 @@ use crate::normalize_path;
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum Service {
S3(ServiceS3),
Sftp(ServiceSftp),
}
impl TryFrom<&'static Service> for &'static ServiceS3 {
type Error = &'static str;
fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value {
Service::S3(p) => Ok(p),
_ => Err("expected s3 service"),
}
}
}
impl TryFrom<&'static Service> for &'static ServiceSftp {
type Error = &'static str;
fn try_from(value: &'static Service) -> Result<Self, Self::Error> {
match value {
Service::Sftp(p) => Ok(p),
_ => Err("expected sftp service"),
}
}
}
@ -23,11 +36,48 @@ impl TryFrom<&'static Service> for &'static ServiceSftp {
impl Service {
pub(super) fn reshape(&mut self) -> io::Result<()> {
match self {
Self::S3(p) => p.reshape(),
Self::Sftp(p) => p.reshape(),
}
}
}
// --- S3
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ServiceS3 {
pub region: Option<String>,
pub endpoint: Option<String>,
pub access_key_id: Option<String>,
pub secret_access_key: Option<String>,
pub session_token: Option<String>,
#[serde(default)]
pub force_path_style: bool,
#[serde(default)]
pub allow_http: bool,
}
impl ServiceS3 {
fn reshape(&mut self) -> io::Result<()> {
self.region = trim_option(self.region.take());
self.endpoint = self.endpoint.take().and_then(|s| {
let s = s.trim().trim_end_matches('/').to_owned();
(!s.is_empty()).then_some(s)
});
self.access_key_id = trim_option(self.access_key_id.take());
self.secret_access_key = trim_option(self.secret_access_key.take());
self.session_token = trim_option(self.session_token.take());
Ok(())
}
}
fn trim_option(value: Option<String>) -> Option<String> {
value.and_then(|s| {
let s = s.trim().to_owned();
(!s.is_empty()).then_some(s)
})
}
// --- SFTP
#[derive(Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ServiceSftp {

View file

@ -39,6 +39,10 @@ fn expand_url_impl(url: UrlCow) -> UrlCow {
loc: LocBuf::<std::path::PathBuf>::with(path.into_os().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
},
Url::S3 { domain, .. } => UrlBuf::S3 {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),
},
Url::Sftp { domain, .. } => UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::with(path.into_unix().unwrap(), uri, urn).unwrap(),
domain: domain.intern(),

View file

@ -51,6 +51,6 @@ fn try_absolute_impl<'a>(url: UrlCow<'a>) -> Option<UrlCow<'a>> {
Some(match url.as_url() {
Url::Regular(_) => UrlBuf::Regular(loc).into(),
Url::Search { domain, .. } => UrlBuf::Search { loc, domain: domain.intern() }.into(),
Url::Archive { .. } | Url::Sftp { .. } => None?,
Url::Archive { .. } | Url::S3 { .. } | Url::Sftp { .. } => None?,
})
}

View file

@ -80,7 +80,7 @@ impl<'a> Provider for Local<'a> {
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
match url {
Url::Regular(loc) | Url::Search { loc, .. } => Ok(Self::Me { url, path: loc.as_inner() }),
Url::Archive { .. } | Url::Sftp { .. } => {
Url::Archive { .. } | Url::S3 { .. } | Url::Sftp { .. } => {
Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a local URL: {url:?}")))
}
}
@ -94,7 +94,7 @@ impl<'a> Provider for Local<'a> {
reader: tokio::fs::read_dir(self.path).await?,
dir: Arc::new(self.url.to_owned()),
},
SchemeKind::Archive | SchemeKind::Sftp => Err(io::Error::new(
SchemeKind::Archive | SchemeKind::S3 | SchemeKind::Sftp => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Not a local URL: {:?}", self.url),
))?,

View file

@ -15,6 +15,9 @@ impl FsScheme for SchemeRef<'_> {
Self::Archive { domain, .. } => Some(
Xdg::cache_dir().join(format!("archive-{}", yazi_shared::scheme::Encode::domain(domain))),
),
Self::S3 { domain, .. } => {
Some(Xdg::cache_dir().join(format!("s3-{}", yazi_shared::scheme::Encode::domain(domain))))
}
Self::Sftp { domain, .. } => {
Some(Xdg::cache_dir().join(format!("sftp-{}", yazi_shared::scheme::Encode::domain(domain))))
}

View file

@ -49,7 +49,7 @@ impl<'a> FsUrl<'a> for Url<'a> {
fn unified_path(self) -> Cow<'a, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(),
Self::Archive { .. } | Self::Sftp { .. } => {
Self::Archive { .. } | Self::S3 { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into()
}
}
@ -64,7 +64,7 @@ impl FsUrl<'_> for UrlBuf {
fn unified_path(self) -> Cow<'static, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(),
Self::Archive { .. } | Self::Sftp { .. } => {
Self::Archive { .. } | Self::S3 { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into()
}
}
@ -79,7 +79,7 @@ impl<'a> FsUrl<'a> for UrlCow<'a> {
fn unified_path(self) -> Cow<'a, Path> {
match self {
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(),
Self::Archive { .. } | Self::Sftp { .. } => {
Self::Archive { .. } | Self::S3 { .. } | Self::Sftp { .. } => {
self.cache().expect("non-local URL should have a cache path").into()
}
}

View file

@ -155,6 +155,7 @@ where
SchemeKind::Regular => Self::bare(path),
SchemeKind::Search => Self::zeroed(path),
SchemeKind::Archive => Self::zeroed(path),
SchemeKind::S3 => Self::bare(path),
SchemeKind::Sftp => Self::bare(path),
}
}

View file

@ -11,6 +11,7 @@ impl From<SchemeKind> for PathKind {
SchemeKind::Regular => Self::Os,
SchemeKind::Search => Self::Os,
SchemeKind::Archive => Self::Os,
SchemeKind::S3 => Self::Unix,
SchemeKind::Sftp => Self::Unix,
}
}

View file

@ -57,6 +57,16 @@ impl<'a> SchemeCow<'a> {
}
}
pub fn s3<T>(domain: T, uri: usize, urn: usize) -> Self
where
T: Into<Cow<'a, str>>,
{
match domain.into() {
Cow::Borrowed(domain) => SchemeRef::S3 { domain, uri, urn }.into(),
Cow::Owned(domain) => Scheme::S3 { domain: domain.intern(), uri, urn }.into(),
}
}
pub fn sftp<T>(domain: T, uri: usize, urn: usize) -> Self
where
T: Into<Cow<'a, str>>,
@ -80,6 +90,7 @@ impl<'a> SchemeCow<'a> {
SchemeKind::Regular => ("".into(), None, None),
SchemeKind::Search => Self::decode_param(&bytes[skip..], &mut skip)?,
SchemeKind::Archive => Self::decode_param(&bytes[skip..], &mut skip)?,
SchemeKind::S3 => Self::decode_param(&bytes[skip..], &mut skip)?,
SchemeKind::Sftp => Self::decode_param(&bytes[skip..], &mut skip)?,
};
@ -92,6 +103,7 @@ impl<'a> SchemeCow<'a> {
SchemeKind::Regular => Self::regular(uri, urn),
SchemeKind::Search => Self::search(domain, uri, urn),
SchemeKind::Archive => Self::archive(domain, uri, urn),
SchemeKind::S3 => Self::s3(domain, uri, urn),
SchemeKind::Sftp => Self::sftp(domain, uri, urn),
};
@ -154,6 +166,11 @@ impl<'a> SchemeCow<'a> {
(uri, urn)
}
SchemeKind::Archive => (uri.unwrap_or(0), urn.unwrap_or(0)),
SchemeKind::S3 => {
let uri = uri.unwrap_or(path.name().is_some() as usize);
let urn = urn.unwrap_or(path.name().is_some() as usize);
(uri, urn)
}
SchemeKind::Sftp => {
let uri = uri.unwrap_or(path.name().is_some() as usize);
let urn = urn.unwrap_or(path.name().is_some() as usize);
@ -167,6 +184,7 @@ impl<'a> SchemeCow<'a> {
Url::Regular(loc) => (loc.file_name().is_some() as usize, loc.file_name().is_some() as usize),
Url::Search { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
Url::Archive { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
Url::S3 { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
Url::Sftp { loc, .. } => (loc.uri().components().count(), loc.urn().components().count()),
}
}

View file

@ -38,7 +38,7 @@ impl<'a> Encode<'a> {
match self.0.0.kind() {
SchemeKind::Regular => Ok(()),
SchemeKind::Search | SchemeKind::Archive => w!(0, 0),
SchemeKind::Sftp => {
SchemeKind::S3 | SchemeKind::Sftp => {
w!(self.0.0.loc().name().is_some() as usize, self.0.0.loc().name().is_some() as usize)
}
}
@ -57,6 +57,7 @@ impl Display for Encode<'_> {
Url::Archive { domain, .. } => {
write!(f, "archive://{}{}/", Self::domain(domain), self.ports())
}
Url::S3 { domain, .. } => write!(f, "s3://{}{}/", Self::domain(domain), self.ports()),
Url::Sftp { domain, .. } => write!(f, "sftp://{}{}/", Self::domain(domain), self.ports()),
}
}

View file

@ -9,6 +9,7 @@ pub enum SchemeKind {
Regular,
Search,
Archive,
S3,
Sftp,
}
@ -21,6 +22,7 @@ where
SchemeRef::Regular { .. } => Self::Regular,
SchemeRef::Search { .. } => Self::Search,
SchemeRef::Archive { .. } => Self::Archive,
SchemeRef::S3 { .. } => Self::S3,
SchemeRef::Sftp { .. } => Self::Sftp,
}
}
@ -34,6 +36,7 @@ impl TryFrom<&[u8]> for SchemeKind {
b"regular" => Ok(Self::Regular),
b"search" => Ok(Self::Search),
b"archive" => Ok(Self::Archive),
b"s3" => Ok(Self::S3),
b"sftp" => Ok(Self::Sftp),
_ => bail!("invalid scheme kind: {}", String::from_utf8_lossy(value)),
}
@ -45,7 +48,7 @@ impl SchemeKind {
pub fn is_local(self) -> bool {
match self {
Self::Regular | Self::Search => true,
Self::Archive | Self::Sftp => false,
Self::Archive | Self::S3 | Self::Sftp => false,
}
}
@ -53,7 +56,7 @@ impl SchemeKind {
pub fn is_remote(self) -> bool {
match self {
Self::Regular | Self::Search | Self::Archive => false,
Self::Sftp => true,
Self::S3 | Self::Sftp => true,
}
}
@ -61,7 +64,7 @@ impl SchemeKind {
pub fn is_virtual(self) -> bool {
match self {
Self::Regular | Self::Search => false,
Self::Archive | Self::Sftp => true,
Self::Archive | Self::S3 | Self::Sftp => true,
}
}

View file

@ -7,6 +7,7 @@ pub enum SchemeRef<'a> {
Regular { uri: usize, urn: usize },
Search { domain: &'a str, uri: usize, urn: usize },
Archive { domain: &'a str, uri: usize, urn: usize },
S3 { domain: &'a str, uri: usize, urn: usize },
Sftp { domain: &'a str, uri: usize, urn: usize },
}
@ -18,6 +19,7 @@ impl Deref for SchemeRef<'_> {
Self::Regular { .. } => &SchemeKind::Regular,
Self::Search { .. } => &SchemeKind::Search,
Self::Archive { .. } => &SchemeKind::Archive,
Self::S3 { .. } => &SchemeKind::S3,
Self::Sftp { .. } => &SchemeKind::Sftp,
}
}
@ -49,9 +51,10 @@ impl<'a> SchemeRef<'a> {
pub const fn domain(self) -> Option<&'a str> {
match self {
Self::Regular { .. } => None,
Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => {
Some(domain)
}
Self::Search { domain, .. }
| Self::Archive { domain, .. }
| Self::S3 { domain, .. }
| Self::Sftp { domain, .. } => Some(domain),
}
}
@ -60,6 +63,7 @@ impl<'a> SchemeRef<'a> {
Self::Regular { .. } => SchemeKind::Regular,
Self::Search { .. } => SchemeKind::Search,
Self::Archive { .. } => SchemeKind::Archive,
Self::S3 { .. } => SchemeKind::S3,
Self::Sftp { .. } => SchemeKind::Sftp,
}
}
@ -69,6 +73,7 @@ impl<'a> SchemeRef<'a> {
Self::Regular { uri, urn } => (uri, urn),
Self::Search { uri, urn, .. } => (uri, urn),
Self::Archive { uri, urn, .. } => (uri, urn),
Self::S3 { uri, urn, .. } => (uri, urn),
Self::Sftp { uri, urn, .. } => (uri, urn),
}
}
@ -78,6 +83,7 @@ impl<'a> SchemeRef<'a> {
Self::Regular { uri, urn } => Scheme::Regular { uri, urn },
Self::Search { domain, uri, urn } => Scheme::Search { domain: domain.intern(), uri, urn },
Self::Archive { domain, uri, urn } => Scheme::Archive { domain: domain.intern(), uri, urn },
Self::S3 { domain, uri, urn } => Scheme::S3 { domain: domain.intern(), uri, urn },
Self::Sftp { domain, uri, urn } => Scheme::Sftp { domain: domain.intern(), uri, urn },
}
}
@ -87,6 +93,7 @@ impl<'a> SchemeRef<'a> {
Self::Regular { .. } => Self::Regular { uri, urn },
Self::Search { domain, .. } => Self::Search { domain, uri, urn },
Self::Archive { domain, .. } => Self::Archive { domain, uri, urn },
Self::S3 { domain, .. } => Self::S3 { domain, uri, urn },
Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn },
}
}

View file

@ -10,6 +10,7 @@ pub enum Scheme {
Regular { uri: usize, urn: usize },
Search { domain: Symbol<str>, uri: usize, urn: usize },
Archive { domain: Symbol<str>, uri: usize, urn: usize },
S3 { domain: Symbol<str>, uri: usize, urn: usize },
Sftp { domain: Symbol<str>, uri: usize, urn: usize },
}
@ -26,9 +27,10 @@ impl Scheme {
pub fn into_domain(self) -> Option<Symbol<str>> {
match self {
Self::Regular { .. } => None,
Self::Search { domain, .. } | Self::Archive { domain, .. } | Self::Sftp { domain, .. } => {
Some(domain)
}
Self::Search { domain, .. }
| Self::Archive { domain, .. }
| Self::S3 { domain, .. }
| Self::Sftp { domain, .. } => Some(domain),
}
}
@ -38,6 +40,7 @@ impl Scheme {
Self::Regular { .. } => Self::Regular { uri, urn },
Self::Search { domain, .. } => Self::Search { domain, uri, urn },
Self::Archive { domain, .. } => Self::Archive { domain, uri, urn },
Self::S3 { domain, .. } => Self::S3 { domain, uri, urn },
Self::Sftp { domain, .. } => Self::Sftp { domain, uri, urn },
}
}

View file

@ -16,6 +16,7 @@ impl AsScheme for Scheme {
Self::Regular { uri, urn } => SchemeRef::Regular { uri, urn },
Self::Search { ref domain, uri, urn } => SchemeRef::Search { domain, uri, urn },
Self::Archive { ref domain, uri, urn } => SchemeRef::Archive { domain, uri, urn },
Self::S3 { ref domain, uri, urn } => SchemeRef::S3 { domain, uri, urn },
Self::Sftp { ref domain, uri, urn } => SchemeRef::Sftp { domain, uri, urn },
}
}

View file

@ -22,6 +22,7 @@ impl From<SchemeKind> for StrandKind {
SchemeKind::Regular => Self::Os,
SchemeKind::Search => Self::Os,
SchemeKind::Archive => Self::Os,
SchemeKind::S3 => Self::Bytes,
SchemeKind::Sftp => Self::Bytes,
}
}

View file

@ -10,6 +10,7 @@ pub enum UrlBuf {
Regular(LocBuf),
Search { loc: LocBuf, domain: Symbol<str> },
Archive { loc: LocBuf, domain: Symbol<str> },
S3 { loc: LocBuf<typed_path::UnixPathBuf>, domain: Symbol<str> },
Sftp { loc: LocBuf<typed_path::UnixPathBuf>, domain: Symbol<str> },
}
@ -28,6 +29,7 @@ impl From<Url<'_>> for UrlBuf {
Url::Regular(loc) => Self::Regular(loc.into()),
Url::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.intern() },
Url::Archive { loc, domain } => Self::Archive { loc: loc.into(), domain: domain.intern() },
Url::S3 { loc, domain } => Self::S3 { loc: loc.into(), domain: domain.intern() },
Url::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.intern() },
}
}
@ -134,6 +136,7 @@ impl UrlBuf {
Self::Regular(loc) => loc.into_inner().into(),
Self::Search { loc, .. } => loc.into_inner().into(),
Self::Archive { loc, .. } => loc.into_inner().into(),
Self::S3 { loc, .. } => loc.into_inner().into(),
Self::Sftp { loc, .. } => loc.into_inner().into(),
}
}
@ -149,6 +152,7 @@ impl UrlBuf {
Self::Regular(loc) => loc.try_set_name(name.as_os()?)?,
Self::Search { loc, .. } => loc.try_set_name(name.as_os()?)?,
Self::Archive { loc, .. } => loc.try_set_name(name.as_os()?)?,
Self::S3 { loc, .. } => loc.try_set_name(name.encoded_bytes())?,
Self::Sftp { loc, .. } => loc.try_set_name(name.encoded_bytes())?,
})
}
@ -162,7 +166,8 @@ impl UrlBuf {
Self::Archive { loc, domain } => {
Self::Archive { loc: loc.rebase(base), domain: domain.clone() }
}
Self::Sftp { loc, domain } => {
Self::S3 { .. } => self.clone(),
Self::Sftp { loc: _, domain: _ } => {
todo!();
// Self::Sftp { loc: loc.rebase(base), domain: domain.clone() }
}

View file

@ -59,6 +59,7 @@ impl<'a> Components<'a> {
Url::Regular(_) => SchemeRef::Regular { uri, urn },
Url::Search { domain, .. } => SchemeRef::Search { domain, uri, urn },
Url::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn },
Url::S3 { domain, .. } => SchemeRef::S3 { domain, uri, urn },
Url::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn },
}
}
@ -86,6 +87,9 @@ impl<'a> Components<'a> {
Url::Archive { domain, .. } => {
Url::Archive { loc: Loc::with(path.as_os().unwrap(), uri, urn).unwrap(), domain }
}
Url::S3 { domain, .. } => {
Url::S3 { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), domain }
}
Url::Sftp { domain, .. } => {
Url::Sftp { loc: Loc::with(path.as_unix().unwrap(), uri, urn).unwrap(), domain }
}

View file

@ -11,6 +11,7 @@ pub enum UrlCow<'a> {
Regular(LocCow<'a>),
Search { loc: LocCow<'a>, domain: SymbolCow<'a, str> },
Archive { loc: LocCow<'a>, domain: SymbolCow<'a, str> },
S3 { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, domain: SymbolCow<'a, str> },
Sftp { loc: LocCow<'a, &'a UnixPath, UnixPathBuf>, domain: SymbolCow<'a, str> },
}
@ -25,6 +26,7 @@ impl<'a> From<Url<'a>> for UrlCow<'a> {
Url::Regular(loc) => Self::Regular(loc.into()),
Url::Search { loc, domain } => Self::Search { loc: loc.into(), domain: domain.into() },
Url::Archive { loc, domain } => Self::Archive { loc: loc.into(), domain: domain.into() },
Url::S3 { loc, domain } => Self::S3 { loc: loc.into(), domain: domain.into() },
Url::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.into() },
}
}
@ -45,6 +47,7 @@ impl From<UrlBuf> for UrlCow<'_> {
UrlBuf::Archive { loc, domain } => {
Self::Archive { loc: loc.into(), domain: domain.into() }
}
UrlBuf::S3 { loc, domain } => Self::S3 { loc: loc.into(), domain: domain.into() },
UrlBuf::Sftp { loc, domain } => Self::Sftp { loc: loc.into(), domain: domain.into() },
}
}
@ -153,6 +156,10 @@ impl<'a> TryFrom<(SchemeCow<'a>, PathDyn<'a>)> for UrlCow<'a> {
loc: Loc::with(path.as_os()?, uri, urn)?.into(),
domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?,
},
SchemeKind::S3 => Self::S3 {
loc: Loc::with(path.as_unix()?, uri, urn)?.into(),
domain: domain.ok_or_else(|| anyhow!("missing domain for s3 scheme"))?,
},
SchemeKind::Sftp => Self::Sftp {
loc: Loc::with(path.as_unix()?, uri, urn)?.into(),
domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?,
@ -180,6 +187,10 @@ impl<'a> TryFrom<(SchemeCow<'a>, PathBufDyn)> for UrlCow<'a> {
loc: LocBuf::<std::path::PathBuf>::with(path.try_into()?, uri, urn)?.into(),
domain: domain.ok_or_else(|| anyhow!("missing domain for archive scheme"))?,
},
SchemeKind::S3 => Self::S3 {
loc: LocBuf::<UnixPathBuf>::with(path.try_into()?, uri, urn)?.into(),
domain: domain.ok_or_else(|| anyhow!("missing domain for s3 scheme"))?,
},
SchemeKind::Sftp => Self::Sftp {
loc: LocBuf::<UnixPathBuf>::with(path.try_into()?, uri, urn)?.into(),
domain: domain.ok_or_else(|| anyhow!("missing domain for sftp scheme"))?,
@ -210,6 +221,7 @@ impl<'a> UrlCow<'a> {
Self::Regular(loc) => loc.is_owned(),
Self::Search { loc, .. } => loc.is_owned(),
Self::Archive { loc, .. } => loc.is_owned(),
Self::S3 { loc, .. } => loc.is_owned(),
Self::Sftp { loc, .. } => loc.is_owned(),
}
}
@ -223,9 +235,13 @@ impl<'a> UrlCow<'a> {
Self::Archive { loc, domain } => {
UrlBuf::Archive { loc: loc.into_owned(), domain: domain.into() }
}
Self::S3 { loc, domain } => {
UrlBuf::S3 { loc: loc.into_owned(), domain: domain.into() }
}
Self::Sftp { loc, domain } => {
UrlBuf::Sftp { loc: loc.into_owned(), domain: domain.into() }
}
}
}
@ -248,6 +264,12 @@ impl<'a> UrlCow<'a> {
}
SymbolCow::Owned(domain) => (Scheme::Archive { domain, uri, urn }.into(), loc.into_path()),
},
Self::S3 { loc, domain } => match domain {
SymbolCow::Borrowed(domain) => {
(SchemeRef::S3 { domain, uri, urn }.into(), loc.into_path())
}
SymbolCow::Owned(domain) => (Scheme::S3 { domain, uri, urn }.into(), loc.into_path()),
},
Self::Sftp { loc, domain } => match domain {
SymbolCow::Borrowed(domain) => {
(SchemeRef::Sftp { domain, uri, urn }.into(), loc.into_path())
@ -270,6 +292,9 @@ impl<'a> UrlCow<'a> {
UrlCow::Archive { loc, domain } => {
UrlCow::Archive { loc: loc.into_owned().into(), domain: domain.into_owned().into() }
}
UrlCow::S3 { loc, domain } => {
UrlCow::S3 { loc: loc.into_owned().into(), domain: domain.into_owned().into() }
}
UrlCow::Sftp { loc, domain } => {
UrlCow::Sftp { loc: loc.into_owned().into(), domain: domain.into_owned().into() }
}

View file

@ -20,6 +20,9 @@ impl Display for Encode<'_> {
Url::Archive { domain, .. } => {
write!(f, "archive~://{}{}/{loc}", E::domain(domain), E::ports((*self).into()))
}
Url::S3 { domain, .. } => {
write!(f, "s3~://{}{}/{loc}", E::domain(domain), E::ports((*self).into()))
}
Url::Sftp { domain, .. } => {
write!(f, "sftp~://{}{}/{loc}", E::domain(domain), E::ports((*self).into()))
}

View file

@ -39,6 +39,7 @@ impl AsUrl for UrlBuf {
Self::Regular(loc) => Url::Regular(loc.as_loc()),
Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain },
Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain },
Self::S3 { loc, domain } => Url::S3 { loc: loc.as_loc(), domain },
Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain },
}
}
@ -60,6 +61,7 @@ impl AsUrl for UrlCow<'_> {
Self::Regular(loc) => Url::Regular(loc.as_loc()),
Self::Search { loc, domain } => Url::Search { loc: loc.as_loc(), domain },
Self::Archive { loc, domain } => Url::Archive { loc: loc.as_loc(), domain },
Self::S3 { loc, domain } => Url::S3 { loc: loc.as_loc(), domain },
Self::Sftp { loc, domain } => Url::Sftp { loc: loc.as_loc(), domain },
}
}

View file

@ -12,6 +12,7 @@ pub enum Url<'a> {
Regular(Loc<'a>),
Search { loc: Loc<'a>, domain: &'a str },
Archive { loc: Loc<'a>, domain: &'a str },
S3 { loc: Loc<'a, &'a typed_path::UnixPath>, domain: &'a str },
Sftp { loc: Loc<'a, &'a typed_path::UnixPath>, domain: &'a str },
}
@ -63,6 +64,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => Self::Regular(Loc::bare(loc.base())),
Self::Search { loc, domain } => Self::Search { loc: Loc::zeroed(loc.base()), domain },
Self::Archive { loc, domain } => Self::Archive { loc: Loc::zeroed(loc.base()), domain },
Self::S3 { loc, domain } => Self::S3 { loc: Loc::bare(loc.base()), domain },
Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.base()), domain },
}
}
@ -82,6 +84,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.extension()?.as_strand(),
Self::Search { loc, .. } => loc.extension()?.as_strand(),
Self::Archive { loc, .. } => loc.extension()?.as_strand(),
Self::S3 { loc, .. } => loc.extension()?.as_strand(),
Self::Sftp { loc, .. } => loc.extension()?.as_strand(),
})
}
@ -92,6 +95,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.has_base(),
Self::Search { loc, .. } => loc.has_base(),
Self::Archive { loc, .. } => loc.has_base(),
Self::S3 { loc, .. } => loc.has_base(),
Self::Sftp { loc, .. } => loc.has_base(),
}
}
@ -105,6 +109,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.has_trail(),
Self::Search { loc, .. } => loc.has_trail(),
Self::Archive { loc, .. } => loc.has_trail(),
Self::S3 { loc, .. } => loc.has_trail(),
Self::Sftp { loc, .. } => loc.has_trail(),
}
}
@ -118,7 +123,7 @@ impl<'a> Url<'a> {
#[inline]
pub fn is_internal(self) -> bool {
match self {
Self::Regular(_) | Self::Sftp { .. } => true,
Self::Regular(_) | Self::S3 { .. } | Self::Sftp { .. } => true,
Self::Search { .. } => !self.uri().is_empty(),
Self::Archive { .. } => false,
}
@ -136,6 +141,7 @@ impl<'a> Url<'a> {
Self::Regular(_) => SchemeKind::Regular,
Self::Search { .. } => SchemeKind::Search,
Self::Archive { .. } => SchemeKind::Archive,
Self::S3 { .. } => SchemeKind::S3,
Self::Sftp { .. } => SchemeKind::Sftp,
}
}
@ -146,6 +152,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.as_path(),
Self::Search { loc, .. } => loc.as_path(),
Self::Archive { loc, .. } => loc.as_path(),
Self::S3 { loc, .. } => loc.as_path(),
Self::Sftp { loc, .. } => loc.as_path(),
}
}
@ -156,6 +163,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.file_name()?.as_strand(),
Self::Search { loc, .. } => loc.file_name()?.as_strand(),
Self::Archive { loc, .. } => loc.file_name()?.as_strand(),
Self::S3 { loc, .. } => loc.file_name()?.as_strand(),
Self::Sftp { loc, .. } => loc.file_name()?.as_strand(),
})
}
@ -189,6 +197,7 @@ impl<'a> Url<'a> {
}
// SFTP
Self::S3 { loc, domain } => Self::S3 { loc: Loc::bare(loc.parent()?), domain },
Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.parent()?), domain },
})
}
@ -205,6 +214,7 @@ impl<'a> Url<'a> {
Self::Regular(_) => SchemeRef::Regular { uri, urn },
Self::Search { domain, .. } => SchemeRef::Search { domain, uri, urn },
Self::Archive { domain, .. } => SchemeRef::Archive { domain, uri, urn },
Self::S3 { domain, .. } => SchemeRef::S3 { domain, uri, urn },
Self::Sftp { domain, .. } => SchemeRef::Sftp { domain, uri, urn },
}
}
@ -215,6 +225,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.file_stem()?.as_strand(),
Self::Search { loc, .. } => loc.file_stem()?.as_strand(),
Self::Archive { loc, .. } => loc.file_stem()?.as_strand(),
Self::S3 { loc, .. } => loc.file_stem()?.as_strand(),
Self::Sftp { loc, .. } => loc.file_stem()?.as_strand(),
})
}
@ -248,6 +259,7 @@ impl<'a> Url<'a> {
Self::Archive { loc: Loc::new(loc.trail(), loc.base(), loc.base()), domain }
}
Self::S3 { loc, domain } => Self::S3 { loc: Loc::bare(loc.trail()), domain },
Self::Sftp { loc, domain } => Self::Sftp { loc: Loc::bare(loc.trail()), domain },
}
}
@ -258,7 +270,7 @@ impl<'a> Url<'a> {
let (base, rest, urn) = loc.triple();
(base.as_path(), rest.as_path(), urn.as_path())
}
Self::Sftp { loc, .. } => {
Self::S3 { loc, .. } | Self::Sftp { loc, .. } => {
let (base, rest, urn) = loc.triple();
(base.as_path(), rest.as_path(), urn.as_path())
}
@ -295,6 +307,9 @@ impl<'a> Url<'a> {
domain: domain.intern(),
},
Self::S3 { domain, .. } => {
UrlBuf::S3 { loc: joined.into_unix()?.into(), domain: domain.intern() }
}
Self::Sftp { domain, .. } => {
UrlBuf::Sftp { loc: joined.into_unix()?.into(), domain: domain.intern() }
}
@ -328,6 +343,10 @@ impl<'a> Url<'a> {
loc: LocBuf::<std::path::PathBuf>::new(path.into_os()?, loc.base(), loc.trail()),
domain: domain.intern(),
},
Self::S3 { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::S3 {
loc: LocBuf::<typed_path::UnixPathBuf>::new(path.into_unix()?, loc.base(), loc.trail()),
domain: domain.intern(),
},
Self::Sftp { loc, domain } if path.try_starts_with(loc.trail())? => UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::new(path.into_unix()?, loc.base(), loc.trail()),
domain: domain.intern(),
@ -341,6 +360,10 @@ impl<'a> Url<'a> {
loc: LocBuf::<std::path::PathBuf>::saturated(path.into_os()?, self.kind()),
domain: domain.intern(),
},
Self::S3 { domain, .. } => UrlBuf::S3 {
loc: LocBuf::<typed_path::UnixPathBuf>::saturated(path.into_unix()?, self.kind()),
domain: domain.intern(),
},
Self::Sftp { domain, .. } => UrlBuf::Sftp {
loc: LocBuf::<typed_path::UnixPathBuf>::saturated(path.into_unix()?, self.kind()),
domain: domain.intern(),
@ -370,6 +393,9 @@ impl<'a> Url<'a> {
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => {
Some(prefix).filter(|_| a == b).ok_or(Exotic)
}
(U::S3 { domain: a, .. }, U::S3 { domain: b, .. }) => {
Some(prefix).filter(|_| a == b).ok_or(Exotic)
}
(U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => {
Some(prefix).filter(|_| a == b).ok_or(Exotic)
}
@ -393,12 +419,20 @@ impl<'a> Url<'a> {
}
// Independent virtual file space
(U::Regular(_), U::S3 { .. }) => Err(Exotic),
(U::Search { .. }, U::S3 { .. }) => Err(Exotic),
(U::Archive { .. }, U::S3 { .. }) => Err(Exotic),
(U::S3 { .. }, U::Regular(_)) => Err(Exotic),
(U::S3 { .. }, U::Search { .. }) => Err(Exotic),
(U::S3 { .. }, U::Archive { .. }) => Err(Exotic),
(U::S3 { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Regular(_), U::Sftp { .. }) => Err(Exotic),
(U::Search { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Archive { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Sftp { .. }, U::Regular(_)) => Err(Exotic),
(U::Sftp { .. }, U::Search { .. }) => Err(Exotic),
(U::Sftp { .. }, U::Archive { .. }) => Err(Exotic),
(U::Sftp { .. }, U::S3 { .. }) => Err(Exotic),
}
}
@ -416,6 +450,9 @@ impl<'a> Url<'a> {
(U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
}
(U::S3 { domain: a, .. }, U::S3 { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
}
(U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => {
Some(suffix).filter(|_| a == b).ok_or(Exotic)
}
@ -439,12 +476,20 @@ impl<'a> Url<'a> {
}
// Independent virtual file space
(U::Regular(_), U::S3 { .. }) => Err(Exotic),
(U::Search { .. }, U::S3 { .. }) => Err(Exotic),
(U::Archive { .. }, U::S3 { .. }) => Err(Exotic),
(U::S3 { .. }, U::Regular(_)) => Err(Exotic),
(U::S3 { .. }, U::Search { .. }) => Err(Exotic),
(U::S3 { .. }, U::Archive { .. }) => Err(Exotic),
(U::S3 { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Regular(_), U::Sftp { .. }) => Err(Exotic),
(U::Search { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Archive { .. }, U::Sftp { .. }) => Err(Exotic),
(U::Sftp { .. }, U::Regular(_)) => Err(Exotic),
(U::Sftp { .. }, U::Search { .. }) => Err(Exotic),
(U::Sftp { .. }, U::Archive { .. }) => Err(Exotic),
(U::Sftp { .. }, U::S3 { .. }) => Err(Exotic),
}
}
@ -454,6 +499,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.uri().as_path(),
Self::Search { loc, .. } => loc.uri().as_path(),
Self::Archive { loc, .. } => loc.uri().as_path(),
Self::S3 { loc, .. } => loc.uri().as_path(),
Self::Sftp { loc, .. } => loc.uri().as_path(),
}
}
@ -464,6 +510,7 @@ impl<'a> Url<'a> {
Self::Regular(loc) => loc.urn().as_path(),
Self::Search { loc, .. } => loc.urn().as_path(),
Self::Archive { loc, .. } => loc.urn().as_path(),
Self::S3 { loc, .. } => loc.urn().as_path(),
Self::Sftp { loc, .. } => loc.urn().as_path(),
}
}

View file

@ -26,6 +26,7 @@ deadpool = { version = "0.13.0", default-features = false, features = [ "mana
either = { workspace = true }
futures = { workspace = true }
hashbrown = { workspace = true }
object_store = { version = "0.12.5", default-features = false, features = [ "aws" ] }
parking_lot = { workspace = true }
russh = { workspace = true }
tokio = { workspace = true }

View file

@ -5,6 +5,7 @@ use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::UrlBuf};
pub enum DirEntry {
Local(yazi_fs::provider::local::DirEntry),
S3(super::s3::DirEntry),
Sftp(super::sftp::DirEntry),
}
@ -12,6 +13,7 @@ impl FileHolder for DirEntry {
async fn file_type(&self) -> io::Result<ChaType> {
match self {
Self::Local(entry) => entry.file_type().await,
Self::S3(entry) => entry.file_type().await,
Self::Sftp(entry) => entry.file_type().await,
}
}
@ -19,6 +21,7 @@ impl FileHolder for DirEntry {
async fn metadata(&self) -> io::Result<Cha> {
match self {
Self::Local(entry) => entry.metadata().await,
Self::S3(entry) => entry.metadata().await,
Self::Sftp(entry) => entry.metadata().await,
}
}
@ -26,6 +29,7 @@ impl FileHolder for DirEntry {
fn name(&self) -> StrandCow<'_> {
match self {
Self::Local(entry) => entry.name(),
Self::S3(entry) => entry.name(),
Self::Sftp(entry) => entry.name(),
}
}
@ -33,6 +37,7 @@ impl FileHolder for DirEntry {
fn path(&self) -> PathBufDyn {
match self {
Self::Local(entry) => entry.path(),
Self::S3(entry) => entry.path(),
Self::Sftp(entry) => entry.path(),
}
}
@ -40,6 +45,7 @@ impl FileHolder for DirEntry {
fn url(&self) -> UrlBuf {
match self {
Self::Local(entry) => entry.url(),
Self::S3(entry) => entry.url(),
Self::Sftp(entry) => entry.url(),
}
}

View file

@ -49,6 +49,9 @@ impl FileBuilder for Gate {
SchemeKind::Archive => {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))?
}
SchemeKind::S3 => {
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider does not expose file handles"))?
}
SchemeKind::Sftp => self.build::<super::sftp::Gate>().open(url).await?.into(),
})
}

View file

@ -1,5 +1,8 @@
yazi_macro::mod_pub!(sftp);
yazi_macro::mod_pub!(s3 sftp);
yazi_macro::mod_flat!(calculator copier dir_entry gate provider providers read_dir rw_file);
pub(super) fn init() { sftp::init(); }
pub(super) fn init() {
s3::init();
sftp::init();
}

View file

@ -52,6 +52,9 @@ where
V: AsUrl,
{
let (from, to) = (from.as_url(), to.as_url());
if matches!(from, Url::S3 { .. }) {
return super::s3::copy_impl(from, to, attrs).await;
}
match (from.kind().is_local(), to.kind().is_local()) {
(true, true) => Local::new(from).await?.copy(to.loc(), attrs).await,
@ -73,6 +76,9 @@ where
A: Into<Attrs>,
{
let (from, to) = (from.as_url(), to.as_url());
if matches!(from, Url::S3 { .. }) {
return Ok(super::s3::copy_with_progress_impl(from.to_owned(), to.to_owned(), attrs.into()));
}
match (from.kind().is_local(), to.kind().is_local()) {
(true, true) => Local::new(from).await?.copy_with_progress(to.loc(), attrs),
@ -262,6 +268,7 @@ where
match url.as_url() {
Url::Regular(_) | Url::Search { .. } => yazi_fs::provider::local::try_absolute(url),
Url::Archive { .. } => None, // TODO
Url::S3 { .. } => crate::provider::s3::try_absolute(url),
Url::Sftp { .. } => crate::provider::sftp::try_absolute(url),
}
}

View file

@ -7,6 +7,7 @@ use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBu
#[derive(Clone)]
pub(super) enum Providers<'a> {
Local(yazi_fs::provider::local::Local<'a>),
S3(super::s3::S3<'a>),
Sftp(super::sftp::Sftp<'a>),
}
@ -20,6 +21,7 @@ impl<'a> Provider for Providers<'a> {
async fn absolute(&self) -> io::Result<Self::UrlCow> {
match self {
Self::Local(p) => p.absolute().await,
Self::S3(p) => p.absolute().await,
Self::Sftp(p) => p.absolute().await,
}
}
@ -27,6 +29,7 @@ impl<'a> Provider for Providers<'a> {
async fn canonicalize(&self) -> io::Result<UrlBuf> {
match self {
Self::Local(p) => p.canonicalize().await,
Self::S3(p) => p.canonicalize().await,
Self::Sftp(p) => p.canonicalize().await,
}
}
@ -34,6 +37,7 @@ impl<'a> Provider for Providers<'a> {
fn capabilities(&self) -> Capabilities {
match self {
Self::Local(p) => p.capabilities(),
Self::S3(p) => p.capabilities(),
Self::Sftp(p) => p.capabilities(),
}
}
@ -41,6 +45,7 @@ impl<'a> Provider for Providers<'a> {
async fn casefold(&self) -> io::Result<UrlBuf> {
match self {
Self::Local(p) => p.casefold().await,
Self::S3(p) => p.casefold().await,
Self::Sftp(p) => p.casefold().await,
}
}
@ -51,6 +56,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.copy(to, attrs).await,
Self::S3(p) => p.copy(to, attrs).await,
Self::Sftp(p) => p.copy(to, attrs).await,
}
}
@ -62,6 +68,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.copy_with_progress(to, attrs),
Self::S3(p) => p.copy_with_progress(to, attrs),
Self::Sftp(p) => p.copy_with_progress(to, attrs),
}
}
@ -69,6 +76,9 @@ impl<'a> Provider for Providers<'a> {
async fn create(&self) -> io::Result<Self::File> {
Ok(match self {
Self::Local(p) => p.create().await?.into(),
Self::S3(_) => {
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only"))?
}
Self::Sftp(p) => p.create().await?.into(),
})
}
@ -76,6 +86,7 @@ impl<'a> Provider for Providers<'a> {
async fn create_dir(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.create_dir().await,
Self::S3(p) => p.create_dir().await,
Self::Sftp(p) => p.create_dir().await,
}
}
@ -83,6 +94,7 @@ impl<'a> Provider for Providers<'a> {
async fn create_dir_all(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.create_dir_all().await,
Self::S3(p) => p.create_dir_all().await,
Self::Sftp(p) => p.create_dir_all().await,
}
}
@ -90,6 +102,9 @@ impl<'a> Provider for Providers<'a> {
async fn create_new(&self) -> io::Result<Self::File> {
Ok(match self {
Self::Local(p) => p.create_new().await?.into(),
Self::S3(_) => {
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only"))?
}
Self::Sftp(p) => p.create_new().await?.into(),
})
}
@ -100,6 +115,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.hard_link(to).await,
Self::S3(p) => p.hard_link(to).await,
Self::Sftp(p) => p.hard_link(to).await,
}
}
@ -107,6 +123,7 @@ impl<'a> Provider for Providers<'a> {
async fn metadata(&self) -> io::Result<Cha> {
match self {
Self::Local(p) => p.metadata().await,
Self::S3(p) => p.metadata().await,
Self::Sftp(p) => p.metadata().await,
}
}
@ -119,6 +136,7 @@ impl<'a> Provider for Providers<'a> {
K::Archive => {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))?
}
K::S3 => Self::Me::S3(super::s3::S3::new(url).await?),
K::Sftp => Self::Me::Sftp(super::sftp::Sftp::new(url).await?),
})
}
@ -126,6 +144,10 @@ impl<'a> Provider for Providers<'a> {
async fn open(&self) -> io::Result<Self::File> {
Ok(match self {
Self::Local(p) => p.open().await?.into(),
Self::S3(_) => Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
))?,
Self::Sftp(p) => p.open().await?.into(),
})
}
@ -133,6 +155,7 @@ impl<'a> Provider for Providers<'a> {
async fn read_dir(self) -> io::Result<Self::ReadDir> {
Ok(match self {
Self::Local(p) => Self::ReadDir::Local(p.read_dir().await?),
Self::S3(p) => Self::ReadDir::S3(p.read_dir().await?),
Self::Sftp(p) => Self::ReadDir::Sftp(p.read_dir().await?),
})
}
@ -140,6 +163,7 @@ impl<'a> Provider for Providers<'a> {
async fn read_link(&self) -> io::Result<PathBufDyn> {
match self {
Self::Local(p) => p.read_link().await,
Self::S3(p) => p.read_link().await,
Self::Sftp(p) => p.read_link().await,
}
}
@ -147,6 +171,7 @@ impl<'a> Provider for Providers<'a> {
async fn remove_dir(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.remove_dir().await,
Self::S3(p) => p.remove_dir().await,
Self::Sftp(p) => p.remove_dir().await,
}
}
@ -154,6 +179,7 @@ impl<'a> Provider for Providers<'a> {
async fn remove_dir_all(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.remove_dir_all().await,
Self::S3(p) => p.remove_dir_all().await,
Self::Sftp(p) => p.remove_dir_all().await,
}
}
@ -161,6 +187,7 @@ impl<'a> Provider for Providers<'a> {
async fn remove_file(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.remove_file().await,
Self::S3(p) => p.remove_file().await,
Self::Sftp(p) => p.remove_file().await,
}
}
@ -171,6 +198,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.rename(to).await,
Self::S3(p) => p.rename(to).await,
Self::Sftp(p) => p.rename(to).await,
}
}
@ -182,6 +210,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.symlink(original, is_dir).await,
Self::S3(p) => p.symlink(original, is_dir).await,
Self::Sftp(p) => p.symlink(original, is_dir).await,
}
}
@ -192,6 +221,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.symlink_dir(original).await,
Self::S3(p) => p.symlink_dir(original).await,
Self::Sftp(p) => p.symlink_dir(original).await,
}
}
@ -202,6 +232,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.symlink_file(original).await,
Self::S3(p) => p.symlink_file(original).await,
Self::Sftp(p) => p.symlink_file(original).await,
}
}
@ -209,6 +240,7 @@ impl<'a> Provider for Providers<'a> {
async fn symlink_metadata(&self) -> io::Result<Cha> {
match self {
Self::Local(p) => p.symlink_metadata().await,
Self::S3(p) => p.symlink_metadata().await,
Self::Sftp(p) => p.symlink_metadata().await,
}
}
@ -216,6 +248,7 @@ impl<'a> Provider for Providers<'a> {
async fn trash(&self) -> io::Result<()> {
match self {
Self::Local(p) => p.trash().await,
Self::S3(p) => p.trash().await,
Self::Sftp(p) => p.trash().await,
}
}
@ -223,6 +256,7 @@ impl<'a> Provider for Providers<'a> {
fn url(&self) -> Url<'_> {
match self {
Self::Local(p) => p.url(),
Self::S3(p) => p.url(),
Self::Sftp(p) => p.url(),
}
}
@ -233,6 +267,7 @@ impl<'a> Provider for Providers<'a> {
{
match self {
Self::Local(p) => p.write(contents).await,
Self::S3(p) => p.write(contents).await,
Self::Sftp(p) => p.write(contents).await,
}
}

View file

@ -4,6 +4,7 @@ use yazi_fs::provider::DirReader;
pub enum ReadDir {
Local(yazi_fs::provider::local::ReadDir),
S3(super::s3::ReadDir),
Sftp(super::sftp::ReadDir),
}
@ -13,6 +14,7 @@ impl DirReader for ReadDir {
async fn next(&mut self) -> io::Result<Option<Self::Entry>> {
Ok(match self {
Self::Local(reader) => reader.next().await?.map(Self::Entry::Local),
Self::S3(reader) => reader.next().await?.map(Self::Entry::S3),
Self::Sftp(reader) => reader.next().await?.map(Self::Entry::Sftp),
})
}

View file

@ -0,0 +1,29 @@
use yazi_fs::CWD;
use yazi_shared::{loc::LocBuf, pool::InternStr, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
pub fn try_absolute<'a, U>(url: U) -> Option<UrlCow<'a>>
where
U: Into<UrlCow<'a>>,
{
let url = url.into();
if url.is_absolute() {
Some(url)
} else if let Url::S3 { domain, .. } = url.as_url() {
let raw = url.loc().to_string_lossy();
let raw = raw.trim_start_matches('/');
let absolute = if raw.is_empty() { "/".to_owned() } else { format!("/{raw}") };
Some(
UrlBuf::S3 {
loc: LocBuf::<typed_path::UnixPathBuf>::zeroed(typed_path::UnixPathBuf::from(absolute)),
domain: domain.intern(),
}
.into(),
)
} else if let cwd = CWD.load()
&& cwd.scheme().covariant(url.scheme())
{
Some(cwd.try_join(url.loc()).ok()?.into())
} else {
None
}
}

View file

@ -0,0 +1,97 @@
use std::time::SystemTime;
use object_store::ObjectMeta;
use yazi_fs::cha::{Cha, ChaKind, ChaMode};
use yazi_shared::strand::AsStrand;
pub(super) fn dir(_name: impl AsStrand) -> Cha {
Cha {
kind: ChaKind::empty(),
mode: default_dir_mode(),
len: 0,
atime: None,
btime: None,
ctime: None,
mtime: None,
dev: 0,
uid: 0,
gid: 0,
nlink: 0,
}
}
pub(super) fn object(_name: impl AsStrand, meta: &ObjectMeta) -> Cha {
Cha {
kind: ChaKind::empty(),
mode: default_file_mode(),
len: meta.size,
atime: None,
btime: None,
ctime: None,
mtime: Some(SystemTime::from(meta.last_modified)),
dev: 0,
uid: 0,
gid: 0,
nlink: 0,
}
}
#[inline]
const fn default_dir_mode() -> ChaMode {
ChaMode::T_DIR
.union(ChaMode::U_READ)
.union(ChaMode::U_WRITE)
.union(ChaMode::U_EXEC)
.union(ChaMode::G_READ)
.union(ChaMode::G_EXEC)
.union(ChaMode::O_READ)
.union(ChaMode::O_EXEC)
}
#[inline]
const fn default_file_mode() -> ChaMode {
ChaMode::T_FILE
.union(ChaMode::U_READ)
.union(ChaMode::U_WRITE)
.union(ChaMode::G_READ)
.union(ChaMode::O_READ)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn object_uses_readable_file_permissions() {
let cha = object(
"producer.8-2.json",
&ObjectMeta {
location: "producer.8-2.json".into(),
last_modified: chrono::Utc::now().into(),
size: 61,
e_tag: None,
version: None,
},
);
assert!(cha.mode.contains(ChaMode::T_FILE));
assert!(cha.mode.contains(ChaMode::U_READ));
assert!(cha.mode.contains(ChaMode::U_WRITE));
assert!(cha.mode.contains(ChaMode::G_READ));
assert!(cha.mode.contains(ChaMode::O_READ));
}
#[test]
fn dir_uses_traversable_directory_permissions() {
let cha = dir("e2e_test");
assert!(cha.mode.contains(ChaMode::T_DIR));
assert!(cha.mode.contains(ChaMode::U_READ));
assert!(cha.mode.contains(ChaMode::U_WRITE));
assert!(cha.mode.contains(ChaMode::U_EXEC));
assert!(cha.mode.contains(ChaMode::G_READ));
assert!(cha.mode.contains(ChaMode::G_EXEC));
assert!(cha.mode.contains(ChaMode::O_READ));
assert!(cha.mode.contains(ChaMode::O_EXEC));
}
}

View file

@ -0,0 +1,10 @@
yazi_macro::mod_flat!(absolute metadata read_dir s3);
pub use absolute::try_absolute;
pub use read_dir::{DirEntry, ReadDir};
pub use s3::S3;
pub(crate) use s3::{copy_impl, copy_with_progress_impl};
type DynStore = std::sync::Arc<object_store::aws::AmazonS3>;
pub(super) fn init() {}

View file

@ -0,0 +1,125 @@
use std::{collections::VecDeque, io, sync::Arc};
use object_store::{ListResult, ObjectMeta, path::Path};
use yazi_fs::provider::{DirReader, FileHolder};
use yazi_shared::{path::PathBufDyn, strand::StrandCow, url::{UrlBuf, UrlLike}};
use super::DynStore;
pub struct ReadDir {
pub(super) dir: Arc<UrlBuf>,
pub(super) store: DynStore,
pub(super) prefix: String,
pub(super) token: Option<String>,
pub(super) finished: bool,
pub(super) page_size: usize,
pub(super) buffer: VecDeque<DirEntry>,
}
impl DirReader for ReadDir {
type Entry = DirEntry;
async fn next(&mut self) -> io::Result<Option<Self::Entry>> {
loop {
if let Some(entry) = self.buffer.pop_front() {
return Ok(Some(entry));
}
if self.finished {
return Ok(None);
}
self.fetch_next_page().await?;
}
}
}
impl ReadDir {
async fn fetch_next_page(&mut self) -> io::Result<()> {
use object_store::list::{PaginatedListOptions, PaginatedListStore};
let result = self
.store
.list_paginated(
Some(&self.prefix),
PaginatedListOptions {
delimiter: Some("/".into()),
max_keys: Some(self.page_size),
page_token: self.token.take(),
..Default::default()
},
)
.await
.map_err(super::s3::to_io)?;
self.extend(result.result);
self.token = result.page_token;
self.finished = self.token.is_none();
Ok(())
}
fn extend(&mut self, result: ListResult) {
for prefix in result.common_prefixes {
self.buffer.push_back(DirEntry {
dir: self.dir.clone(),
name: basename(&prefix),
kind: Kind::Dir,
});
}
for object in result.objects {
self.buffer.push_back(DirEntry {
dir: self.dir.clone(),
name: basename(&object.location),
kind: Kind::File(object),
});
}
}
}
pub enum Kind {
Dir,
File(ObjectMeta),
}
pub struct DirEntry {
pub(super) dir: Arc<UrlBuf>,
pub(super) name: String,
pub(super) kind: Kind,
}
impl FileHolder for DirEntry {
async fn file_type(&self) -> io::Result<yazi_fs::cha::ChaType> {
Ok(match self.kind {
Kind::Dir => yazi_fs::cha::ChaType::Dir,
Kind::File(_) => yazi_fs::cha::ChaType::File,
})
}
async fn metadata(&self) -> io::Result<yazi_fs::cha::Cha> {
match &self.kind {
Kind::Dir => Ok(super::metadata::dir(self.name.as_str())),
Kind::File(meta) => Ok(super::metadata::object(self.name.as_str(), meta)),
}
}
fn name(&self) -> StrandCow<'_> { self.name.as_str().into() }
fn path(&self) -> PathBufDyn { self.url().into_loc() }
fn url(&self) -> UrlBuf {
self.dir.try_join(self.name.as_str()).expect("entry name is valid S3 path component")
}
}
pub(super) fn basename(path: &Path) -> String {
path.to_string().rsplit('/').next().unwrap_or_default().to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basename_returns_last_segment() {
assert_eq!(basename(&Path::from("foo/bar/baz.txt")), "baz.txt");
assert_eq!(basename(&Path::from("prefix/dir")), "dir");
}
}

View file

@ -0,0 +1,407 @@
use std::{collections::VecDeque, io, pin::Pin, sync::Arc, task::{Context, Poll}};
use object_store::{ObjectStore, path::Path};
use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt, BufWriter, ReadBuf}, sync::mpsc};
use typed_path::Component;
use yazi_config::vfs::{ServiceS3, Vfs};
use yazi_fs::provider::{Capabilities, FileBuilder, Provider};
use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}};
use super::{DynStore, read_dir::ReadDir};
const PAGE_SIZE: usize = 500;
const COPY_BUF_SIZE: usize = 512 * 1024;
const COPY_CHUNK: usize = 64 * 1024;
#[derive(Clone)]
pub struct S3<'a> {
url: Url<'a>,
key: Path,
store: DynStore,
}
pub struct File;
#[derive(Clone, Copy, Default)]
pub struct Gate;
impl<'a> Provider for S3<'a> {
type File = File;
type Gate = Gate;
type Me<'b> = S3<'b>;
type ReadDir = ReadDir;
type UrlCow = UrlCow<'a>;
async fn absolute(&self) -> io::Result<Self::UrlCow> {
Ok(if let Some(u) = super::absolute::try_absolute(self.url) { u } else { self.url.to_owned().into() })
}
async fn canonicalize(&self) -> io::Result<UrlBuf> { Ok(self.url.to_owned()) }
fn capabilities(&self) -> Capabilities { Capabilities { symlink: false } }
async fn casefold(&self) -> io::Result<UrlBuf> { Ok(self.url.to_owned()) }
async fn copy<P>(&self, _to: P, _attrs: yazi_fs::provider::Attrs) -> io::Result<u64>
where
P: AsPath,
{
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only"))
}
fn copy_with_progress<P, A>(&self, _to: P, _attrs: A) -> io::Result<tokio::sync::mpsc::Receiver<io::Result<u64>>>
where
P: AsPath,
A: Into<yazi_fs::provider::Attrs>,
{
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only"))
}
async fn create(&self) -> io::Result<Self::File> {
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only"))
}
async fn create_dir(&self) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only")) }
async fn create_new(&self) -> io::Result<Self::File> {
Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only"))
}
async fn hard_link<P>(&self, _to: P) -> io::Result<()>
where
P: AsPath,
{ Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only")) }
async fn metadata(&self) -> io::Result<yazi_fs::cha::Cha> {
if self.key.to_string().is_empty() {
return Ok(super::metadata::dir(self.url.name().unwrap_or_default()));
}
match self.store.head(&self.key).await {
Ok(meta) => Ok(super::metadata::object(self.url.name().unwrap_or_default(), &meta)),
Err(object_store::Error::NotFound { .. }) if self.dir_exists().await? => {
Ok(super::metadata::dir(self.url.name().unwrap_or_default()))
}
Err(error) => Err(to_io(error)),
}
}
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
match url {
Url::S3 { loc, domain } => {
let (_name, config) = Vfs::service::<&ServiceS3>(domain).await?;
let (bucket, key) = split_bucket_and_key(loc.as_inner())?;
let store = build_store(config, &bucket)?;
Ok(Self::Me { url, key, store })
}
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not an S3 URL: {url:?}"))),
}
}
async fn open(&self) -> io::Result<Self::File> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
))
}
async fn read_dir(self) -> io::Result<Self::ReadDir> {
let prefix = self.list_prefix();
let dir = Arc::new(self.url.to_owned());
Ok(ReadDir {
dir,
store: self.store,
prefix,
token: None,
finished: false,
page_size: PAGE_SIZE,
buffer: VecDeque::new(),
})
}
async fn read_link(&self) -> io::Result<PathBufDyn> { Err(io::Error::new(io::ErrorKind::Unsupported, "S3 has no symlinks")) }
async fn remove_dir(&self) -> io::Result<()> { Ok(()) }
async fn remove_file(&self) -> io::Result<()> { self.store.delete(&self.key).await.map_err(to_io) }
async fn rename<P>(&self, _to: P) -> io::Result<()>
where
P: AsPath,
{ Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only")) }
async fn symlink<S, F>(&self, _original: S, _is_dir: F) -> io::Result<()>
where
S: AsStrand,
F: AsyncFnOnce() -> io::Result<bool>,
{ Err(io::Error::new(io::ErrorKind::Unsupported, "S3 has no symlinks")) }
async fn symlink_metadata(&self) -> io::Result<yazi_fs::cha::Cha> { self.metadata().await }
async fn trash(&self) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only")) }
fn url(&self) -> Url<'_> { self.url }
async fn write<C>(&self, _contents: C) -> io::Result<()>
where
C: AsRef<[u8]>,
{ Err(io::Error::new(io::ErrorKind::Unsupported, "S3 provider is read-only")) }
}
impl<'a> S3<'a> {
pub(super) async fn read_bytes(&self) -> io::Result<Vec<u8>> {
let bytes = self.store.get(&self.key).await.map_err(to_io)?.bytes().await.map_err(to_io)?;
Ok(bytes.to_vec())
}
async fn dir_exists(&self) -> io::Result<bool> {
use object_store::list::{PaginatedListOptions, PaginatedListStore};
let prefix = self.list_prefix();
let result = self
.store
.list_paginated(
Some(&prefix),
PaginatedListOptions {
delimiter: Some("/".into()),
max_keys: Some(1),
..Default::default()
},
)
.await
.map_err(to_io)?;
Ok(!result.result.common_prefixes.is_empty() || !result.result.objects.is_empty())
}
fn list_prefix(&self) -> String {
let key = self.key.to_string();
if key.is_empty() || key.ends_with('/') {
key
} else {
format!("{key}/")
}
}
}
impl FileBuilder for Gate {
type File = File;
fn append(&mut self, _append: bool) -> &mut Self { self }
fn attrs(&mut self, _attrs: yazi_fs::provider::Attrs) -> &mut Self { self }
fn create(&mut self, _create: bool) -> &mut Self { self }
fn create_new(&mut self, _create_new: bool) -> &mut Self { self }
async fn open<U>(&self, _url: U) -> io::Result<Self::File>
where
U: yazi_shared::url::AsUrl,
{
Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
))
}
fn read(&mut self, _read: bool) -> &mut Self { self }
fn truncate(&mut self, _truncate: bool) -> &mut Self { self }
fn write(&mut self, _write: bool) -> &mut Self { self }
}
impl AsyncRead for File {
fn poll_read(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
)))
}
}
impl AsyncSeek for File {
fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
))
}
fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
)))
}
}
impl AsyncWrite for File {
fn poll_write(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &[u8]) -> Poll<io::Result<usize>> {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
)))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
)))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 provider does not expose file handles",
)))
}
}
pub(crate) async fn copy_impl(from: Url<'_>, to: Url<'_>, attrs: yazi_fs::provider::Attrs) -> io::Result<u64> {
let provider = S3::new(from).await?;
let bytes = provider.read_bytes().await?;
let dist = crate::provider::create(to).await?;
let mut writer = BufWriter::with_capacity(COPY_BUF_SIZE, dist);
writer.write_all(&bytes).await?;
writer.flush().await?;
writer.get_ref().set_attrs(attrs).await.ok();
writer.shutdown().await.ok();
Ok(bytes.len() as u64)
}
pub(crate) fn copy_with_progress_impl(
from: UrlBuf,
to: UrlBuf,
attrs: yazi_fs::provider::Attrs,
) -> mpsc::Receiver<io::Result<u64>> {
let (tx, rx) = mpsc::channel(10);
tokio::spawn(async move {
let result = async {
let provider = S3::new(from.as_url()).await?;
let bytes = provider.read_bytes().await?;
let dist = crate::provider::create(&to).await?;
let mut writer = BufWriter::with_capacity(COPY_BUF_SIZE, dist);
for chunk in bytes.chunks(COPY_CHUNK) {
writer.write_all(chunk).await?;
tx.send(Ok(chunk.len() as u64)).await.ok();
}
writer.flush().await?;
let mut file = writer.into_inner();
file.set_attrs(attrs).await.ok();
file.shutdown().await.ok();
Ok(())
}
.await;
match result {
Ok(()) => {
tx.send(Ok(0)).await.ok();
}
Err(error) => {
tx.send(Err(error)).await.ok();
}
}
});
rx
}
fn split_bucket_and_key(path: &typed_path::UnixPath) -> io::Result<(String, Path)> {
let mut components = path.components().filter(|component| !component.is_root());
let Some(bucket) = components.next() else {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"S3 service root is not supported; use s3://<service>/<bucket>/",
));
};
let bucket = String::from_utf8_lossy(bucket.as_bytes()).into_owned();
if bucket.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"S3 bucket name must not be empty",
));
}
let key = components
.map(|component| String::from_utf8_lossy(component.as_bytes()).into_owned())
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join("/");
Ok((bucket, Path::from(key)))
}
fn build_store(config: &ServiceS3, bucket: &str) -> io::Result<DynStore> {
let mut builder = object_store::aws::AmazonS3Builder::new().with_bucket_name(bucket);
if let Some(region) = &config.region {
builder = builder.with_region(region);
}
if let Some(endpoint) = &config.endpoint {
builder = builder.with_endpoint(endpoint);
}
if let Some(key) = &config.access_key_id {
builder = builder.with_access_key_id(key);
}
if let Some(secret) = &config.secret_access_key {
builder = builder.with_secret_access_key(secret);
}
if let Some(token) = &config.session_token {
builder = builder.with_token(token);
}
builder = builder.with_allow_http(config.allow_http);
builder = builder.with_virtual_hosted_style_request(!config.force_path_style);
Ok(Arc::new(builder.build().map_err(io::Error::other)?))
}
pub(super) fn to_io(error: object_store::Error) -> io::Error {
match error {
object_store::Error::NotFound { .. } => io::Error::from(io::ErrorKind::NotFound),
object_store::Error::PermissionDenied { .. }
| object_store::Error::Unauthenticated { .. } => io::Error::from(io::ErrorKind::PermissionDenied),
other => io::Error::other(other),
}
}
#[cfg(test)]
mod tests {
use super::*;
use object_store::aws::AmazonS3Builder;
use yazi_shared::loc::Loc;
#[test]
fn split_bucket_and_key_uses_first_segment_as_bucket() {
let (bucket, key) = split_bucket_and_key(typed_path::UnixPath::new("/srgdata/foo/bar")).unwrap();
assert_eq!(bucket, "srgdata");
assert_eq!(key.to_string(), "foo/bar");
}
#[test]
fn split_bucket_and_key_returns_empty_key_for_bucket_root() {
let (bucket, key) = split_bucket_and_key(typed_path::UnixPath::new("/srgdata")).unwrap();
assert_eq!(bucket, "srgdata");
assert_eq!(key.to_string(), "");
}
#[test]
fn provider_uses_bucket_root_as_empty_key() {
let url = Url::S3 { loc: Loc::zeroed(typed_path::UnixPath::new("/srgdata")), domain: "yabos" };
let s3 = S3 {
url,
key: Path::from(""),
store: Arc::new(AmazonS3Builder::new().with_bucket_name("srgdata").with_region("us-east-1").build().unwrap()),
};
assert_eq!(s3.key.to_string(), "");
}
#[test]
fn list_prefix_adds_trailing_slash_for_directories() {
let url = Url::S3 { loc: Loc::zeroed(typed_path::UnixPath::new("/srgdata/data")), domain: "yabos" };
let s3 = S3 {
url,
key: Path::from("data"),
store: Arc::new(AmazonS3Builder::new().with_bucket_name("srgdata").with_region("us-east-1").build().unwrap()),
};
assert_eq!(s3.list_prefix(), "data/");
}
}

View file

@ -146,7 +146,7 @@ impl<'a> Provider for Sftp<'a> {
async fn new<'b>(url: Url<'b>) -> io::Result<Self::Me<'b>> {
match url {
Url::Regular(_) | Url::Search { .. } | Url::Archive { .. } => {
Url::Regular(_) | Url::Search { .. } | Url::Archive { .. } | Url::S3 { .. } => {
Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}")))
}
Url::Sftp { loc, domain } => {

View file

@ -22,6 +22,7 @@ impl Reporter {
match url.as_url().kind() {
SchemeKind::Regular | SchemeKind::Search => self.report_local(url),
SchemeKind::Archive => {}
SchemeKind::S3 => self.report_remote(url),
SchemeKind::Sftp => self.report_remote(url),
}
}