diff --git a/src/app/webdav.ts b/src/app/webdav.ts index e91c76d..cae0721 100644 --- a/src/app/webdav.ts +++ b/src/app/webdav.ts @@ -102,34 +102,47 @@ export interface EntryInfo { * path-relative work everywhere else. */ function pathFromUri(uri: string, baseUrl: string): string { + let p: string; try { // Absolute URL → take just the pathname. if (/^https?:\/\//i.test(uri)) { const u = new URL(uri); - return normalizePath(decodeURIComponent(u.pathname)); + p = u.pathname; + } else { + p = uri; } } catch { - /* ignore */ + p = uri; } - // Already-relative URI: strip a host prefix if the server added one, - // then ensure a leading `/`. - let p = uri; + // Strip a host prefix if the server added one (e.g. lighttpd). try { const host = new URL(baseUrl).hostname; if (p.startsWith('/' + host + '/')) p = p.slice(host.length + 1); } catch { /* ignore */ } + // Always URL-decode so the rest of the app sees literal characters + // (e.g. spaces) instead of %20 in names and paths. + try { + p = decodeURIComponent(p); + } catch { + /* leave as-is on malformed encoding */ + } return normalizePath(p); } function toEntryInfo(e: WebDAVEntry, baseUrl: string): EntryInfo { const rawPath = e.path && e.path.length > 0 ? e.path : e.uri; const fullPath = pathFromUri(rawPath, baseUrl); - // Some servers (notably webdav3.py) report the full path as the - // for collections. Always use the leaf of the resolved - // path so the UI shows a name, not a path. - const name = basename(fullPath) || e.name; + // Prefer the server-provided displayname when it looks like a leaf + // (no slashes). Some servers (notably older webdav3.py) used to + // return the full path as the displayname; in that case fall back to + // the leaf of the resolved path. + const looksLikePath = (s: string) => s.includes('/'); + const name = + (e.name && !looksLikePath(e.name) ? e.name : '') || + basename(fullPath) || + e.name; return { name, path: fullPath, @@ -149,11 +162,18 @@ export async function listDirectory(path: string): Promise { // The manager's `open` requires an absolute URL on the configured server. const collectionUrl = pathToUrl(normalizePath(path), base); const listing = await manager.open(collectionUrl); + const selfPath = normalizePath(path); const entries: EntryInfo[] = []; for (const e of Object.values(listing)) { - if (e && e.name) { - entries.push(toEntryInfo(e, base)); - } + if (!e || !e.name) continue; + const info = toEntryInfo(e, base); + // Filter out the listing root itself (it appears as an entry whose + // path equals the requested collection) and any entries whose path + // doesn't sit directly under the requested collection. + if (info.path === selfPath) continue; + const parent = splitPath(info.path).parent; + if (parent !== selfPath) continue; + entries.push(info); } // Folders first, then alpha (case-insensitive). entries.sort((a, b) => {