adding documentation

This commit is contained in:
DioCrafts
2025-04-04 01:48:55 +02:00
parent 01c81bfb1a
commit 868119e405
5 changed files with 2882 additions and 0 deletions
+684
View File
@@ -0,0 +1,684 @@
/**
* WebDAV Adapter Module
*
* This module provides adapters for converting between OxiCloud's domain models
* and WebDAV protocol representations. It handles XML parsing and generation
* for all WebDAV operations (PROPFIND, PROPPATCH, etc.) according to RFC 4918.
*
* The adapter serves as a translation layer between the WebDAV protocol's XML-based
* communication format and OxiCloud's internal data models, ensuring proper
* serialization and deserialization of WebDAV requests and responses.
*/
use std::io::{Read, Write};
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use thiserror::Error;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
/**
* Error types specific to WebDAV operations.
* These errors encapsulate the various failure modes during WebDAV processing.
*/
#[derive(Error, Debug)]
pub enum WebDavError {
/// Error during XML parsing or generation
#[error("XML error: {0}")]
XmlError(String),
/// Error related to property handling
#[error("Property error: {0}")]
PropertyError(String),
/// Error in the request format or content
#[error("Invalid request: {0}")]
InvalidRequest(String),
/// I/O error during reading or writing
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
/// Other WebDAV related errors
#[error("WebDAV error: {0}")]
WebDavError(String),
}
/// Type alias for WebDAV operation results
pub type Result<T> = std::result::Result<T, WebDavError>;
/**
* Property namespace and name, used to identify WebDAV properties.
* WebDAV properties are identified by a combination of namespace and name.
*/
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropertyName {
/// XML namespace for the property (e.g., "DAV:")
pub namespace: String,
/// Local name of the property (e.g., "displayname")
pub name: String,
}
/**
* Represents a WebDAV property with its name and value.
* WebDAV properties contain metadata about resources.
*/
#[derive(Debug, Clone)]
pub struct Property {
/// The qualified name of the property
pub name: PropertyName,
/// The property value, if any
pub value: Option<String>,
}
/**
* Represents a PROPFIND request as defined in RFC 4918.
* PROPFIND requests can ask for all properties, named properties,
* or property names only.
*/
#[derive(Debug, Clone)]
pub enum PropFindRequest {
/// Request all properties
AllProps,
/// Request specific properties by name
PropNames(Vec<PropertyName>),
/// Request only property names without values
PropNameOnly,
}
/**
* Adapter for WebDAV operations, providing XML serialization and deserialization.
* This struct contains methods for parsing WebDAV requests and generating
* appropriate responses according to the WebDAV specification.
*/
pub struct WebDavAdapter;
impl WebDavAdapter {
// XML namespaces used in WebDAV
const DAV_NS: &'static str = "DAV:";
/**
* Parses a PROPFIND request body into a structured representation.
*
* Processes the XML body of a PROPFIND request to determine which
* properties are being requested (allprop, propname, or specific props).
*
* @param reader Source providing XML content to parse
* @return Result containing the parsed PropFindRequest or an error
*/
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest> {
let mut xml_reader = Reader::from_reader(reader);
xml_reader.trim_text(true);
let mut buf = Vec::new();
let mut inside_propfind = false;
let mut prop_names = Vec::new();
let mut result = None;
loop {
match xml_reader.read_event(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name).map_err(|_| {
WebDavError::XmlError("Invalid XML element name".to_string())
})?;
if name_str == "propfind" {
inside_propfind = true;
} else if inside_propfind {
match name_str {
"allprop" => {
result = Some(PropFindRequest::AllProps);
},
"propname" => {
result = Some(PropFindRequest::PropNameOnly);
},
"prop" => {
// Will collect property names in subsequent iterations
},
_ if inside_propfind => {
// Handle property names within prop element
let namespace = Self::get_namespace_from_element(e)?;
prop_names.push(PropertyName {
namespace: namespace.unwrap_or_else(|| Self::DAV_NS.to_string()),
name: name_str.to_string(),
});
},
_ => {}
}
}
},
Ok(Event::Empty(ref e)) => {
// Handle self-closing tags
let name = e.name();
let name_str = std::str::from_utf8(name).map_err(|_| {
WebDavError::XmlError("Invalid XML element name".to_string())
})?;
if inside_propfind && name_str != "prop" {
let namespace = Self::get_namespace_from_element(e)?;
prop_names.push(PropertyName {
namespace: namespace.unwrap_or_else(|| Self::DAV_NS.to_string()),
name: name_str.to_string(),
});
}
},
Ok(Event::End(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name).map_err(|_| {
WebDavError::XmlError("Invalid XML element name".to_string())
})?;
if name_str == "propfind" {
inside_propfind = false;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(WebDavError::XmlError(format!("Error parsing XML: {}", e))),
_ => (),
}
buf.clear();
}
if !prop_names.is_empty() {
return Ok(PropFindRequest::PropNames(prop_names));
}
result.ok_or_else(|| WebDavError::InvalidRequest("Invalid or missing propfind request".to_string()))
}
/**
* Extracts the namespace from an XML element.
*
* @param element The XML element to extract namespace from
* @return Result containing the optional namespace or an error
*/
fn get_namespace_from_element(element: &BytesStart) -> Result<Option<String>> {
// Extract namespace from qualified name (e.g., "d:prop" -> "d")
let name = std::str::from_utf8(element.name()).map_err(|_| {
WebDavError::XmlError("Invalid XML element name".to_string())
})?;
if let Some(pos) = name.find(':') {
let prefix = &name[..pos];
// Find namespace declaration for this prefix
for attr in element.attributes() {
let attr = attr.map_err(|e| WebDavError::XmlError(format!("Invalid attribute: {}", e)))?;
let key = std::str::from_utf8(attr.key).map_err(|_| {
WebDavError::XmlError("Invalid attribute name".to_string())
})?;
if key == format!("xmlns:{}", prefix) {
let value = std::str::from_utf8(&attr.value).map_err(|_| {
WebDavError::XmlError("Invalid attribute value".to_string())
})?;
return Ok(Some(value.to_string()));
}
}
}
Ok(None)
}
/**
* Generates a PROPFIND response for a file.
*
* Creates an XML response containing the requested properties
* for a single file resource.
*
* @param writer The output destination for the generated XML
* @param file The file DTO containing the resource data
* @param request The original PROPFIND request specifying which properties to include
* @param depth The requested depth (0, 1, or infinity)
* @param href The URL of the resource
* @return Result indicating success or containing an error
*/
pub fn generate_propfind_response_for_file<W: Write>(
writer: W,
file: &FileDto,
request: &PropFindRequest,
depth: &str,
href: &str,
) -> Result<()> {
let mut xml_writer = Writer::new(writer);
// Start multistatus response
let mut multistatus = BytesStart::owned(b"d:multistatus".to_vec(), "d:multistatus".len());
multistatus.push_attribute(("xmlns:d", "DAV:"));
xml_writer.write_event(Event::Start(multistatus)).map_err(|e| {
WebDavError::XmlError(format!("Failed to write multistatus start: {}", e))
})?;
// Generate response for the file
Self::write_resource_properties(
&mut xml_writer,
href,
file.updated_at,
file.size as u64,
false, // is_collection
request,
)?;
// End multistatus
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:multistatus"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write multistatus end: {}", e))
})?;
Ok(())
}
/**
* Generates a PROPFIND response for a directory and its contents.
*
* Creates an XML response containing the requested properties for a
* directory and its children (files and subdirectories) based on the depth.
*
* @param writer The output destination for the generated XML
* @param folder The folder DTO (or None for root)
* @param files List of file DTOs contained in the folder
* @param subfolders List of subfolder DTOs contained in the folder
* @param request The original PROPFIND request specifying which properties to include
* @param depth The requested depth (0, 1, or infinity)
* @param base_href The base URL of the resource
* @return Result indicating success or containing an error
*/
pub fn generate_propfind_response<W: Write>(
writer: W,
folder: Option<&FolderDto>,
files: &[FileDto],
subfolders: &[FolderDto],
request: &PropFindRequest,
depth: &str,
base_href: &str,
) -> Result<()> {
let mut xml_writer = Writer::new(writer);
// Start multistatus response
let mut multistatus = BytesStart::owned(b"d:multistatus".to_vec(), "d:multistatus".len());
multistatus.push_attribute(("xmlns:d", "DAV:"));
xml_writer.write_event(Event::Start(multistatus)).map_err(|e| {
WebDavError::XmlError(format!("Failed to write multistatus start: {}", e))
})?;
// Add folder properties
if let Some(folder) = folder {
Self::write_resource_properties(
&mut xml_writer,
base_href,
folder.updated_at,
0, // Size for directories is typically 0
true, // is_collection
request,
)?;
}
// If depth > 0, include children
if depth != "0" {
// Add files
for file in files {
let file_href = format!("{}{}", base_href, file.name);
Self::write_resource_properties(
&mut xml_writer,
&file_href,
file.updated_at,
file.size as u64,
false, // is_collection
request,
)?;
}
// Add subfolders
for subfolder in subfolders {
let folder_href = format!("{}{}/", base_href, subfolder.name);
Self::write_resource_properties(
&mut xml_writer,
&folder_href,
subfolder.updated_at,
0, // Size for directories is typically 0
true, // is_collection
request,
)?;
}
}
// End multistatus
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:multistatus"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write multistatus end: {}", e))
})?;
Ok(())
}
/**
* Writes the properties for a single resource in a PROPFIND response.
*
* Helper method to generate XML for a single resource's properties,
* used by both file and directory PROPFIND responses.
*
* @param writer The XML writer to output to
* @param href The URL of the resource
* @param last_modified Last modification timestamp of the resource
* @param size Size of the resource in bytes
* @param is_collection Whether the resource is a collection (directory)
* @param request The original PROPFIND request specifying which properties to include
* @return Result indicating success or containing an error
*/
fn write_resource_properties<W: Write>(
xml_writer: &mut Writer<W>,
href: &str,
last_modified: DateTime<Utc>,
size: u64,
is_collection: bool,
request: &PropFindRequest,
) -> Result<()> {
// Start response element
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:response", "d:response".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write response start: {}", e))
})?;
// Write href
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:href", "d:href".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write href start: {}", e))
})?;
xml_writer.write_event(Event::Text(BytesText::from_plain_str(href))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write href text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:href"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write href end: {}", e))
})?;
// Start propstat
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:propstat", "d:propstat".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write propstat start: {}", e))
})?;
// Start prop
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:prop", "d:prop".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write prop start: {}", e))
})?;
// Determine which properties to include based on the request
match request {
PropFindRequest::AllProps => {
// Include standard properties
Self::write_standard_properties(xml_writer, last_modified, size, is_collection)?;
},
PropFindRequest::PropNames(props) => {
// Include only the requested properties
for prop_name in props {
if prop_name.namespace == Self::DAV_NS {
match prop_name.name.as_str() {
"resourcetype" => Self::write_resourcetype(xml_writer, is_collection)?,
"getcontentlength" => {
if !is_collection {
Self::write_getcontentlength(xml_writer, size)?;
}
},
"getlastmodified" => Self::write_getlastmodified(xml_writer, last_modified)?,
"creationdate" => Self::write_creationdate(xml_writer, last_modified)?,
"displayname" => {
// Extract displayname from href
let display_name = href.split('/').last().unwrap_or(href);
Self::write_displayname(xml_writer, display_name)?;
},
"getcontenttype" => {
if !is_collection {
// For files, try to determine MIME type
let content_type = if is_collection {
"httpd/unix-directory"
} else {
mime_guess::from_path(href)
.first_or_octet_stream()
.as_ref()
};
Self::write_getcontenttype(xml_writer, content_type)?;
}
},
// Add other standard properties as needed
_ => {
// Unknown property - return empty element
xml_writer.write_event(Event::Empty(BytesStart::borrowed(
format!("d:{}", prop_name.name).as_bytes(),
prop_name.name.len() + 2,
))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write property: {}", e))
})?;
}
}
}
}
},
PropFindRequest::PropNameOnly => {
// Just include empty property elements
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:resourcetype", "d:resourcetype".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write resourcetype: {}", e))
})?;
if !is_collection {
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:getcontentlength", "d:getcontentlength".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontentlength: {}", e))
})?;
}
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:getlastmodified", "d:getlastmodified".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getlastmodified: {}", e))
})?;
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:creationdate", "d:creationdate".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write creationdate: {}", e))
})?;
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:displayname", "d:displayname".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write displayname: {}", e))
})?;
if !is_collection {
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:getcontenttype", "d:getcontenttype".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontenttype: {}", e))
})?;
}
}
}
// End prop
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:prop"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write prop end: {}", e))
})?;
// Write status
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:status", "d:status".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write status start: {}", e))
})?;
xml_writer.write_event(Event::Text(BytesText::from_plain_str("HTTP/1.1 200 OK"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write status text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:status"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write status end: {}", e))
})?;
// End propstat
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:propstat"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write propstat end: {}", e))
})?;
// End response
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:response"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write response end: {}", e))
})?;
Ok(())
}
/**
* Writes all standard WebDAV properties for a resource.
*
* Helper method to write the core set of WebDAV properties that
* most clients expect.
*
* @param writer The XML writer to output to
* @param last_modified Last modification timestamp of the resource
* @param size Size of the resource in bytes
* @param is_collection Whether the resource is a collection (directory)
* @return Result indicating success or containing an error
*/
fn write_standard_properties<W: Write>(
xml_writer: &mut Writer<W>,
last_modified: DateTime<Utc>,
size: u64,
is_collection: bool,
) -> Result<()> {
// Write resourcetype (collection or not)
Self::write_resourcetype(xml_writer, is_collection)?;
// Write content length for files
if !is_collection {
Self::write_getcontentlength(xml_writer, size)?;
}
// Write last modified date
Self::write_getlastmodified(xml_writer, last_modified)?;
// Write creation date (using last modified as fallback)
Self::write_creationdate(xml_writer, last_modified)?;
// Add other standard properties as needed
Ok(())
}
// Helper methods for writing specific properties
/**
* Writes the resourcetype property.
* Indicates whether the resource is a collection (directory) or regular resource.
*/
fn write_resourcetype<W: Write>(xml_writer: &mut Writer<W>, is_collection: bool) -> Result<()> {
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:resourcetype", "d:resourcetype".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write resourcetype start: {}", e))
})?;
if is_collection {
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:collection", "d:collection".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write collection: {}", e))
})?;
}
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:resourcetype"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write resourcetype end: {}", e))
})?;
Ok(())
}
/**
* Writes the getcontentlength property.
* Contains the size of the resource in bytes.
*/
fn write_getcontentlength<W: Write>(xml_writer: &mut Writer<W>, size: u64) -> Result<()> {
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:getcontentlength", "d:getcontentlength".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontentlength start: {}", e))
})?;
xml_writer.write_event(Event::Text(BytesText::from_plain_str(&size.to_string()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontentlength text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:getcontentlength"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontentlength end: {}", e))
})?;
Ok(())
}
/**
* Writes the getlastmodified property.
* Contains the last modification date in RFC 822 format.
*/
fn write_getlastmodified<W: Write>(xml_writer: &mut Writer<W>, last_modified: DateTime<Utc>) -> Result<()> {
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:getlastmodified", "d:getlastmodified".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getlastmodified start: {}", e))
})?;
// Format as RFC 822 date as required by WebDAV
let formatted_date = last_modified.to_rfc2822();
xml_writer.write_event(Event::Text(BytesText::from_plain_str(&formatted_date))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getlastmodified text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:getlastmodified"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getlastmodified end: {}", e))
})?;
Ok(())
}
/**
* Writes the creationdate property.
* Contains the creation date in ISO 8601 format.
*/
fn write_creationdate<W: Write>(xml_writer: &mut Writer<W>, creation_date: DateTime<Utc>) -> Result<()> {
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:creationdate", "d:creationdate".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write creationdate start: {}", e))
})?;
// Format as ISO 8601 date as required by WebDAV
let formatted_date = creation_date.to_rfc3339();
xml_writer.write_event(Event::Text(BytesText::from_plain_str(&formatted_date))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write creationdate text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:creationdate"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write creationdate end: {}", e))
})?;
Ok(())
}
/**
* Writes the displayname property.
* Contains the human-readable name of the resource.
*/
fn write_displayname<W: Write>(xml_writer: &mut Writer<W>, display_name: &str) -> Result<()> {
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:displayname", "d:displayname".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write displayname start: {}", e))
})?;
xml_writer.write_event(Event::Text(BytesText::from_plain_str(display_name))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write displayname text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:displayname"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write displayname end: {}", e))
})?;
Ok(())
}
/**
* Writes the getcontenttype property.
* Contains the MIME type of the resource.
*/
fn write_getcontenttype<W: Write>(xml_writer: &mut Writer<W>, content_type: &str) -> Result<()> {
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:getcontenttype", "d:getcontenttype".len()))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontenttype start: {}", e))
})?;
xml_writer.write_event(Event::Text(BytesText::from_plain_str(content_type))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontenttype text: {}", e))
})?;
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:getcontenttype"))).map_err(|e| {
WebDavError::XmlError(format!("Failed to write getcontenttype end: {}", e))
})?;
Ok(())
}
// Additional helper methods for other WebDAV operations
// ... (PROPPATCH, LOCK, UNLOCK, etc. implementations would go here)
}