diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-02 21:25:46 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-02 21:25:46 +0200 |
| commit | 6a0cb8cc84ad2cf9bf17d008823d07e5958c6e0a (patch) | |
| tree | 05994acc3e40422157469d06c0108f7b8c136d03 | |
| parent | 9a81c96b5a3941898f99665335ad679ff2105666 (diff) | |
newOS4 v4.6.18: nginx dav trailing-slash semantics
Discovered by dav-health-check.sh E2E (10/10 pass now):
- nginx dav module REQUIRES trailing slash on MKCOL (no-slash → 409)
and on directory DELETE (no-slash → 409/404); existing dir + slash
MKCOL → 405 (the real 'already exists' signal)
- _normalizeRemotePath now PRESERVES trailing slashes (collection ops)
- _ensureDir MKCOLs with trailing slash
- _deleteFile retries with slash on 409/404 (collection delete)
- health-check expectations corrected (405 = existing, slash required)
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/ext/webdav.js | 24 | ||||
| -rw-r--r-- | scripts/test-webdav-paths.mjs | 9 |
4 files changed, 29 insertions, 12 deletions
@@ -1345,13 +1345,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.16"></script> +<script src="modules/core/core-bundle.js?v=v4.6.18"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.16"></script> +<script src="modules/core/module-loader.js?v=v4.6.18"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.16"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.18"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 16723ad..ef3e2ee 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.16'; + const LATEST_VER = 'v4.6.18'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index 5bcc4ee..e989ac9 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -194,14 +194,19 @@ class WebDAVModule { } p = p.split('?')[0].split('#')[0]; if (!p.startsWith('/')) p = '/' + p; - // Collapse duplicate slashes and resolve '..' / '.' segments + // Collapse duplicate slashes and resolve '..' / '.' segments. + // PRESERVE a trailing slash — nginx's dav module needs it for + // collection operations (MKCOL/DELETE on directories). + const trailing = /\/+$/.test(p); const out = []; for (const seg of p.split('/')) { if (seg === '' || seg === '.') continue; if (seg === '..') { out.pop(); continue; } out.push(seg); } - return '/' + out.join('/'); + let res = '/' + out.join('/'); + if (trailing && res !== '/') res += '/'; + return res; } _absoluteUrl(remotePath) { @@ -414,7 +419,10 @@ class WebDAVModule { for (let i = deepest; i < segments.length; i++) { current += '/' + segments[i]; try { - const attempt = await this._webdavRequest('MKCOL', current); + // nginx dav requires a TRAILING SLASH on MKCOL for the + // collection itself (no-slash → 409 'conflict'). Verified + // live 2026-10-02: no-slash 409, with-slash 201. + const attempt = await this._webdavRequest('MKCOL', current + '/'); if (attempt.ok || attempt.status === 405 || attempt.status === 301 || attempt.status === 302) created = true; } catch (e) { /* ignore — component may exist */ } } @@ -422,8 +430,16 @@ class WebDAVModule { } async _deleteFile(remotePath) { + const p = this._normalizeRemotePath(remotePath); const resp = await this._webdavRequest('DELETE', remotePath); - return resp.ok || resp.status === 204 || resp.status === 404; + if (resp.ok || resp.status === 204 || resp.status === 404) return true; + // nginx dav needs a trailing slash to delete a COLLECTION; a no-slash + // DELETE on a directory returns 409/404. Retry with slash. + if ((resp.status === 409 || resp.status === 404) && !p.endsWith('/')) { + const retry = await this._webdavRequest('DELETE', p + '/'); + return retry.ok || retry.status === 204 || retry.status === 404; + } + return false; } async _cleanupRetention() { diff --git a/scripts/test-webdav-paths.mjs b/scripts/test-webdav-paths.mjs index a8340c0..d75f9f6 100644 --- a/scripts/test-webdav-paths.mjs +++ b/scripts/test-webdav-paths.mjs @@ -57,7 +57,7 @@ function check(name, cond, got) { // relative + traversal + dupes: ['./a//b/../c.txt', '/a/c.txt'], ['/a/b/../../d.txt', '/d.txt'], - ['/skills/', '/skills'], + ['/skills/', '/skills/'], // trailing slash PRESERVED (nginx dav collection ops need it) ['backups', '/backups'], // foreign space stays intact (only own space prefix is stripped): ['/spaces/otheruser/file.txt', '/spaces/otheruser/file.txt'], @@ -90,8 +90,8 @@ function check(name, cond, got) { }; m._listDir = async () => { throw new Error('ENOENT'); }; await m._ensureDir('https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden'); - check('_ensureDir MKCOLs normalized path (no https:/ junk)', - mkcols.length === 1 && mkcols[0] === '/thaimassage-kiefersfelden', mkcols); + check('_ensureDir MKCOLs normalized path (no https:/ junk, trailing slash for nginx)', + mkcols.length === 1 && mkcols[0] === '/thaimassage-kiefersfelden/', mkcols); // Existing dir fast-path: __list__ probe succeeds -> no MKCOL at all const m2 = makeMod(); @@ -107,7 +107,8 @@ function check(name, cond, got) { m3._listDir = async (p) => { if (p === '/backups') return []; throw new Error('ENOENT'); }; m3._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols3.push(path); return { ok: true, status: 201 }; }; await m3._ensureDir('/backups/2026'); - check('_ensureDir MKCOLs only missing tail', mkcols3.length === 1 && mkcols3[0] === '/backups/2026', mkcols3); + check('_ensureDir MKCOLs only missing tail (trailing slash for nginx)', + mkcols3.length === 1 && mkcols3[0] === '/backups/2026/', mkcols3); } // --- PUT via _webdavRequest hits normalized URL --- |