93 lines
3.5 KiB
JavaScript
93 lines
3.5 KiB
JavaScript
// The WebDAV vocabulary of the app, expressed over webdavRequest.
|
|
//
|
|
// Callers speak in paths relative to the user root ('Tofu/plannings/2026-W38.json')
|
|
// and get result objects back — never an exception, never a Response. The URL
|
|
// shape, the HTTP verbs and the Basic header stay here.
|
|
|
|
import { webdavRequest } from '#tofu/services/webdav/webdavRequest.js'
|
|
import { buildBasicAuthHeader } from '#tofu/tools/webdav/buildBasicAuthHeader.js'
|
|
import { buildDavUrl } from '#tofu/tools/webdav/buildDavUrl.js'
|
|
import { describeWebdavFailure } from '#tofu/tools/webdav/describeWebdavFailure.js'
|
|
import { parsePropfindXml } from '#tofu/tools/webdav/parsePropfindXml.js'
|
|
import { PROPFIND_BODY } from '#tofu/const/PROPFIND_BODY.js'
|
|
|
|
// MKCOL on a collection that already exists answers 405. For ensureFolder that
|
|
// is the goal reached, not a failure.
|
|
const FOLDER_ALREADY_EXISTS = 405
|
|
const XML_CONTENT_TYPE = 'application/xml; charset=utf-8'
|
|
const JSON_CONTENT_TYPE = 'application/json'
|
|
|
|
export function createWebdavClient({ serverUrl, username, password, folder }) {
|
|
// Built once: the password is read here and never leaves this closure.
|
|
const authorization = buildBasicAuthHeader(username, password)
|
|
|
|
function urlFor(path) {
|
|
return buildDavUrl({ serverUrl, username, path })
|
|
}
|
|
|
|
// Everything the UI may display about the target — deliberately without the
|
|
// password.
|
|
function describeTarget() {
|
|
return { serverUrl, username, folder, baseUrl: urlFor('') }
|
|
}
|
|
|
|
async function listFolder(path) {
|
|
const result = await webdavRequest({
|
|
url: urlFor(path),
|
|
method: 'PROPFIND',
|
|
headers: { Depth: '1', 'Content-Type': XML_CONTENT_TYPE },
|
|
body: PROPFIND_BODY,
|
|
authorization
|
|
})
|
|
if (!result.ok) return result
|
|
return { ok: true, entries: parsePropfindXml(result.text, path) }
|
|
}
|
|
|
|
async function readJson(path) {
|
|
const result = await webdavRequest({
|
|
url: urlFor(path),
|
|
method: 'GET',
|
|
headers: { Accept: JSON_CONTENT_TYPE },
|
|
authorization
|
|
})
|
|
// A 404 already arrives as failure.kind === 'notFound': a file that was
|
|
// never written is a normal case, the caller decides what it means.
|
|
if (!result.ok) return result
|
|
|
|
try {
|
|
return { ok: true, data: JSON.parse(result.text) }
|
|
} catch (error) {
|
|
// The transport worked, the content did not. JSON.parse throws a
|
|
// SyntaxError, which describeWebdavFailure reports as kind 'parse'.
|
|
const failure = describeWebdavFailure({ error, response: null, method: 'GET' })
|
|
return { ok: false, failure: { ...failure, status: result.status } }
|
|
}
|
|
}
|
|
|
|
async function writeJson(path, value) {
|
|
const result = await webdavRequest({
|
|
url: urlFor(path),
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': JSON_CONTENT_TYPE },
|
|
body: JSON.stringify(value, null, 2),
|
|
authorization
|
|
})
|
|
if (!result.ok) return result
|
|
return { ok: true }
|
|
}
|
|
|
|
// MKCOL creates one level at a time: the caller creates parents first.
|
|
async function ensureFolder(path) {
|
|
const result = await webdavRequest({ url: urlFor(path), method: 'MKCOL', authorization })
|
|
if (result.ok || result.failure.status === FOLDER_ALREADY_EXISTS) return { ok: true }
|
|
return result
|
|
}
|
|
|
|
async function remove(path) {
|
|
const result = await webdavRequest({ url: urlFor(path), method: 'DELETE', authorization })
|
|
if (!result.ok) return result
|
|
return { ok: true }
|
|
}
|
|
|
|
return { describeTarget, listFolder, readJson, writeJson, ensureFolder, remove }
|
|
}
|