Step 1: complete webdav.ts — XML builders + parsers for file operations
- All PROPFIND builders (standard, extended, trashbin, versions, quota) - PROPPATCH builder for property updates (favorites) - REPORT builder for favorite filtering - SEARCH builder (rfc5323) with dynamic where-clause construction - All response parsers (files, single file, search, trashbin, versions, quota) - Helper extractors for properties, booleans, numerics - Namespace handling for both prefixed and unprefixed XML tags
This commit is contained in:
+79
-29
@@ -4,8 +4,9 @@
|
||||
import { SearchOptions, FileMetadata, TrashedFile, FileVersion, QuotaInfo } from "./types.js";
|
||||
import { normalizePath } from "./utils.js";
|
||||
|
||||
// --- XML Builders (scaffolding) ---
|
||||
// --- XML Builders ---
|
||||
|
||||
/** PROPFIND standard — richiede il set di proprietà di default o quelle specificate */
|
||||
export function buildPropfindBody(selectedProps?: string[]): string {
|
||||
const props = selectedProps && selectedProps.length > 0
|
||||
? selectedProps.map((p) => ` <${p} />`).join("\n")
|
||||
@@ -28,6 +29,7 @@ ${props}
|
||||
</d:propfind>`;
|
||||
}
|
||||
|
||||
/** PROPFIND esteso — per get_file_info (include owner, checksum, has-preview) */
|
||||
export function buildPropfindExtendedBody(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
||||
@@ -50,6 +52,7 @@ export function buildPropfindExtendedBody(): string {
|
||||
</d:propfind>`;
|
||||
}
|
||||
|
||||
/** PROPFIND per cestino — proprietà trashbin-specific */
|
||||
export function buildTrashbinPropfindBody(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
||||
@@ -70,6 +73,7 @@ export function buildTrashbinPropfindBody(): string {
|
||||
</d:propfind>`;
|
||||
}
|
||||
|
||||
/** PROPFIND per versioni file */
|
||||
export function buildVersionsPropfindBody(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
@@ -82,17 +86,19 @@ export function buildVersionsPropfindBody(): string {
|
||||
</d:propfind>`;
|
||||
}
|
||||
|
||||
/** PROPPATCH generico — imposta una proprietà su una risorsa */
|
||||
export function buildProppatchBody(namespace: string, property: string, value: string): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
|
||||
<d:set>
|
||||
<d:prop>
|
||||
<${namespace}:${property}>${value}</${namespace}:${property}>
|
||||
<${namespace}:${property}>${escapeXml(value)}</${namespace}:${property}>
|
||||
</d:prop>
|
||||
</d:set>
|
||||
</d:propertyupdate>`;
|
||||
}
|
||||
|
||||
/** REPORT — filtro per file preferiti */
|
||||
export function buildFavoriteFilterBody(selectedProps?: string[]): string {
|
||||
const props = selectedProps && selectedProps.length > 0
|
||||
? selectedProps.map((p) => ` <${p} />`).join("\n")
|
||||
@@ -118,6 +124,18 @@ ${props}
|
||||
</oc:filter-files>`;
|
||||
}
|
||||
|
||||
/** PROPFIND per quota — richiede d:quota-used-bytes e d:quota-available-bytes */
|
||||
export function buildQuotaBody(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
<d:prop>
|
||||
<d:quota-used-bytes />
|
||||
<d:quota-available-bytes />
|
||||
</d:prop>
|
||||
</d:propfind>`;
|
||||
}
|
||||
|
||||
/** SEARCH request generica (rfc5323) — costruisce dinamicamente il where-clause */
|
||||
export function buildSearchRequest(options: SearchOptions): string {
|
||||
const filters: string[] = [];
|
||||
|
||||
@@ -225,72 +243,104 @@ ${limit}
|
||||
|
||||
// --- XML Parsers (regex-based, like caldav.ts) ---
|
||||
|
||||
function extractProperty(block: string, namespace: string, property: string): string | null {
|
||||
const regex = new RegExp(`<(?:${namespace}:)?${property}[^>]*>([^<]*)</(?:${namespace}:)?${property}>`, "i");
|
||||
/** Estrae il valore testuale di una proprietà XML da un blocco <response>.
|
||||
* Gestisce namespace con prefisso (<d:getcontenttype>) e senza prefisso
|
||||
* (<getcontenttype xmlns="DAV:">). */
|
||||
export function extractProperty(block: string, namespace: string, property: string): string | null {
|
||||
const regex = new RegExp(`<(?:${namespace}:)?${property}\\b[^>]*>([^<]*)</(?:${namespace}:)?${property}>`, "i");
|
||||
const match = block.match(regex);
|
||||
return match ? decodeXmlText(match[1]) : null;
|
||||
}
|
||||
|
||||
function extractNumericProperty(block: string, namespace: string, property: string): number | null {
|
||||
/** Estrae una proprietà numerica; restituisce null se mancante o non valida. */
|
||||
export function extractNumericProperty(block: string, namespace: string, property: string): number | null {
|
||||
const val = extractProperty(block, namespace, property);
|
||||
if (val === null || val === "") return null;
|
||||
const num = Number(val);
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
|
||||
function extractBooleanProperty(block: string, namespace: string, property: string): boolean {
|
||||
/** Estrae una proprietà booleana (1/true = true, tutto il resto = false). */
|
||||
export function extractBooleanProperty(block: string, namespace: string, property: string): boolean {
|
||||
const val = extractProperty(block, namespace, property);
|
||||
return val === "1" || val?.toLowerCase() === "true";
|
||||
}
|
||||
|
||||
function hasCollection(block: string): boolean {
|
||||
/** Verifica se il blocco contiene un <collection> (cartella). */
|
||||
export function hasCollection(block: string): boolean {
|
||||
return /<(?:\w+:)?collection\b/i.test(block);
|
||||
}
|
||||
|
||||
function extractHref(block: string): string | null {
|
||||
const match = block.match(/<(?:\w+:)?href[^>]*>([\s\S]*?)<\/(?:\w+:)?href>/);
|
||||
/** Estrae l'href da un blocco <response>. */
|
||||
export function extractHref(block: string): string | null {
|
||||
const match = block.match(/<(?:\w+:)?href\b[^>]*>([\s\S]*?)<\/(?:\w+:)?href>/);
|
||||
return match ? decodeXmlText(match[1].trim()) : null;
|
||||
}
|
||||
|
||||
/** Converte un blocco <response> in un oggetto FileMetadata. */
|
||||
function parseFileMetadataFromBlock(block: string, basePath: string): FileMetadata | null {
|
||||
const href = extractHref(block);
|
||||
if (!href) return null;
|
||||
|
||||
const name = extractProperty(block, "d", "displayname") || "";
|
||||
const isFolder = hasCollection(block);
|
||||
|
||||
return {
|
||||
name,
|
||||
path: resolveRelativePathFromHref(href, basePath),
|
||||
type: isFolder ? "folder" : "file",
|
||||
size: extractNumericProperty(block, "oc", "size") ?? undefined,
|
||||
contentLength: extractNumericProperty(block, "d", "getcontentlength") ?? undefined,
|
||||
mimeType: extractProperty(block, "d", "getcontenttype") ?? undefined,
|
||||
lastModified: extractProperty(block, "d", "getlastmodified") ?? undefined,
|
||||
etag: extractProperty(block, "d", "getetag") ?? undefined,
|
||||
fileId: extractNumericProperty(block, "oc", "fileid") ?? undefined,
|
||||
permissions: extractProperty(block, "oc", "permissions") ?? undefined,
|
||||
favorite: extractBooleanProperty(block, "oc", "favorite"),
|
||||
ownerId: extractProperty(block, "oc", "owner-id") ?? undefined,
|
||||
ownerDisplayName: extractProperty(block, "oc", "owner-display-name") ?? undefined,
|
||||
hasPreview: extractBooleanProperty(block, "nc", "has-preview"),
|
||||
};
|
||||
}
|
||||
|
||||
/** PROPFIND response → array FileMetadata.
|
||||
* ESCLUDE il primo elemento (la cartella root stessa). */
|
||||
export function parsePropfindFilesResponse(xml: string, basePath: string): FileMetadata[] {
|
||||
const files: FileMetadata[] = [];
|
||||
const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g);
|
||||
|
||||
let isFirst = true;
|
||||
for (const match of responseMatches) {
|
||||
const block = match[0];
|
||||
const href = extractHref(block);
|
||||
if (!href) continue;
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
continue; // Skip root folder
|
||||
}
|
||||
|
||||
const name = extractProperty(block, "d", "displayname") || "";
|
||||
const isFolder = hasCollection(block);
|
||||
|
||||
files.push({
|
||||
name,
|
||||
path: resolveRelativePathFromHref(href, basePath),
|
||||
type: isFolder ? "folder" : "file",
|
||||
size: extractNumericProperty(block, "oc", "size") ?? undefined,
|
||||
contentLength: extractNumericProperty(block, "d", "getcontentlength") ?? undefined,
|
||||
mimeType: extractProperty(block, "d", "getcontenttype") ?? undefined,
|
||||
lastModified: extractProperty(block, "d", "getlastmodified") ?? undefined,
|
||||
etag: extractProperty(block, "d", "getetag") ?? undefined,
|
||||
fileId: extractNumericProperty(block, "oc", "fileid") ?? undefined,
|
||||
permissions: extractProperty(block, "oc", "permissions") ?? undefined,
|
||||
favorite: extractBooleanProperty(block, "oc", "favorite"),
|
||||
});
|
||||
const meta = parseFileMetadataFromBlock(block, basePath);
|
||||
if (meta) files.push(meta);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/** PROPFIND Depth:0 response → singolo FileMetadata.
|
||||
* Non esclude il primo elemento (è l'unico elemento richiesto). */
|
||||
export function parsePropfindSingleFileResponse(xml: string, basePath: string): FileMetadata | null {
|
||||
const files = parsePropfindFilesResponse(xml, basePath);
|
||||
return files[0] || null;
|
||||
const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g);
|
||||
for (const match of responseMatches) {
|
||||
const meta = parseFileMetadataFromBlock(match[0], basePath);
|
||||
if (meta) return meta;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseSearchResponse(xml: string, basePath: string): FileMetadata[] {
|
||||
return parsePropfindFilesResponse(xml, basePath);
|
||||
}
|
||||
|
||||
/** PROPFIND trashbin → array TrashedFile.
|
||||
* Il primo elemento è il cestino stesso; viene incluso (diverso da parsePropfindFilesResponse). */
|
||||
export function parseTrashbinResponse(xml: string): TrashedFile[] {
|
||||
const files: TrashedFile[] = [];
|
||||
const responseMatches = xml.matchAll(/<(?:\w+:)?response\b[\s\S]*?<\/(?:\w+:)?response>/g);
|
||||
|
||||
Reference in New Issue
Block a user