diff --git a/src/webdav.ts b/src/webdav.ts index cb089c0..38db58a 100644 --- a/src/webdav.ts +++ b/src/webdav.ts @@ -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} `; } +/** PROPFIND esteso — per get_file_info (include owner, checksum, has-preview) */ export function buildPropfindExtendedBody(): string { return ` @@ -50,6 +52,7 @@ export function buildPropfindExtendedBody(): string { `; } +/** PROPFIND per cestino — proprietà trashbin-specific */ export function buildTrashbinPropfindBody(): string { return ` @@ -70,6 +73,7 @@ export function buildTrashbinPropfindBody(): string { `; } +/** PROPFIND per versioni file */ export function buildVersionsPropfindBody(): string { return ` @@ -82,17 +86,19 @@ export function buildVersionsPropfindBody(): string { `; } +/** PROPPATCH generico — imposta una proprietà su una risorsa */ export function buildProppatchBody(namespace: string, property: string, value: string): string { return ` - <${namespace}:${property}>${value} + <${namespace}:${property}>${escapeXml(value)} `; } +/** 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} `; } +/** PROPFIND per quota — richiede d:quota-used-bytes e d:quota-available-bytes */ +export function buildQuotaBody(): string { + return ` + + + + + +`; +} + +/** 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}[^>]*>([^<]*)`, "i"); +/** Estrae il valore testuale di una proprietà XML da un blocco . + * Gestisce namespace con prefisso () e senza prefisso + * (). */ +export function extractProperty(block: string, namespace: string, property: string): string | null { + const regex = new RegExp(`<(?:${namespace}:)?${property}\\b[^>]*>([^<]*)`, "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 (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 . */ +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 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);