// --------------------------------------------------------------------------- // Nextcloud WebDAV client // Uses native fetch — no npm package needed. // --------------------------------------------------------------------------- function getConfig() { const url = process.env.NEXTCLOUD_URL const user = process.env.NEXTCLOUD_USER const password = process.env.NEXTCLOUD_PASSWORD const basePath = process.env.NEXTCLOUD_BASE_PATH || 'Models' if (!url || !user || !password) { throw new Error('Nextcloud non configure (NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_PASSWORD)') } // WebDAV base: https://cloud.example.com/remote.php/dav/files/{user}/ const davBase = `${url.replace(/\/+$/, '')}/remote.php/dav/files/${encodeURIComponent(user)}` const auth = 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64') return { davBase, auth, basePath } } function davUrl(davBase: string, path: string): string { const clean = path.replace(/^\/+/, '').replace(/\/+$/, '') return `${davBase}/${clean}` } // --------------------------------------------------------------------------- // Low-level WebDAV helpers // --------------------------------------------------------------------------- async function davRequest( method: string, path: string, body?: Buffer | string | null, extraHeaders?: Record, ): Promise { const { davBase, auth } = getConfig() const url = davUrl(davBase, path) const res = await fetch(url, { method, headers: { Authorization: auth, ...extraHeaders, }, body: body ?? undefined, }) return res } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** Check if a folder exists on the Nextcloud instance. */ export async function folderExists(path: string): Promise { try { const res = await davRequest('PROPFIND', path + '/', null, { Depth: '0' }) return res.status >= 200 && res.status < 300 } catch { return false } } /** * Create a folder and all parent segments if they don't exist. * Like `mkdir -p`. */ export async function mkdirRecursive(path: string): Promise { const segments = path.replace(/^\/+/, '').replace(/\/+$/, '').split('/') let current = '' for (const seg of segments) { current += '/' + seg const exists = await folderExists(current) if (!exists) { const res = await davRequest('MKCOL', current + '/') if (res.status !== 201 && res.status !== 405) { // 405 = already exists (race condition), that's fine const text = await res.text().catch(() => '') throw new Error(`MKCOL ${current} failed (${res.status}): ${text.slice(0, 200)}`) } } } } /** Upload a file (overwrite if exists). */ export async function uploadFile(path: string, content: Buffer): Promise { const res = await davRequest('PUT', path, content, { 'Content-Type': 'application/octet-stream', 'Content-Length': String(content.length), }) if (res.status < 200 || res.status >= 300) { const text = await res.text().catch(() => '') throw new Error(`PUT ${path} failed (${res.status}): ${text.slice(0, 200)}`) } } /** Move (rename) a folder or file. */ export async function moveFolder(from: string, to: string): Promise { const { davBase } = getConfig() const destination = davUrl(davBase, to) + '/' const res = await davRequest('MOVE', from + '/', null, { Destination: destination, Overwrite: 'F', }) if (res.status < 200 || res.status >= 300) { const text = await res.text().catch(() => '') throw new Error(`MOVE ${from} -> ${to} failed (${res.status}): ${text.slice(0, 200)}`) } } // --------------------------------------------------------------------------- // High-level: find next available version folder // --------------------------------------------------------------------------- /** * Find the next available Vx folder for archiving. * E.g. if V1/coffeetest exists but V2/coffeetest doesn't, returns "V2". */ export async function findNextVersion( basePath: string, folderName: string, ): Promise { for (let i = 1; ; i++) { const versionPath = `${basePath}/V${i}/${folderName}` const exists = await folderExists(versionPath) if (!exists) return `V${i}` } }