@@ -27,11 +27,12 @@ export function escapeXml(s: string): string {
2727 return s . replace ( / [ & < > " ' ] / g, ( c ) => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } ) [ c ] ! ) ;
2828}
2929
30- /** Absolute href for a path, with a trailing slash for collections. */
31- export function hrefFor ( segments : string [ ] , isCollection : boolean ) : string {
30+ /** Absolute href for a path, with a trailing slash for collections. `base` is the public path to
31+ * the WebDAV root (e.g. "/dav", or "/api/dav" behind a proxy), so links work through nginx. */
32+ export function hrefFor ( base : string , segments : string [ ] , isCollection : boolean ) : string {
3233 const encoded = segments . map ( ( s ) => encodeURIComponent ( s ) ) . join ( '/' ) ;
33- const base = encoded ? `${ PREFIX } /${ encoded } ` : ` ${ PREFIX } ` ;
34- return isCollection ? `${ base } /` : base ;
34+ const full = encoded ? `${ base } /${ encoded } ` : base ;
35+ return isCollection ? `${ full } /` : full ;
3536}
3637
3738/** Split a WebDAV request path (the part after /dav) into decoded, non-empty segments. */
@@ -48,6 +49,13 @@ export function pathSegments(star: string | undefined): string[] {
4849 } ) ;
4950}
5051
52+ /** Public path to the WebDAV root, honouring a reverse-proxy prefix (X-Forwarded-Prefix) so the
53+ * hrefs we emit match the URL the client actually used (e.g. /api/dav behind nginx). */
54+ function davBase ( req : FastifyRequest ) : string {
55+ const fwd = String ( req . headers [ 'x-forwarded-prefix' ] ?? '' ) . trim ( ) . replace ( / \/ + $ / , '' ) ;
56+ return `${ fwd } ${ PREFIX } ` ;
57+ }
58+
5159type Resolved =
5260 | { type : 'root' }
5361 | { type : 'folder' ; folder : Folder ; parentId : string | null }
@@ -76,12 +84,12 @@ async function resolvePath(ownerId: string, segments: string[]): Promise<Resolve
7684 return { type : 'missing' , parentId, name : last } ;
7785}
7886
79- function propXml ( segments : string [ ] , isCollection : boolean , name : string , size : number , mtime : Date , mime : string ) : string {
87+ function propXml ( base : string , segments : string [ ] , isCollection : boolean , name : string , size : number , mtime : Date , mime : string ) : string {
8088 const props = isCollection
8189 ? `<D:resourcetype><D:collection/></D:resourcetype>`
8290 : `<D:resourcetype/><D:getcontentlength>${ size } </D:getcontentlength><D:getcontenttype>${ escapeXml ( mime ) } </D:getcontenttype>` ;
8391 return (
84- `<D:response><D:href>${ escapeXml ( hrefFor ( segments , isCollection ) ) } </D:href>` +
92+ `<D:response><D:href>${ escapeXml ( hrefFor ( base , segments , isCollection ) ) } </D:href>` +
8593 `<D:propstat><D:prop>` +
8694 `<D:displayname>${ escapeXml ( name ) } </D:displayname>` +
8795 `<D:getlastmodified>${ mtime . toUTCString ( ) } </D:getlastmodified>` +
@@ -153,16 +161,18 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
153161
154162 const node = await resolvePath ( ownerId , segments ) ;
155163
164+ const base = davBase ( req ) ;
165+
156166 if ( method === 'PROPFIND' ) {
157167 if ( node . type === 'invalid' || node . type === 'missing' ) return reply . code ( 404 ) . send ( ) ;
158168 const depth = String ( req . headers . depth ?? '1' ) ;
159169 const parts : string [ ] = [ ] ;
160170 if ( node . type === 'root' ) {
161- parts . push ( propXml ( [ ] , true , 'OpenCoperLock' , 0 , new Date ( ) , '' ) ) ;
171+ parts . push ( propXml ( base , [ ] , true , 'OpenCoperLock' , 0 , new Date ( ) , '' ) ) ;
162172 } else if ( node . type === 'folder' ) {
163- parts . push ( propXml ( segments , true , node . folder . name , 0 , node . folder . createdAt , '' ) ) ;
173+ parts . push ( propXml ( base , segments , true , node . folder . name , 0 , node . folder . createdAt , '' ) ) ;
164174 } else {
165- parts . push ( propXml ( segments , false , node . file . name , Number ( node . file . sizeBytes ) , node . file . createdAt , node . file . mimeType ) ) ;
175+ parts . push ( propXml ( base , segments , false , node . file . name , Number ( node . file . sizeBytes ) , node . file . createdAt , node . file . mimeType ) ) ;
166176 }
167177 if ( depth !== '0' && node . type !== 'file' ) {
168178 const parentId = node . type === 'root' ? null : node . folder . id ;
@@ -173,8 +183,8 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
173183 orderBy : { name : 'asc' } ,
174184 } ) ,
175185 ] ) ;
176- for ( const f of folders ) parts . push ( propXml ( [ ...segments , f . name ] , true , f . name , 0 , f . createdAt , '' ) ) ;
177- for ( const f of files ) parts . push ( propXml ( [ ...segments , f . name ] , false , f . name , Number ( f . sizeBytes ) , f . createdAt , f . mimeType ) ) ;
186+ for ( const f of folders ) parts . push ( propXml ( base , [ ...segments , f . name ] , true , f . name , 0 , f . createdAt , '' ) ) ;
187+ for ( const f of files ) parts . push ( propXml ( base , [ ...segments , f . name ] , false , f . name , Number ( f . sizeBytes ) , f . createdAt , f . mimeType ) ) ;
178188 }
179189 return reply
180190 . header ( 'Content-Type' , 'application/xml; charset=utf-8' )
@@ -267,7 +277,7 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
267277 . code ( 207 )
268278 . send (
269279 `<?xml version="1.0" encoding="utf-8"?>\n<D:multistatus xmlns:D="DAV:"><D:response>` +
270- `<D:href>${ escapeXml ( hrefFor ( segments , node . type === 'folder' || node . type === 'root' ) ) } </D:href>` +
280+ `<D:href>${ escapeXml ( hrefFor ( base , segments , node . type === 'folder' || node . type === 'root' ) ) } </D:href>` +
271281 `<D:propstat><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>` ,
272282 ) ;
273283 }
0 commit comments