Improve WebDav support to include file size and modified time
This commit is contained in:
@@ -143,7 +143,7 @@ export async function createFile(
|
||||
name: string,
|
||||
contents: string | ArrayBuffer,
|
||||
): Promise<boolean> {
|
||||
const rootPath = `${getFilesRootPath()}/${userId}${path}`;
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
|
||||
try {
|
||||
if (typeof contents === 'string') {
|
||||
@@ -162,14 +162,17 @@ 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 extension = (name || path).split('.').slice(-1).join('').toLowerCase();
|
||||
|
||||
const contentType = lookup(extension) || 'application/octet-stream';
|
||||
|
||||
@@ -177,14 +180,17 @@ export async function getFile(
|
||||
success: true,
|
||||
contents,
|
||||
contentType,
|
||||
byteSize: stat.size,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchFilesAndDirectories(
|
||||
userId: string,
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function addDavPrefixToKeys(object: Record<string, any>, prefix = 'D:'):
|
||||
export function getPropertyNames(xml: Record<string, any>): 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);
|
||||
}
|
||||
if (typeof prop[propKey] === 'undefined') {
|
||||
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' && propKey !== 'allprop') {
|
||||
if (propKey.startsWith('s:')) {
|
||||
notFound[propKey.slice(2)] = { '@xmlns:s': 'SAR:' };
|
||||
} else {
|
||||
|
||||
@@ -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()) || '<No Body>');
|
||||
// console.info(`Response`, response.headers);
|
||||
// console.info(`Status`, response.status);
|
||||
// }
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
@@ -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<Data, FreshContextState> = 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<Data, FreshContextState> = 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), {
|
||||
|
||||
@@ -39,7 +39,11 @@ export const handler: Handlers<Data, FreshContextState> = {
|
||||
|
||||
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(),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user