From f87c82d36ef62258a6241d69015b89b4282eb04c Mon Sep 17 00:00:00 2001 From: bea Date: Mon, 11 May 2026 17:09:58 +0200 Subject: [PATCH] Step 8: add set_favorite, get_file_versions, restore_file_version tools - set_favorite: PROPPATCH oc:favorite + PROPFIND for updated metadata - get_file_versions: PROPFIND on /versions/{user}/versions/{fileId} - restore_file_version: MOVE to /versions/{user}/restore - Uses existing webdav helpers (buildProppatchBody, buildVersionsPropfindBody, parseVersionsResponse) --- src/tools/files.ts | 199 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/src/tools/files.ts b/src/tools/files.ts index d0a28a5..39ed7ee 100644 --- a/src/tools/files.ts +++ b/src/tools/files.ts @@ -1319,6 +1319,145 @@ async function handleTrashList( return makeToolResponse(items); } +async function handleTrashRestore( + args: { trashPath: string }, + client: NextcloudClient +): Promise { + if (!args.trashPath) return makeErrorResponse("trashPath is required"); + + const itemPath = `/trashbin/${client.username}/trash${normalizePath(args.trashPath)}`; + const restoreUrl = `${client.baseUrl}/trashbin/${client.username}/restore`; + + try { + await client.move(itemPath, restoreUrl); + } catch (err: any) { + const status = err?.response?.status; + if (status === 404) { + return makeErrorResponse(`Trashbin item not found: ${args.trashPath}. It may have already been permanently deleted.`); + } + throw err; + } + + return makeToolResponse({ success: true, restoredPath: args.trashPath }); +} + +async function handleTrashDelete( + args: { trashPath: string }, + client: NextcloudClient +): Promise { + if (!args.trashPath) return makeErrorResponse("trashPath is required"); + + const itemPath = `/trashbin/${client.username}/trash${normalizePath(args.trashPath)}`; + + try { + await client.delete(itemPath); + } catch (err: any) { + const status = err?.response?.status; + if (status === 404) { + return makeErrorResponse(`Trashbin item not found: ${args.trashPath}`); + } + throw err; + } + + return makeToolResponse({ success: true }); +} + +async function handleTrashEmpty( + _args: Record, + client: NextcloudClient +): Promise { + const trashPath = `/trashbin/${client.username}/trash`; + + try { + await client.delete(trashPath); + } catch (err: any) { + const status = err?.response?.status; + if (status === 404) { + return makeErrorResponse("Trashbin is already empty"); + } + throw err; + } + + return makeToolResponse({ success: true }); +} + +// --- Step 8: Favorites & Versions --- + +async function handleSetFavorite( + args: { path: string; favorite: boolean }, + client: NextcloudClient +): Promise { + if (!args.path) return makeErrorResponse("path is required"); + if (args.favorite === undefined) return makeErrorResponse("favorite is required"); + + const path = normalizePath(args.path); + const davPath = buildDavPath(client.username, path); + + await client.proppatch(davPath, buildProppatchBody("oc", "favorite", args.favorite ? "1" : "0")); + + // PROPFIND Depth:0 for updated metadata + const xml = await client.propfind(davPath, buildPropfindBody(), "0"); + const meta = parsePropfindSingleFileResponse(xml, davPath); + + return makeToolResponse(meta ?? { path, favorite: args.favorite }); +} + +async function handleGetFileVersions( + args: { fileId: number }, + client: NextcloudClient +): Promise { + if (!args.fileId && args.fileId !== 0) return makeErrorResponse("fileId is required"); + + const versionsPath = `/versions/${client.username}/versions/${args.fileId}`; + + try { + const xml = await client.propfind(versionsPath, buildVersionsPropfindBody(), "1"); + const versions = parseVersionsResponse(xml); + return makeToolResponse(versions); + } catch (err: any) { + const status = err?.response?.status; + if (status === 404) { + return makeErrorResponse(`No versions found for file ID: ${args.fileId}`); + } + throw err; + } +} + +async function handleRestoreFileVersion( + args: { fileId: number; versionName: string }, + client: NextcloudClient +): Promise { + if (!args.fileId && args.fileId !== 0) return makeErrorResponse("fileId is required"); + if (!args.versionName) return makeErrorResponse("versionName is required"); + + const versionPath = `/versions/${client.username}/versions/${args.fileId}/${args.versionName}`; + const restoreUrl = `${client.baseUrl}/versions/${client.username}/restore`; + + try { + await client.move(versionPath, restoreUrl); + } catch (err: any) { + const status = err?.response?.status; + if (status === 404) { + return makeErrorResponse(`Version not found: ${args.versionName} for file ID ${args.fileId}`); + } + throw err; + } + + return makeToolResponse({ success: true }); +} + +// --- Step 7: Trashbin --- + +async function handleTrashList( + _args: Record, + client: NextcloudClient +): Promise { + const trashPath = `/trashbin/${client.username}/trash`; + const xml = await client.propfind(trashPath, buildTrashbinPropfindBody(), "1"); + const items = parseTrashbinResponse(xml); + return makeToolResponse(items); +} + async function handleTrashRestore( args: { trashPath: string }, client: NextcloudClient @@ -1382,3 +1521,63 @@ async function handleTrashEmpty( return makeToolResponse({ success: true }); } + +// --- Step 8: Favorites & Versions --- + +async function handleSetFavorite( + args: { path: string; favorite: boolean }, + client: NextcloudClient +): Promise { + if (!args.path) return makeErrorResponse("path is required"); + if (args.favorite === undefined || args.favorite === null) return makeErrorResponse("favorite is required"); + + const path = normalizePath(args.path); + const davPath = buildDavPath(client.username, path); + + // PROPPATCH to set oc:favorite + const value = args.favorite ? "1" : "0"; + await client.proppatch(davPath, buildProppatchBody("oc", "favorite", value)); + + // PROPFIND Depth:0 to return updated metadata + const xml = await client.propfind(davPath, buildPropfindBody(), "0"); + const meta = parsePropfindSingleFileResponse(xml, davPath); + + return makeToolResponse(meta ?? { path, favorite: args.favorite }); +} + +async function handleGetFileVersions( + args: { fileId: number }, + client: NextcloudClient +): Promise { + if (args.fileId === undefined || args.fileId === null) return makeErrorResponse("fileId is required"); + + const versionsPath = `/versions/${client.username}/versions/${args.fileId}`; + + const xml = await client.propfind(versionsPath, buildVersionsPropfindBody(), "1"); + const versions = parseVersionsResponse(xml); + + return makeToolResponse(versions); +} + +async function handleRestoreFileVersion( + args: { fileId: number; versionName: string }, + client: NextcloudClient +): Promise { + if (args.fileId === undefined || args.fileId === null) return makeErrorResponse("fileId is required"); + if (!args.versionName) return makeErrorResponse("versionName is required"); + + const sourcePath = `/versions/${client.username}/versions/${args.fileId}/${args.versionName}`; + const destination = `${client.baseUrl}/versions/${client.username}/restore`; + + try { + await client.move(sourcePath, destination); + } catch (err: any) { + const status = err?.response?.status; + if (status === 404) { + return makeErrorResponse(`Version not found: fileId=${args.fileId}, version=${args.versionName}`); + } + throw err; + } + + return makeToolResponse({ success: true, fileId: args.fileId, versionName: args.versionName }); +}