diff --git a/lib/data/files.ts b/lib/data/files.ts index 0bece3c..63c2b4a 100644 --- a/lib/data/files.ts +++ b/lib/data/files.ts @@ -143,7 +143,7 @@ export async function createFile( name: string, contents: string | ArrayBuffer, ): Promise { - const rootPath = `${getFilesRootPath()}/${userId}${path}`; + const rootPath = join(getFilesRootPath(), userId, path); try { if (typeof contents === 'string') { @@ -162,28 +162,34 @@ export async function createFile( export async function getFile( userId: string, path: string, - name: string, -): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string }> { - const rootPath = `${getFilesRootPath()}/${userId}${path}`; + name?: string, +): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string; byteSize?: number }> { + const rootPath = join(getFilesRootPath(), userId, path); try { - const contents = await Deno.readFile(join(rootPath, name)); + const stat = await Deno.stat(join(rootPath, name || '')); - const extension = name.split('.').slice(-1).join('').toLowerCase(); + if (stat) { + const contents = await Deno.readFile(join(rootPath, name || '')); - const contentType = lookup(extension) || 'application/octet-stream'; + const extension = (name || path).split('.').slice(-1).join('').toLowerCase(); - return { - success: true, - contents, - contentType, - }; + const contentType = lookup(extension) || 'application/octet-stream'; + + return { + success: true, + contents, + contentType, + byteSize: stat.size, + }; + } } catch (error) { console.error(error); - return { - success: false, - }; } + + return { + success: false, + }; } export async function searchFilesAndDirectories( @@ -213,7 +219,7 @@ async function searchDirectoryNames( userId: string, searchTerm: string, ): Promise<{ success: boolean; directories: Directory[] }> { - const rootPath = `${getFilesRootPath()}/${userId}/`; + const rootPath = join(getFilesRootPath(), userId); const directories: Directory[] = []; @@ -284,7 +290,7 @@ async function searchFileNames( userId: string, searchTerm: string, ): Promise<{ success: boolean; files: DirectoryFile[] }> { - const rootPath = `${getFilesRootPath()}/${userId}/`; + const rootPath = join(getFilesRootPath(), userId); const files: DirectoryFile[] = []; @@ -355,7 +361,7 @@ async function searchFileContents( userId: string, searchTerm: string, ): Promise<{ success: boolean; files: DirectoryFile[] }> { - const rootPath = `${getFilesRootPath()}/${userId}/`; + const rootPath = join(getFilesRootPath(), userId); const files: DirectoryFile[] = []; diff --git a/lib/utils/webdav.ts b/lib/utils/webdav.ts index c16901d..9ce5f3c 100644 --- a/lib/utils/webdav.ts +++ b/lib/utils/webdav.ts @@ -24,7 +24,7 @@ export function addDavPrefixToKeys(object: Record, prefix = 'D:'): export function getPropertyNames(xml: Record): string[] { const propFindElement = xml['D:propfind'] || xml.propfind; if (!propFindElement) { - return []; + return ['allprop']; } const propElement = propFindElement['D:prop'] || propFindElement.prop; @@ -126,11 +126,11 @@ export async function buildPropFindResponse( for (const propKey of properties) { switch (propKey) { case 'displayname': - prop.displayname = isDirectory ? filePath : filePath.split('/').pop(); + prop.displayname = isDirectory ? filePath.split('/').filter(Boolean).pop() : filePath.split('/').pop(); break; case 'getcontentlength': if (!isDirectory) { - prop.getcontentlength = (stat.size ?? 0)?.toString(); + prop.getcontentlength = stat.size?.toString() || '0'; } break; case 'getcontenttype': @@ -150,8 +150,24 @@ export async function buildPropFindResponse( case 'getetag': prop.etag = stat.mtime?.toUTCString(); break; + case 'getctag': + prop.ctag = stat.mtime?.toUTCString(); + break; + case 'allprop': + prop.displayname = isDirectory ? filePath.split('/').filter(Boolean).pop() : filePath.split('/').pop(); + if (!isDirectory) { + prop.getcontentlength = stat.size?.toString() || '0'; + prop.getcontenttype = lookup(filePath); + } + prop.resourcetype = isDirectory ? { collection: { '@xmlns:D': 'DAV:' } } : {}; + prop.getlastmodified = stat.mtime?.toUTCString() || ''; + prop.creationdate = stat.birthtime?.toUTCString() || ''; + prop.etag = stat.mtime?.toUTCString(); + prop.ctag = stat.mtime?.toUTCString(); + break; } - if (typeof prop[propKey] === 'undefined') { + + if (typeof prop[propKey] === 'undefined' && propKey !== 'allprop') { if (propKey.startsWith('s:')) { notFound[propKey.slice(2)] = { '@xmlns:s': 'SAR:' }; } else { diff --git a/routes/_middleware.tsx b/routes/_middleware.tsx index 1168f6e..182fca7 100644 --- a/routes/_middleware.tsx +++ b/routes/_middleware.tsx @@ -66,6 +66,13 @@ export const handler = [ const response = await context.next(); console.info(`${new Date().toISOString()} - [${response.status}] ${request.method} ${request.url}`); + // NOTE: Uncomment when debugging WebDav stuff + // if (request.url.includes('/dav')) { + // console.info(`Request`, request.headers); + // console.info((await request.clone().text()) || ''); + // console.info(`Response`, response.headers); + // console.info(`Status`, response.status); + // } return response; }, diff --git a/routes/dav.tsx b/routes/dav.tsx index dc74eb7..e07f347 100644 --- a/routes/dav.tsx +++ b/routes/dav.tsx @@ -10,6 +10,7 @@ import { getProperDestinationPath, getPropertyNames, } from '/lib/utils/webdav.ts'; +import { getFile } from '/lib/data/files.ts'; interface Data {} @@ -49,15 +50,20 @@ export const handler: Handler = async (request, context if (request.method === 'GET') { try { - const stat = await Deno.stat(join(rootPath, filePath)); + const fileResult = await getFile(context.state.user.id, filePath); - if (stat) { - const contents = await Deno.readFile(join(rootPath, filePath)); - - return new Response(contents, { status: 200 }); + if (!fileResult.success) { + return new Response('Not Found', { status: 404 }); } - return new Response('Not Found', { status: 404 }); + return new Response(fileResult.contents!, { + status: 200, + headers: { + 'cache-control': 'no-cache, no-store, must-revalidate', + 'content-type': fileResult.contentType!, + 'content-length': fileResult.byteSize!.toString(), + }, + }); } catch (error) { console.error(error); } @@ -80,7 +86,7 @@ export const handler: Handler = async (request, context if (request.method === 'PUT') { const contentLengthString = request.headers.get('content-length'); const contentLength = contentLengthString ? parseInt(contentLengthString, 10) : null; - const body = contentLength === 0 ? new Blob([new Uint8Array([0])]).stream() : request.body; + const body = contentLength === 0 ? new Blob([new Uint8Array([0])]).stream() : request.clone().body; try { const newFile = await Deno.open(join(rootPath, filePath), { diff --git a/routes/files/open/[fileName].tsx b/routes/files/open/[fileName].tsx index f1de96b..c277268 100644 --- a/routes/files/open/[fileName].tsx +++ b/routes/files/open/[fileName].tsx @@ -39,7 +39,11 @@ export const handler: Handlers = { return new Response(fileResult.contents!, { status: 200, - headers: { 'cache-control': 'no-cache, no-store, must-revalidate', 'content-type': fileResult.contentType! }, + headers: { + 'cache-control': 'no-cache, no-store, must-revalidate', + 'content-type': fileResult.contentType!, + 'content-length': fileResult.byteSize!.toString(), + }, }); }, };