From 67f37f4df7e92f0dc00eaae0317f1520af39554f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= <14975046+skjnldsv@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:19:09 +0200 Subject: [PATCH] fix(viewer): keep a legacy dav source relative to its dav home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCA.Viewer.open() took the folder of a file info's source as the root of the node it builds. For a source on dav, that folder starts with /remote.php/dav, and a node rejects it: Error: The root must be relative to the service. e.g /files/emma Apps hand over the dav address of the file they open, as the photos app does for every photo, so none of them opened. A dav source now gets the home of its service as root, /files/emma or /photos/emma; any other source keeps its folder. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com> --- apps/viewer/src/legacy.spec.ts | 21 +++++++++++++++++++++ apps/viewer/src/legacy.ts | 30 ++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/apps/viewer/src/legacy.spec.ts b/apps/viewer/src/legacy.spec.ts index 40679ec5ea286..f4e8827d64fdd 100644 --- a/apps/viewer/src/legacy.spec.ts +++ b/apps/viewer/src/legacy.spec.ts @@ -110,6 +110,27 @@ describe('OCA.Viewer compatibility layer', () => { expect(target.basename).toBe('pineapple.jpg') }) + it('addresses a file with a dav source relative to its dav home', async () => { + await viewer().open({ + fileInfo: { source: 'https://cloud.example/remote.php/dav/files/emma/Photos/2019/x.jpg', filename: '/Photos/2019/x.jpg', mime: 'image/jpeg' }, + }) + + const [, target] = viewerOpen.mock.calls[0] + expect(target.source).toBe('https://cloud.example/remote.php/dav/files/emma/Photos/2019/x.jpg') + expect(target.root).toBe('/files/emma') + expect(target.path).toBe('/Photos/2019/x.jpg') + }) + + it('addresses a file of another dav service relative to its home there', async () => { + await viewer().open({ + fileInfo: { source: 'https://cloud.example/remote.php/dav/photos/emma/albums/Trip/x.jpg', mime: 'image/jpeg' }, + }) + + const [, target] = viewerOpen.mock.calls[0] + expect(target.root).toBe('/photos/emma') + expect(target.basename).toBe('x.jpg') + }) + it('passes the handler on to openWith', async () => { await viewer().openWith('richdocuments', { fileInfo: { fileid: 1, filename: '/a.pdf', mime: 'application/pdf' } }) expect(viewerOpen.mock.calls[0][3]).toBe('richdocuments') diff --git a/apps/viewer/src/legacy.ts b/apps/viewer/src/legacy.ts index 0681dafb69885..e884f45c8cd07 100644 --- a/apps/viewer/src/legacy.ts +++ b/apps/viewer/src/legacy.ts @@ -145,12 +145,33 @@ function directoryOf(source: string): string { } } +/** + * The root a node served from an address is relative to. + * + * An address on dav sits under a home of one of its services, such as + * `/files/emma`, and a node has to be relative to that home. Anything else + * has no home, so the folder the file sits in stands in for it and the node + * still knows its own name. + * + * @param source - the address the file is served from + */ +function rootOf(source: string): string { + const davRemote = getRemoteURL() + '/' + if (source.startsWith(davRemote)) { + const [service, home] = source.slice(davRemote.length).split('/') + if (service && home) { + return `/${service}/${home}` + } + } + return directoryOf(source) +} + /** * Build a node out of what an app handed over. * * Callers filled in what they had, so most of this is defaulting. A file - * with a `source` of its own is served from somewhere that is not dav, and - * keeps it; everything else is addressed under the user's files. + * with a `source` of its own keeps it; everything else is addressed under + * the user's files. * * @param info - the file info an app passed to open */ @@ -169,10 +190,7 @@ function nodeFromFileInfo(info: LegacyFileInfo): IFile { const data: NodeData = { source, - // A file under the user's files is relative to their dav home. One - // served from somewhere else has no home, so the folder it sits in - // stands in for it and the node still knows its own name. - root: info.source === undefined ? getRootPath() : directoryOf(info.source), + root: info.source === undefined ? getRootPath() : rootOf(info.source), displayname: info.basename ?? basenameOf(source), mime: info.mime ?? 'application/octet-stream', owner,