2026-03-04 14:02:15 +01:00
|
|
|
use axum::{
|
|
|
|
|
body::Body,
|
|
|
|
|
http::{HeaderName, Request, StatusCode, header},
|
|
|
|
|
response::Response,
|
|
|
|
|
};
|
|
|
|
|
use quick_xml::{
|
|
|
|
|
Writer,
|
|
|
|
|
events::{BytesEnd, BytesStart, Event},
|
|
|
|
|
};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-06-17 01:44:24 +02:00
|
|
|
use crate::application::ports::file_ports::FileRetrievalUseCase;
|
|
|
|
|
use crate::application::ports::folder_ports::FolderUseCase;
|
2026-03-04 14:02:15 +01:00
|
|
|
use crate::application::ports::trash_ports::TrashUseCase;
|
|
|
|
|
use crate::common::di::AppState;
|
|
|
|
|
use crate::interfaces::errors::AppError;
|
2026-03-09 00:08:34 +01:00
|
|
|
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
2026-03-04 14:02:15 +01:00
|
|
|
use crate::interfaces::nextcloud::webdav_handler::{
|
2026-06-17 01:44:24 +02:00
|
|
|
batch_resolve_ids, extract_nc_subpath_from_dest, format_oc_id, nc_to_internal_path,
|
|
|
|
|
write_text_element,
|
2026-03-04 14:02:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
|
|
|
|
|
|
|
|
|
/// Dispatch Nextcloud WebDAV trashbin request to the appropriate handler.
|
|
|
|
|
///
|
|
|
|
|
/// `subpath` is everything after `/remote.php/dav/trashbin/{user}/`.
|
|
|
|
|
pub async fn handle_nc_trashbin(
|
|
|
|
|
state: Arc<AppState>,
|
|
|
|
|
req: Request<Body>,
|
2026-03-09 00:08:34 +01:00
|
|
|
user: AuthUser,
|
2026-03-04 14:02:15 +01:00
|
|
|
subpath: String,
|
|
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let method = req.method().clone();
|
|
|
|
|
let subpath_trimmed = subpath.trim_matches('/');
|
|
|
|
|
|
|
|
|
|
match method.as_str() {
|
|
|
|
|
"OPTIONS" => handle_options(),
|
|
|
|
|
"PROPFIND" if subpath_trimmed == "trash" || subpath_trimmed.is_empty() => {
|
|
|
|
|
handle_propfind(state, &user).await
|
|
|
|
|
}
|
|
|
|
|
"MOVE" if subpath_trimmed.starts_with("trash/") => {
|
2026-06-17 01:44:24 +02:00
|
|
|
let dest_header = req
|
|
|
|
|
.headers()
|
|
|
|
|
.get("destination")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.map(|s| s.to_string());
|
|
|
|
|
handle_restore(state, dest_header, &user, subpath_trimmed).await
|
2026-03-04 14:02:15 +01:00
|
|
|
}
|
|
|
|
|
"DELETE" if subpath_trimmed == "trash" || subpath_trimmed.is_empty() => {
|
|
|
|
|
handle_empty_trash(state, &user).await
|
|
|
|
|
}
|
|
|
|
|
"DELETE" if subpath_trimmed.starts_with("trash/") => {
|
|
|
|
|
handle_delete_permanent(state, &user, subpath_trimmed).await
|
|
|
|
|
}
|
|
|
|
|
_ => Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::METHOD_NOT_ALLOWED)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ──────────────────── OPTIONS ────────────────────
|
|
|
|
|
|
|
|
|
|
fn handle_options() -> Result<Response<Body>, AppError> {
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header(HEADER_DAV, "1, 2, 3")
|
|
|
|
|
.header(header::ALLOW, "OPTIONS, PROPFIND, MOVE, DELETE")
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ──────────────────── PROPFIND (list trash) ────────────────────
|
|
|
|
|
|
|
|
|
|
async fn handle_propfind(
|
|
|
|
|
state: Arc<AppState>,
|
|
|
|
|
user: &CurrentUser,
|
|
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let trash_svc = state
|
|
|
|
|
.trash_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
|
|
|
|
|
|
|
|
|
let items = trash_svc
|
2026-03-07 14:59:32 +01:00
|
|
|
.get_trash_items(user.id)
|
2026-03-04 14:02:15 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to list trash: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
let nc = state.nextcloud.as_ref();
|
|
|
|
|
let file_id_svc = nc.map(|n| &n.file_ids);
|
|
|
|
|
|
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
|
write_trashbin_multistatus(&mut buf, &items, &user.username, file_id_svc)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("XML generation failed: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::MULTI_STATUS)
|
|
|
|
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
|
|
|
|
.body(Body::from(buf))
|
|
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ──────────────────── MOVE (restore) ────────────────────
|
|
|
|
|
|
|
|
|
|
async fn handle_restore(
|
|
|
|
|
state: Arc<AppState>,
|
2026-06-17 01:44:24 +02:00
|
|
|
dest_header: Option<String>,
|
2026-03-04 14:02:15 +01:00
|
|
|
user: &CurrentUser,
|
|
|
|
|
subpath: &str,
|
|
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let id = extract_trash_id(subpath)?;
|
|
|
|
|
|
|
|
|
|
let trash_svc = state
|
|
|
|
|
.trash_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
|
|
|
|
|
2026-06-17 01:44:24 +02:00
|
|
|
// RFC 4918 §9.9.4 + Sabre convention: clients send `Destination` to
|
|
|
|
|
// tell the server where the restored item should land. We don't yet
|
|
|
|
|
// honor it for relocation (restore always lands at the original
|
|
|
|
|
// path), but we DO honor it for the collision check: if the requested
|
|
|
|
|
// destination is taken by a live resource the move must be refused
|
|
|
|
|
// with 412 — there is no `Overwrite: T` workflow for trash restore in
|
|
|
|
|
// either Sabre/DAV or the NC desktop client (a live file being
|
|
|
|
|
// silently replaced by an undeleted one would be a footgun).
|
|
|
|
|
if let Some(dest_header) = dest_header
|
|
|
|
|
&& let Some(dest_subpath) = extract_nc_subpath_from_dest(&dest_header, &user.username)
|
|
|
|
|
{
|
|
|
|
|
let dest_internal = nc_to_internal_path(&user.username, &dest_subpath)?;
|
|
|
|
|
let folder_service = &state.applications.folder_service;
|
|
|
|
|
let file_service = &state.applications.file_retrieval_service;
|
|
|
|
|
let dest_taken = file_service.get_file_by_path(&dest_internal).await.is_ok()
|
|
|
|
|
|| folder_service
|
|
|
|
|
.get_folder_by_path(&dest_internal)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok();
|
|
|
|
|
if dest_taken {
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::PRECONDITION_FAILED)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 23:24:00 +02:00
|
|
|
match trash_svc.restore_item(&id, user.id).await {
|
|
|
|
|
Ok(()) => Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::CREATED)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap()),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
// Collision at the original path — a live file/folder is sitting
|
|
|
|
|
// where the trashed one wants to come back to. Mirrors the G4/G5
|
|
|
|
|
// semantics in webdav_handler::handle_move ("Overwrite: F to an
|
|
|
|
|
// existing path → 412"); restore has no Overwrite header so the
|
|
|
|
|
// refusal is unconditional. The caller can resolve by renaming
|
|
|
|
|
// or trashing the conflicting live resource first.
|
|
|
|
|
//
|
|
|
|
|
// We string-match for the unique-index / duplicate-key signature
|
|
|
|
|
// because restore_item currently re-wraps every storage error as
|
|
|
|
|
// InternalError, so the original DomainError::AlreadyExists kind
|
|
|
|
|
// is not propagated. A follow-up should thread the kind through
|
|
|
|
|
// and let this be a kind-based check.
|
|
|
|
|
let msg = format!("{}", e);
|
|
|
|
|
if msg.contains("duplicate key")
|
|
|
|
|
|| msg.contains("unique constraint")
|
|
|
|
|
|| msg.to_ascii_lowercase().contains("already exists")
|
|
|
|
|
{
|
|
|
|
|
return Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::PRECONDITION_FAILED)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap());
|
|
|
|
|
}
|
|
|
|
|
Err(AppError::internal_error(format!(
|
|
|
|
|
"Failed to restore item: {}",
|
|
|
|
|
e
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-04 14:02:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ──────────────────── DELETE (empty trash) ────────────────────
|
|
|
|
|
|
|
|
|
|
async fn handle_empty_trash(
|
|
|
|
|
state: Arc<AppState>,
|
|
|
|
|
user: &CurrentUser,
|
|
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let trash_svc = state
|
|
|
|
|
.trash_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
|
|
|
|
|
|
|
|
|
trash_svc
|
2026-03-07 14:59:32 +01:00
|
|
|
.empty_trash(user.id)
|
2026-03-04 14:02:15 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| AppError::internal_error(format!("Failed to empty trash: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ──────────────────── DELETE (single item) ────────────────────
|
|
|
|
|
|
|
|
|
|
async fn handle_delete_permanent(
|
|
|
|
|
state: Arc<AppState>,
|
|
|
|
|
user: &CurrentUser,
|
|
|
|
|
subpath: &str,
|
|
|
|
|
) -> Result<Response<Body>, AppError> {
|
|
|
|
|
let id = extract_trash_id(subpath)?;
|
|
|
|
|
|
|
|
|
|
let trash_svc = state
|
|
|
|
|
.trash_service
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
|
|
|
|
|
|
|
|
|
trash_svc
|
2026-03-07 14:59:32 +01:00
|
|
|
.delete_permanently(&id, user.id)
|
2026-03-04 14:02:15 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
AppError::internal_error(format!("Failed to permanently delete item: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
|
.body(Body::empty())
|
|
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ────────────── Helpers ──────────────
|
|
|
|
|
|
|
|
|
|
/// Extract the item ID from a trashbin subpath like `trash/{id}`.
|
|
|
|
|
fn extract_trash_id(subpath: &str) -> Result<String, AppError> {
|
|
|
|
|
// subpath is already trimmed, e.g. "trash/some-uuid"
|
|
|
|
|
subpath
|
|
|
|
|
.strip_prefix("trash/")
|
|
|
|
|
.map(|s| s.trim_matches('/').to_string())
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
.ok_or_else(|| AppError::bad_request("Missing trash item ID in path"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Infer MIME content type from filename extension.
|
|
|
|
|
fn mime_from_name(name: &str) -> String {
|
|
|
|
|
mime_guess::from_path(name)
|
|
|
|
|
.first_or_octet_stream()
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Strip the "My Folder - {username}/" prefix from an original path to produce
|
|
|
|
|
/// the Nextcloud-relative original location.
|
|
|
|
|
fn strip_home_prefix<'a>(original_path: &'a str, username: &str) -> &'a str {
|
|
|
|
|
let prefix = format!("My Folder - {}/", username);
|
|
|
|
|
original_path.strip_prefix(&prefix).unwrap_or(original_path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ────────────── Trashbin PROPFIND XML Generation ──────────────
|
|
|
|
|
|
|
|
|
|
use crate::application::dtos::trash_dto::TrashedItemDto;
|
|
|
|
|
use crate::application::services::nextcloud_file_id_service::NextcloudFileIdService;
|
2026-06-10 08:39:06 +00:00
|
|
|
use std::collections::HashMap;
|
2026-03-04 14:02:15 +01:00
|
|
|
|
|
|
|
|
/// Generate a complete Nextcloud-compatible multistatus XML response for the trashbin.
|
|
|
|
|
async fn write_trashbin_multistatus<W: std::io::Write>(
|
|
|
|
|
writer: W,
|
|
|
|
|
items: &[TrashedItemDto],
|
|
|
|
|
username: &str,
|
|
|
|
|
file_id_svc: Option<&Arc<NextcloudFileIdService>>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
let mut xml = Writer::new(writer);
|
|
|
|
|
|
|
|
|
|
// Root element with all required namespaces.
|
|
|
|
|
let mut ms = BytesStart::new("d:multistatus");
|
|
|
|
|
ms.push_attribute(("xmlns:d", "DAV:"));
|
|
|
|
|
ms.push_attribute(("xmlns:oc", "http://owncloud.org/ns"));
|
|
|
|
|
ms.push_attribute(("xmlns:nc", "http://nextcloud.org/ns"));
|
|
|
|
|
xml.write_event(Event::Start(ms))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// Root container entry for the trash collection itself.
|
|
|
|
|
write_trash_root_response(&mut xml, username)?;
|
|
|
|
|
|
2026-06-10 08:39:06 +00:00
|
|
|
// Pre-resolve every oc:fileid in two batch queries by object type (was one
|
|
|
|
|
// INSERT round-trip per item). File and folder UUIDs are disjoint, so the
|
|
|
|
|
// two maps merge cleanly into one keyed by original_id.
|
|
|
|
|
let mut file_uuids: Vec<String> = Vec::new();
|
|
|
|
|
let mut folder_uuids: Vec<String> = Vec::new();
|
|
|
|
|
for item in items {
|
|
|
|
|
if item.item_type == "folder" {
|
|
|
|
|
folder_uuids.push(item.original_id.clone());
|
|
|
|
|
} else {
|
|
|
|
|
file_uuids.push(item.original_id.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let (mut id_map, folder_id_map) =
|
|
|
|
|
batch_resolve_ids(file_id_svc, &file_uuids, &folder_uuids).await;
|
|
|
|
|
id_map.extend(folder_id_map);
|
|
|
|
|
|
2026-03-04 14:02:15 +01:00
|
|
|
// Individual trashed items.
|
|
|
|
|
for item in items {
|
2026-06-10 08:39:06 +00:00
|
|
|
write_trash_item_response(&mut xml, item, username, file_id_svc, &id_map)?;
|
2026-03-04 14:02:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:multistatus")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Write the root collection response entry for the trash folder.
|
|
|
|
|
fn write_trash_root_response<W: std::io::Write>(
|
|
|
|
|
xml: &mut Writer<W>,
|
|
|
|
|
username: &str,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:response")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
let href = format!("/remote.php/dav/trashbin/{}/trash/", username);
|
|
|
|
|
write_text_element(xml, "d:href", &href)?;
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:propstat")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:prop")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// resourcetype = collection
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:resourcetype")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
xml.write_event(Event::Empty(BytesStart::new("d:collection")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:resourcetype")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:prop")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
write_text_element(xml, "d:status", "HTTP/1.1 200 OK")?;
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:propstat")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:response")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Write a single trashed item as a `<d:response>` element.
|
2026-06-10 08:39:06 +00:00
|
|
|
fn write_trash_item_response<W: std::io::Write>(
|
2026-03-04 14:02:15 +01:00
|
|
|
xml: &mut Writer<W>,
|
|
|
|
|
item: &TrashedItemDto,
|
|
|
|
|
username: &str,
|
|
|
|
|
file_id_svc: Option<&Arc<NextcloudFileIdService>>,
|
2026-06-10 08:39:06 +00:00
|
|
|
id_map: &HashMap<String, i64>,
|
2026-03-04 14:02:15 +01:00
|
|
|
) -> Result<(), String> {
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:response")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// href
|
|
|
|
|
let href = format!("/remote.php/dav/trashbin/{}/trash/{}", username, item.id);
|
|
|
|
|
write_text_element(xml, "d:href", &href)?;
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:propstat")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:prop")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
// d:displayname
|
|
|
|
|
write_text_element(xml, "d:displayname", &item.name)?;
|
|
|
|
|
|
|
|
|
|
// d:getlastmodified
|
|
|
|
|
write_text_element(xml, "d:getlastmodified", &item.trashed_at.to_rfc2822())?;
|
|
|
|
|
|
|
|
|
|
// d:getetag
|
|
|
|
|
write_text_element(xml, "d:getetag", &format!("\"{}\"", item.original_id))?;
|
|
|
|
|
|
|
|
|
|
// d:resourcetype
|
|
|
|
|
if item.item_type == "folder" {
|
|
|
|
|
xml.write_event(Event::Start(BytesStart::new("d:resourcetype")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
xml.write_event(Event::Empty(BytesStart::new("d:collection")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:resourcetype")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
} else {
|
|
|
|
|
xml.write_event(Event::Empty(BytesStart::new("d:resourcetype")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// d:getcontenttype
|
|
|
|
|
let content_type = if item.item_type == "folder" {
|
|
|
|
|
"httpd/unix-directory".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
mime_from_name(&item.name)
|
|
|
|
|
};
|
|
|
|
|
write_text_element(xml, "d:getcontenttype", &content_type)?;
|
|
|
|
|
|
|
|
|
|
// d:getcontentlength
|
|
|
|
|
write_text_element(xml, "d:getcontentlength", "0")?;
|
|
|
|
|
|
2026-06-10 08:39:06 +00:00
|
|
|
// oc:fileid and oc:id — resolved up front in a batch query.
|
|
|
|
|
let file_id = id_map.get(&item.original_id).copied();
|
2026-03-04 14:02:15 +01:00
|
|
|
if let Some(id) = file_id {
|
|
|
|
|
write_text_element(xml, "oc:fileid", &id.to_string())?;
|
|
|
|
|
let oc_id = format_oc_id(id, file_id_svc);
|
|
|
|
|
write_text_element(xml, "oc:id", &oc_id)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nc:trashbin-filename
|
|
|
|
|
write_text_element(xml, "nc:trashbin-filename", &item.name)?;
|
|
|
|
|
|
|
|
|
|
// nc:trashbin-original-location
|
|
|
|
|
let original_location = strip_home_prefix(&item.original_path, username);
|
|
|
|
|
write_text_element(xml, "nc:trashbin-original-location", original_location)?;
|
|
|
|
|
|
|
|
|
|
// nc:trashbin-deletion-time
|
|
|
|
|
write_text_element(
|
|
|
|
|
xml,
|
|
|
|
|
"nc:trashbin-deletion-time",
|
|
|
|
|
&item.trashed_at.timestamp().to_string(),
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
// oc:permissions — empty in trash
|
|
|
|
|
write_text_element(xml, "oc:permissions", "")?;
|
|
|
|
|
|
|
|
|
|
// oc:size
|
|
|
|
|
write_text_element(xml, "oc:size", "0")?;
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:prop")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
write_text_element(xml, "d:status", "HTTP/1.1 200 OK")?;
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:propstat")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
xml.write_event(Event::End(BytesEnd::new("d:response")))
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|