mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
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:
parent
4857d46918
commit
558622e512
36 changed files with 1731 additions and 22 deletions
804
Cargo.lock
generated
804
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
))?,
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
29
yazi-vfs/src/provider/s3/absolute.rs
Normal file
29
yazi-vfs/src/provider/s3/absolute.rs
Normal 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
|
||||
}
|
||||
}
|
||||
97
yazi-vfs/src/provider/s3/metadata.rs
Normal file
97
yazi-vfs/src/provider/s3/metadata.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
10
yazi-vfs/src/provider/s3/mod.rs
Normal file
10
yazi-vfs/src/provider/s3/mod.rs
Normal 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() {}
|
||||
125
yazi-vfs/src/provider/s3/read_dir.rs
Normal file
125
yazi-vfs/src/provider/s3/read_dir.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
407
yazi-vfs/src/provider/s3/s3.rs
Normal file
407
yazi-vfs/src/provider/s3/s3.rs
Normal 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/");
|
||||
}
|
||||
}
|
||||
|
|
@ -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 } => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue