use reqwest_dav::list_cmd::ListEntity;
use thiserror::Error;
use urlencoding::decode;
use crate::{
sorted_vec::{Sortable, SortedVec},
vfs::{DirTree, FileMeta, VfsNode, VirtualPath, VirtualPathBuf, VirtualPathError},
};
use super::{NC_DAV_PATH_STR, NextcloudFsError, NextcloudSyncInfo};
#[derive(Error, Debug)]
pub enum TagError {
#[error("the tag is not present")]
MissingTag,
#[error("the tag is not a valid number: {0}")]
InvalidTag(String),
}
impl Sortable for ListEntity {
type Key = str;
fn key(&self) -> &Self::Key {
dav_entity_href(self)
}
}
pub(crate) fn dav_parse_vfs(
entities: Vec<ListEntity>,
folder_name: &str,
) -> Result<VfsNode<NextcloudSyncInfo>, NextcloudFsError> {
let entities = SortedVec::from_vec(entities);
let mut entities_iter = entities.into_iter().map(|entity| DavEntity {
entity,
folder_name: folder_name.to_string(),
});
let root_entity = entities_iter.next().ok_or(NextcloudFsError::BadStructure)?;
if !root_entity.name()?.is_empty() {
return Err(NextcloudFsError::BadStructure);
}
let empty_root_node = root_entity.try_into()?;
let root = match empty_root_node {
VfsNode::File(file) => VfsNode::File(file),
VfsNode::Dir(root) => VfsNode::Dir(dav_build_tree(root, &mut entities_iter)?),
};
Ok(root)
}
pub(crate) fn dav_parse_entity_tag(
entity: ListEntity,
) -> Result<NextcloudSyncInfo, NextcloudFsError> {
let entity = DavEntity {
entity,
folder_name: String::new(),
};
entity
.tag()
.map_err(|e| e.into())
.map(NextcloudSyncInfo::new)
}
fn dav_build_tree<I: ExactSizeIterator<Item = DavEntity>>(
root: DirTree<NextcloudSyncInfo>,
entities: &mut I,
) -> Result<DirTree<NextcloudSyncInfo>, NextcloudFsError> {
let mut dirs: Vec<DirTree<NextcloudSyncInfo>> = Vec::with_capacity(entities.len());
let mut current_dir = root;
for entity in entities {
while entity.parent()? != Some(current_dir.name()) {
let parent_opt = dirs.pop();
if let Some(mut parent) = parent_opt {
if parent.insert_child(VfsNode::Dir(current_dir)) {
current_dir = parent;
} else {
return Err(NextcloudFsError::BadStructure);
}
} else {
return Err(NextcloudFsError::BadStructure);
}
}
let node = entity.try_into()?;
if let VfsNode::Dir(folder) = node {
dirs.push(current_dir);
current_dir = folder;
} else {
current_dir.insert_child(node);
}
}
while let Some(mut parent) = dirs.pop() {
if parent.insert_child(VfsNode::Dir(current_dir)) {
current_dir = parent;
} else {
return Err(NextcloudFsError::BadStructure);
}
}
Ok(current_dir)
}
struct DavEntity {
entity: ListEntity,
folder_name: String,
}
impl DavEntity {
fn name(&self) -> Result<&str, VirtualPathError> {
self.path().map(|path| path.name())
}
fn parent(&self) -> Result<Option<&str>, VirtualPathError> {
Ok(self.path()?.parent().map(|parent| parent.name()))
}
fn href(&self) -> &str {
dav_entity_href(&self.entity)
}
fn path(&self) -> Result<&VirtualPath, VirtualPathError> {
let dav_path: &VirtualPath = self.href().try_into()?;
let mut path_to_folder = VirtualPathBuf::new(NC_DAV_PATH_STR).unwrap();
path_to_folder.push(&self.folder_name);
dav_path.chroot(&path_to_folder)
}
fn tag(&self) -> Result<u128, TagError> {
let tag_opt = match &self.entity {
ListEntity::File(file) => file.tag.as_ref(),
ListEntity::Folder(folder) => folder.tag.as_ref(),
};
tag_opt
.ok_or(TagError::MissingTag)
.and_then(|tag| parse_dav_tag(tag).map_err(|_| TagError::InvalidTag(tag.to_string())))
}
}
impl TryFrom<DavEntity> for VfsNode<NextcloudSyncInfo> {
type Error = NextcloudFsError;
fn try_from(value: DavEntity) -> Result<Self, Self::Error> {
let name = decode(value.name()?)?;
let tag = value.tag()?;
let sync = NextcloudSyncInfo::new(tag);
match &value.entity {
ListEntity::File(file) => Ok(VfsNode::File(FileMeta::new(
&name,
file.content_length as u64,
sync,
))),
ListEntity::Folder(_) => Ok(VfsNode::Dir(DirTree::new(&name, sync))),
}
}
}
fn parse_dav_tag(tag: &str) -> Result<u128, ()> {
let trimmed = tag.trim_matches('"');
u128::from_str_radix(trimmed, 16).map_err(|_| ())
}
fn dav_entity_href(entity: &ListEntity) -> &str {
match entity {
ListEntity::File(file) => &file.href,
ListEntity::Folder(folder) => &folder.href,
}
}
#[cfg(test)]
mod test {
use reqwest_dav::list_cmd::ListEntity;
use super::dav_parse_vfs;
#[test]
fn test_parse_vfs() {
use crate::test_utils::TestNode::{D, F};
let reference = D(
"",
vec![
D("Doc", vec![F("f1.md"), F("a spaced file.pdf")]),
D("a", vec![D("b", vec![D("c", vec![])])]),
],
)
.into_node();
let dav_folder = "[
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/\",
last_modified: \"2024-09-24T23:06:38Z\",
quota_used_bytes: Some(39044475),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fe2a304\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/Doc/\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
File(
ListFile (
href: \"/remote.php/dav/files/admin/Doc/f1.md\",
last_modified: \"2024-09-24T23:06:37Z\",
content_length: 1095,
content_type: \"text/markdown\",
tag: Some(\"ede1fda2e7e2acc2ca5311836516efba\"),
),
),
File(
ListFile (
href: \"/remote.php/dav/files/admin/Doc/a%20spaced%20file.pdf\",
last_modified: \"2024-09-24T23:06:37Z\",
content_length: 1083339,
content_type: \"application/pdf\",
tag: Some(\"53767089464852abb96fec2ff31ab2de\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/a/\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/a/b/\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/a/b/c/\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
]";
let elements: Vec<ListEntity> = ron::from_str(dav_folder).unwrap();
let res = dav_parse_vfs(elements, "admin").unwrap();
assert!(res.structural_eq(&reference))
}
#[test]
fn test_invalid_tree() {
let no_root = "[
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/a\",
last_modified: \"2024-09-24T23:06:38Z\",
quota_used_bytes: Some(39044475),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fe2a304\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/b\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
]
";
let elements: Vec<ListEntity> = ron::from_str(no_root).unwrap();
assert!(dav_parse_vfs(elements, "admin").is_err());
let bad_hierarchy = "[
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/\",
last_modified: \"2024-09-24T23:06:38Z\",
quota_used_bytes: Some(39044475),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fe2a304\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/b\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
File(
ListFile (
href: \"/remote.php/dav/files/admin/Doc/f1.md\",
last_modified: \"2024-09-24T23:06:37Z\",
content_length: 1095,
content_type: \"text/markdown\",
tag: Some(\"ede1fda2e7e2acc2ca5311836516efba\"),
),
),
]
";
let elements: Vec<ListEntity> = ron::from_str(bad_hierarchy).unwrap();
assert!(dav_parse_vfs(elements, "admin").is_err());
let bad_root = "[
Folder(
ListFolder (
href: \"/remote.php/dav/files/admin/\",
last_modified: \"2024-09-24T23:06:38Z\",
quota_used_bytes: Some(39044475),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fe2a304\"),
),
),
Folder(
ListFolder (
href: \"/remote.php/dav/files/user/b\",
last_modified: \"2024-09-24T23:06:37Z\",
quota_used_bytes: Some(1108865),
quota_available_bytes: Some(-3),
tag: Some(\"66f345fd36676\"),
),
),
File(
ListFile (
href: \"/remote.php/dav/files/user/b/f1.md\",
last_modified: \"2024-09-24T23:06:37Z\",
content_length: 1095,
content_type: \"text/markdown\",
tag: Some(\"ede1fda2e7e2acc2ca5311836516efba\"),
),
),
]
";
let elements: Vec<ListEntity> = ron::from_str(bad_root).unwrap();
assert!(dav_parse_vfs(elements, "admin").is_err());
}
}