use std::fmt;
use std::path::{Path, PathBuf};
use crate::cloud_store::CloudObject;
use crate::cloud_store::CloudProvider;
pub(crate) const URI_SCHEME_SEPARATOR: &str = "://";
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum UriError {
#[error("invalid URI: {0}")]
InvalidUri(Uri),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("cloud error: {0}")]
Cloud(#[from] crate::CloudError),
#[error("not a directory: {0}")]
NotADirectory(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Uri {
raw: String,
}
impl Uri {
pub fn scheme(&self) -> &str {
self.raw
.find(URI_SCHEME_SEPARATOR)
.map_or("", |pos| &self.raw[..pos])
}
pub fn cloud_scheme(&self) -> Option<&str> {
let scheme = self.scheme();
if CloudProvider::parse(&self.raw).is_some() {
Some(scheme)
} else {
None
}
}
pub fn bucket(&self) -> Option<&str> {
let scheme = self.cloud_scheme()?;
let start = scheme.len() + URI_SCHEME_SEPARATOR.len();
if let Some(end) = self.raw[start..].find('/') {
self.raw.get(start..start + end)
} else {
self.raw.get(start..)
}
}
pub fn key(&self) -> Option<&str> {
let bucket = self.bucket()?;
let scheme = self.scheme();
let start = scheme.len()
+ URI_SCHEME_SEPARATOR.len()
+ bucket.len()
+ "/".len();
self.raw.get(start..)
}
pub fn is_local(&self) -> bool {
matches!(self.scheme(), "file" | "")
}
pub fn is_cloud(&self) -> bool {
self.cloud_scheme().is_some()
}
pub fn is_valid(&self) -> bool {
self.is_local() || self.is_cloud()
}
pub fn parent(&self) -> Option<Uri> {
if self.is_local() {
self.as_path()?.parent().map(Uri::from)
} else {
let raw = self.raw.trim_end_matches('/');
let pos = raw.rfind('/')?;
if raw[..pos].ends_with(":/") {
return None;
}
Some(Uri {
raw: raw[..pos].to_string(),
})
}
}
pub fn list_children(&self) -> Result<Vec<Uri>, UriError> {
if self.is_local() {
let path = self
.as_path()
.ok_or_else(|| UriError::InvalidUri(self.clone()))?;
let mut children = Vec::new();
for entry in std::fs::read_dir(&path)? {
let entry = entry?;
children.push(Uri::from(entry.path()));
}
Ok(children)
} else if self.is_cloud() {
let cloud_object = CloudObject::new(self.as_ref())?;
let children = cloud_object.children()?;
Ok(children.into_iter().map(Uri::from).collect())
} else {
Err(UriError::NotADirectory(self.raw.clone()))
}
}
pub fn exists(&self) -> Result<bool, UriError> {
match self.as_path() {
Some(path) => Ok(path.exists()),
None => {
if self.is_cloud() {
let _cloud_object = CloudObject::new(self.as_ref())?;
Ok(true)
} else {
Err(UriError::InvalidUri(self.clone()))
}
},
}
}
pub fn is_file(&self) -> Result<bool, UriError> {
match self.as_path() {
Some(path) => {
if let Ok(path) = std::fs::metadata(&path) {
Ok(path.is_file())
} else {
Ok(false)
}
},
None => {
if self.is_cloud() {
let cloud_object = CloudObject::new(self.as_ref())?;
let has_len = cloud_object.len().is_ok();
Ok(has_len)
} else {
Err(UriError::InvalidUri(self.clone()))
}
},
}
}
pub fn is_folder(&self) -> Result<bool, UriError> {
if self.is_local() {
let path = self
.as_path()
.ok_or_else(|| UriError::InvalidUri(self.clone()))?;
match std::fs::metadata(&path) {
Ok(m) => Ok(m.is_dir()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(UriError::Io(e)),
}
} else if self.is_cloud() {
use crate::cloud_store::CloudObject;
if self.raw.ends_with('/') {
return Ok(true);
}
let cloud_object =
CloudObject::new(self.as_ref()).map_err(UriError::Cloud)?;
if cloud_object.len().is_ok() {
return Ok(false);
}
let children = cloud_object.children().map_err(UriError::Cloud)?;
Ok(!children.is_empty())
} else {
Err(UriError::InvalidUri(self.clone()))
}
}
pub fn join(&self, segment: &str) -> Uri {
if let Some(base) = self.as_path() {
Uri::from(base.join(segment))
} else {
let raw = self.raw.trim_end_matches('/');
let segment = segment.trim_start_matches('/');
Uri {
raw: format!("{}/{}", raw, segment),
}
}
}
pub fn as_path(&self) -> Option<PathBuf> {
let local_uri = self;
match local_uri.scheme() {
"" => Some(PathBuf::from(&self.raw)),
"file" => {
let path_str =
&self.raw[("file".len() + URI_SCHEME_SEPARATOR.len())..];
Some(PathBuf::from(path_str))
},
_ => None,
}
}
}
impl From<&str> for Uri {
fn from(s: &str) -> Self {
Uri { raw: s.to_string() }
}
}
impl From<String> for Uri {
fn from(s: String) -> Self {
Uri { raw: s }
}
}
impl From<&String> for Uri {
fn from(s: &String) -> Self {
Uri { raw: s.clone() }
}
}
impl From<&Path> for Uri {
fn from(p: &Path) -> Self {
Uri {
raw: p.to_string_lossy().into_owned(),
}
}
}
impl From<&PathBuf> for Uri {
fn from(p: &PathBuf) -> Self {
Uri {
raw: p.to_string_lossy().into_owned(),
}
}
}
impl From<PathBuf> for Uri {
fn from(p: PathBuf) -> Self {
Uri {
raw: p.to_string_lossy().into_owned(),
}
}
}
impl AsRef<str> for Uri {
fn as_ref(&self) -> &str {
&self.raw
}
}
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}