diff --git a/packages/core/src/sync/webdav-client.test.ts b/packages/core/src/sync/webdav-client.test.ts
index 63afacff..44031d78 100644
--- a/packages/core/src/sync/webdav-client.test.ts
+++ b/packages/core/src/sync/webdav-client.test.ts
@@ -110,6 +110,51 @@ describe("WebDavClient PROPFIND parsing", () => {
]);
});
+ it("treats MKCOL auth failure as success when the parent listing shows the directory", async () => {
+ const calls: { method: string; url: string }[] = [];
+ installFetchStub((url, options) => {
+ const method = String(options?.method ?? "GET");
+ calls.push({ method, url });
+
+ if (method === "PROPFIND" && url.endsWith("/readany/")) {
+ return new Response("", { status: 404 });
+ }
+ if (method === "MKCOL") {
+ return new Response("", { status: 401 });
+ }
+ return new Response(
+ `
+
+
+ /dav/
+
+
+
+ /dav/readany/
+
+
+ `,
+ { status: 207 },
+ );
+ });
+
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ try {
+ const client = new WebDavClient("https://dav.example.com/dav", "alice", "secret");
+ await client.ensureDirectory("/readany");
+ } finally {
+ warnSpy.mockRestore();
+ }
+
+ expect(calls.map((call) => call.method)).toEqual(["PROPFIND", "MKCOL", "PROPFIND", "PROPFIND"]);
+ expect(calls.map((call) => call.url)).toEqual([
+ "https://dav.example.com/dav/readany/",
+ "https://dav.example.com/dav/readany/",
+ "https://dav.example.com/dav/readany/",
+ "https://dav.example.com/dav/",
+ ]);
+ });
+
it("uses a collection path when safely reading a directory", async () => {
const calls: { method: string; url: string }[] = [];
installFetchStub((url, options) => {
diff --git a/packages/core/src/sync/webdav-client.ts b/packages/core/src/sync/webdav-client.ts
index e39ce90b..47bddbf3 100644
--- a/packages/core/src/sync/webdav-client.ts
+++ b/packages/core/src/sync/webdav-client.ts
@@ -93,6 +93,17 @@ function toCollectionPath(path: string): string {
return path.endsWith("/") ? path : `${path}/`;
}
+function getParentCollectionPath(path: string): { parent: string; name: string } | null {
+ const collectionPath = toCollectionPath(path);
+ if (collectionPath === "/") return null;
+
+ const trimmed = collectionPath.replace(/\/+$/g, "");
+ const lastSlash = trimmed.lastIndexOf("/");
+ const name = trimmed.slice(lastSlash + 1);
+ const parent = lastSlash <= 0 ? "/" : trimmed.slice(0, lastSlash);
+ return { parent: toCollectionPath(parent), name };
+}
+
function createHttpWebDavError(
status: number,
statusText: string | undefined,
@@ -407,7 +418,7 @@ export class WebDavClient {
try {
resp = await this.request("MKCOL", collectionPath);
} catch (e) {
- if (await this.propfindExists(collectionPath, { timeoutMs: DIRECTORY_PROBE_TIMEOUT_MS })) {
+ if (await this.collectionExists(collectionPath)) {
console.warn(`[WebDAV] MKCOL ${path} failed but directory exists; continuing`);
return;
}
@@ -420,6 +431,10 @@ export class WebDavClient {
if (status === 405 || status === 409) {
return;
}
+ if ((status === 401 || status === 403) && (await this.collectionExists(collectionPath))) {
+ console.warn(`[WebDAV] MKCOL ${path} returned ${status} but directory exists; continuing`);
+ return;
+ }
throw new Error(`WebDAV MKCOL failed for ${path}: ${status} ${resp.statusText || ""}`);
}
@@ -661,6 +676,27 @@ export class WebDavClient {
}
}
+ private async parentListingContainsCollection(path: string): Promise {
+ const parentInfo = getParentCollectionPath(path);
+ if (!parentInfo) return true;
+
+ try {
+ const resources = await this.propfind(parentInfo.parent);
+ return resources.some(
+ (resource) => resource.isCollection && resource.name === parentInfo.name,
+ );
+ } catch {
+ return false;
+ }
+ }
+
+ private async collectionExists(path: string): Promise {
+ if (await this.propfindExists(path, { timeoutMs: DIRECTORY_PROBE_TIMEOUT_MS })) {
+ return true;
+ }
+ return this.parentListingContainsCollection(path);
+ }
+
/** List directory contents (PROPFIND Depth 1) */
async propfind(path: string): Promise {
const resp = await this.request("PROPFIND", path, {