use std::{
error::Error,
fmt::{Display, Formatter},
io::{self},
string::FromUtf8Error,
sync::Arc,
};
use bytes::Bytes;
use futures::{Stream, TryStream, TryStreamExt};
use reqwest::Body;
use reqwest_dav::{Auth, Client, ClientBuilder, Depth};
use serde::{Deserialize, Serialize};
use thiserror::Error;
mod dav;
use crate::{
update::{IsModified, ModificationState},
vfs::{Vfs, VirtualPath, VirtualPathError},
};
use dav::{TagError, dav_parse_entity_tag, dav_parse_vfs};
use super::{FSBackend, FsBackendError, FsInstanceDescription, Named};
const NC_DAV_PATH_STR: &str = "/remote.php/dav/files/";
#[derive(Error, Debug)]
pub enum NextcloudFsError {
#[error("a path provided by the server is invalid")]
InvalidPath(#[from] VirtualPathError),
#[error("a tag provided by the server is invalid")]
InvalidTag(#[from] TagError),
#[error("the structure of the nextcloud FS is not valid")]
BadStructure,
#[error("failed to decode server provided url")]
UrlDecode(#[from] FromUtf8Error),
#[error("a dav protocol error occurred during communication with the nextcloud server")]
ProtocolError(#[from] reqwest_dav::Error),
#[error("io error while sending or receiving a file")]
IoError(#[from] io::Error),
}
impl NextcloudFsError {
pub fn protocol_error_message(&self) -> Option<String> {
match self {
NextcloudFsError::ProtocolError(reqwest_dav::Error::Reqwest(error)) => {
if let Some(source) = error.source() {
if let Some(source2) = source.source() {
Some(source2.to_string())
} else {
Some(source.to_string())
}
} else {
Some(error.to_string())
}
}
_ => None,
}
}
}
impl From<reqwest::Error> for NextcloudFsError {
fn from(value: reqwest::Error) -> Self {
Self::ProtocolError(value.into())
}
}
impl From<NextcloudFsError> for FsBackendError {
fn from(value: NextcloudFsError) -> Self {
Self(Arc::new(value))
}
}
#[derive(Debug)]
pub struct NextcloudFs {
client: Client,
name: String,
}
impl NextcloudFs {
pub fn new(url: &str, login: &str, password: &str) -> Result<Self, NextcloudFsError> {
let name = login.to_string();
let client = ClientBuilder::new()
.set_host(format!("{}{}{}/", url, NC_DAV_PATH_STR, &name))
.set_auth(Auth::Basic(login.to_string(), password.to_string()))
.build()?;
Ok(Self {
client,
name: name.to_string(),
})
}
}
impl FSBackend for NextcloudFs {
type SyncInfo = NextcloudSyncInfo;
type IoError = NextcloudFsError;
type CreationInfo = NextcloudFsCreationInfo;
type Description = NextcloudFsDescription;
async fn validate(info: &Self::CreationInfo) -> Result<(), Self::IoError> {
let nextcloud: Self = info.clone().try_into()?;
nextcloud
.client
.list("", Depth::Number(0))
.await
.map(|_| ())
.map_err(|e| e.into())
}
fn description(&self) -> Self::Description {
NextcloudFsDescription {
server_url: self
.client
.host
.trim_end_matches('/')
.trim_end_matches(&self.name)
.trim_end_matches(&NC_DAV_PATH_STR)
.to_string(),
name: self.name.clone(),
}
}
async fn get_sync_info(&self, path: &VirtualPath) -> Result<Self::SyncInfo, Self::IoError> {
let elements = self.client.list(path.into(), Depth::Number(0)).await?;
let elem = elements.first().ok_or(NextcloudFsError::BadStructure)?;
dav_parse_entity_tag(elem.clone())
}
async fn load_virtual(&self) -> Result<Vfs<Self::SyncInfo>, Self::IoError> {
let elements = self.client.list("", Depth::Infinity).await?;
let vfs_root = dav_parse_vfs(elements, &self.name)?;
Ok(Vfs::new(vfs_root))
}
async fn read_file(
&self,
path: &VirtualPath,
) -> Result<impl Stream<Item = Result<Bytes, Self::IoError>> + 'static, Self::IoError> {
Ok(self
.client
.get(path.into())
.await?
.bytes_stream()
.map_err(|e| e.into()))
}
async fn write_file<Data: TryStream + Send + 'static>(
&self,
path: &VirtualPath,
data: Data,
) -> Result<Self::SyncInfo, Self::IoError>
where
Data::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
Bytes: From<Data::Ok>,
{
let body = Body::wrap_stream(data);
self.client.put(path.into(), body).await?;
let mut entities = self.client.list(path.into(), Depth::Number(0)).await?;
entities
.pop()
.ok_or(NextcloudFsError::BadStructure)
.and_then(dav_parse_entity_tag)
}
async fn rm(&self, path: &VirtualPath) -> Result<(), Self::IoError> {
self.client.delete(path.into()).await.map_err(|e| e.into())
}
async fn mkdir(&self, path: &VirtualPath) -> Result<Self::SyncInfo, Self::IoError> {
self.client.mkcol(path.into()).await?;
let mut entities = self.client.list(path.into(), Depth::Number(0)).await?;
entities
.pop()
.ok_or(NextcloudFsError::BadStructure)
.and_then(dav_parse_entity_tag)
}
async fn rmdir(&self, path: &VirtualPath) -> Result<(), Self::IoError> {
self.client.delete(path.into()).await.map_err(|e| e.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NextcloudSyncInfo {
tag: u128,
}
impl NextcloudSyncInfo {
pub fn new(tag: u128) -> Self {
Self { tag }
}
}
impl Named for NextcloudSyncInfo {
const TYPE_NAME: &'static str = "Nextcloud";
}
impl IsModified for NextcloudSyncInfo {
fn modification_state(&self, reference: &Self) -> ModificationState {
if self.tag != reference.tag {
ModificationState::Modified
} else {
ModificationState::RecursiveUnmodified
}
}
}
impl<'a> From<&'a NextcloudSyncInfo> for NextcloudSyncInfo {
fn from(value: &'a NextcloudSyncInfo) -> Self {
value.to_owned()
}
}
impl<'a> From<&'a NextcloudSyncInfo> for () {
fn from(_value: &'a NextcloudSyncInfo) -> Self {}
}
#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct NextcloudFsDescription {
server_url: String,
name: String,
}
impl FsInstanceDescription for NextcloudFsDescription {
fn name(&self) -> &str {
&self.name
}
}
impl Display for NextcloudFsDescription {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "url: {}, folder: {}", self.server_url, self.name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NextcloudFsCreationInfo {
server_url: String,
login: String,
password: String,
}
impl NextcloudFsCreationInfo {
pub fn new(server_url: &str, login: &str, password: &str) -> Self {
Self {
server_url: server_url.to_string(),
login: login.to_string(),
password: password.to_string(),
}
}
}
impl From<NextcloudFsCreationInfo> for NextcloudFsDescription {
fn from(value: NextcloudFsCreationInfo) -> Self {
Self {
server_url: value.server_url,
name: value.login,
}
}
}
impl TryFrom<NextcloudFsCreationInfo> for NextcloudFs {
type Error = <NextcloudFs as FSBackend>::IoError;
fn try_from(value: NextcloudFsCreationInfo) -> Result<Self, Self::Error> {
Self::new(&value.server_url, &value.login, &value.password)
}
}