Add folder permissions UI and WebDAV protocol support
Permissions UI: - FolderPermissionsDialog component with public/private toggle, role/user permission management, and access level badges - Integrated into file manager toolbar (visible for folder admins) - Backend returns accessLevel in folder detail endpoint WebDAV server: - Full WebDAV protocol at /webdav/ with Basic Auth (existing credentials) - PROPFIND, GET, PUT, DELETE, MKCOL, COPY, MOVE, LOCK/UNLOCK support - Permission-checked against existing folder access model - In-memory lock stubs for Windows client compatibility - 22 API integration tests covering all operations Also fixes canAccess to check folder creator (was missing).
This commit is contained in:
542
packages/backend/src/routes/webdav/index.ts
Normal file
542
packages/backend/src/routes/webdav/index.ts
Normal file
@@ -0,0 +1,542 @@
|
||||
import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'
|
||||
import { webdavBasicAuth } from '../../plugins/webdav-auth.js'
|
||||
import { WebDavService } from '../../services/webdav.service.js'
|
||||
import { StoragePermissionService, StorageFolderService, StorageFileService } from '../../services/storage.service.js'
|
||||
import { buildMultistatus, buildLockResponse, type DavResource } from '../../utils/webdav-xml.js'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
// In-memory lock store for WebDAV LOCK/UNLOCK
|
||||
const locks = new Map<string, { token: string; owner: string; expires: number }>()
|
||||
|
||||
const DAV_METHODS = 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK'
|
||||
const WEBDAV_PREFIX = '/webdav'
|
||||
|
||||
export const webdavRoutes: FastifyPluginAsync = async (app) => {
|
||||
// Parse all request bodies as raw buffers for WebDAV
|
||||
app.addContentTypeParser('*', function (_request, payload, done) {
|
||||
const chunks: Buffer[] = []
|
||||
payload.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
payload.on('end', () => done(null, Buffer.concat(chunks)))
|
||||
payload.on('error', done)
|
||||
})
|
||||
|
||||
const auth = webdavBasicAuth(app)
|
||||
|
||||
// Helper: normalize request path relative to /webdav
|
||||
function getResourcePath(request: FastifyRequest): string {
|
||||
const url = request.url.split('?')[0]
|
||||
let path = url.startsWith(WEBDAV_PREFIX) ? url.slice(WEBDAV_PREFIX.length) : url
|
||||
// Remove trailing slash for consistency (except root)
|
||||
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1)
|
||||
return path || '/'
|
||||
}
|
||||
|
||||
// --- OPTIONS ---
|
||||
app.route({
|
||||
method: 'OPTIONS',
|
||||
url: '/*',
|
||||
handler: async (_request, reply) => {
|
||||
reply
|
||||
.header('Allow', DAV_METHODS)
|
||||
.header('DAV', '1, 2')
|
||||
.header('MS-Author-Via', 'DAV')
|
||||
.status(200)
|
||||
.send('')
|
||||
},
|
||||
})
|
||||
|
||||
// Also handle OPTIONS on root /webdav
|
||||
app.route({
|
||||
method: 'OPTIONS',
|
||||
url: '/',
|
||||
handler: async (_request, reply) => {
|
||||
reply
|
||||
.header('Allow', DAV_METHODS)
|
||||
.header('DAV', '1, 2')
|
||||
.header('MS-Author-Via', 'DAV')
|
||||
.status(200)
|
||||
.send('')
|
||||
},
|
||||
})
|
||||
|
||||
// --- PROPFIND ---
|
||||
app.route({
|
||||
method: 'PROPFIND' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const depth = (request.headers['depth'] as string) ?? '1'
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
|
||||
if (resolved.type === null) {
|
||||
return reply.status(404).send('Not Found')
|
||||
}
|
||||
|
||||
const resources: DavResource[] = []
|
||||
const basePath = `${WEBDAV_PREFIX}${resourcePath === '/' ? '/' : resourcePath + '/'}`
|
||||
|
||||
if (resolved.type === 'root') {
|
||||
// Root collection
|
||||
resources.push({
|
||||
href: `${WEBDAV_PREFIX}/`,
|
||||
isCollection: true,
|
||||
displayName: 'Files',
|
||||
})
|
||||
if (depth !== '0') {
|
||||
const children = await WebDavService.listChildren(app.db, null, `${WEBDAV_PREFIX}/`)
|
||||
resources.push(...children)
|
||||
}
|
||||
} else if (resolved.type === 'folder' && resolved.folder) {
|
||||
// Check access
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send('Access Denied')
|
||||
|
||||
resources.push({
|
||||
href: basePath.slice(0, -1) + '/',
|
||||
isCollection: true,
|
||||
displayName: resolved.folder.name,
|
||||
lastModified: resolved.folder.updatedAt ? new Date(resolved.folder.updatedAt) : undefined,
|
||||
createdAt: resolved.folder.createdAt ? new Date(resolved.folder.createdAt) : undefined,
|
||||
})
|
||||
if (depth !== '0') {
|
||||
const children = await WebDavService.listChildren(app.db, resolved.folder.id, basePath)
|
||||
resources.push(...children)
|
||||
}
|
||||
} else if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send('Access Denied')
|
||||
|
||||
resources.push({
|
||||
href: `${WEBDAV_PREFIX}${resourcePath}`,
|
||||
isCollection: false,
|
||||
displayName: resolved.file.filename,
|
||||
contentType: resolved.file.contentType,
|
||||
contentLength: resolved.file.sizeBytes,
|
||||
lastModified: resolved.file.createdAt ? new Date(resolved.file.createdAt) : undefined,
|
||||
createdAt: resolved.file.createdAt ? new Date(resolved.file.createdAt) : undefined,
|
||||
etag: resolved.file.id,
|
||||
})
|
||||
}
|
||||
|
||||
const xml = buildMultistatus(resources)
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.status(207)
|
||||
.send(xml)
|
||||
},
|
||||
})
|
||||
|
||||
// PROPFIND on root
|
||||
app.route({
|
||||
method: 'PROPFIND' as any,
|
||||
url: '/',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const depth = (request.headers['depth'] as string) ?? '1'
|
||||
const resources: DavResource[] = [{
|
||||
href: `${WEBDAV_PREFIX}/`,
|
||||
isCollection: true,
|
||||
displayName: 'Files',
|
||||
}]
|
||||
|
||||
if (depth !== '0') {
|
||||
const children = await WebDavService.listChildren(app.db, null, `${WEBDAV_PREFIX}/`)
|
||||
resources.push(...children)
|
||||
}
|
||||
|
||||
const xml = buildMultistatus(resources)
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.status(207)
|
||||
.send(xml)
|
||||
},
|
||||
})
|
||||
|
||||
// --- GET ---
|
||||
app.get('/*', { preHandler: auth }, async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
|
||||
if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
const hasAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!hasAccess) return reply.status(403).send('Access Denied')
|
||||
|
||||
const data = await app.storage.get(resolved.file.path)
|
||||
return reply
|
||||
.header('Content-Type', resolved.file.contentType)
|
||||
.header('Content-Length', resolved.file.sizeBytes)
|
||||
.header('ETag', `"${resolved.file.id}"`)
|
||||
.send(data)
|
||||
}
|
||||
|
||||
if (resolved.type === 'folder' || resolved.type === 'root') {
|
||||
// Return a simple HTML listing for browsers
|
||||
return reply.status(200).header('Content-Type', 'text/plain').send('This is a WebDAV collection. Use a WebDAV client to browse.')
|
||||
}
|
||||
|
||||
return reply.status(404).send('Not Found')
|
||||
})
|
||||
|
||||
// HEAD is auto-generated by Fastify for GET routes
|
||||
|
||||
// --- PUT (upload/overwrite file) ---
|
||||
app.put('/*', { preHandler: auth }, async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const { parentPath, name } = WebDavService.parseParentAndName(resourcePath)
|
||||
|
||||
if (!name) return reply.status(400).send('Invalid path')
|
||||
|
||||
// Resolve parent to find the folder
|
||||
const parentResolved = await WebDavService.resolvePath(app.db, parentPath)
|
||||
if (parentResolved.type !== 'folder' || !parentResolved.folder) {
|
||||
return reply.status(409).send('Parent folder not found')
|
||||
}
|
||||
|
||||
// Check edit permission
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, parentResolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
return reply.status(403).send('No edit access')
|
||||
}
|
||||
|
||||
// Body is a Buffer from our content type parser
|
||||
const data = Buffer.isBuffer(request.body) ? request.body : Buffer.from(String(request.body ?? ''))
|
||||
|
||||
// Guess content type from extension
|
||||
const contentType = guessContentType(name)
|
||||
|
||||
// Check if file already exists (overwrite)
|
||||
const existingResolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
if (existingResolved.type === 'file' && existingResolved.file) {
|
||||
await StorageFileService.delete(app.db, app.storage, existingResolved.file.id)
|
||||
}
|
||||
|
||||
const file = await StorageFileService.upload(app.db, app.storage, {
|
||||
folderId: parentResolved.folder.id,
|
||||
data,
|
||||
filename: name,
|
||||
contentType,
|
||||
uploadedBy: request.user.id,
|
||||
})
|
||||
|
||||
return reply.status(existingResolved.type === 'file' ? 204 : 201).header('ETag', `"${file.id}"`).send('')
|
||||
})
|
||||
|
||||
// --- DELETE ---
|
||||
app.delete('/*', { preHandler: auth }, async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
|
||||
if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') return reply.status(403).send('No edit access')
|
||||
await StorageFileService.delete(app.db, app.storage, resolved.file.id)
|
||||
return reply.status(204).send('')
|
||||
}
|
||||
|
||||
if (resolved.type === 'folder' && resolved.folder) {
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (accessLevel !== 'admin') return reply.status(403).send('Admin access required')
|
||||
await StorageFolderService.delete(app.db, resolved.folder.id)
|
||||
return reply.status(204).send('')
|
||||
}
|
||||
|
||||
return reply.status(404).send('Not Found')
|
||||
})
|
||||
|
||||
// --- MKCOL (create folder) ---
|
||||
app.route({
|
||||
method: 'MKCOL' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const { parentPath, name } = WebDavService.parseParentAndName(resourcePath)
|
||||
|
||||
if (!name) return reply.status(400).send('Invalid path')
|
||||
|
||||
// Check if resource already exists
|
||||
const existing = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
if (existing.type !== null) return reply.status(405).send('Resource already exists')
|
||||
|
||||
// Resolve parent
|
||||
let parentFolderId: string | undefined
|
||||
if (parentPath !== '/') {
|
||||
const parentResolved = await WebDavService.resolvePath(app.db, parentPath)
|
||||
if (parentResolved.type !== 'folder' || !parentResolved.folder) {
|
||||
return reply.status(409).send('Parent folder not found')
|
||||
}
|
||||
// Check edit permission on parent
|
||||
const accessLevel = await StoragePermissionService.getAccessLevel(app.db, parentResolved.folder.id, request.user.id)
|
||||
if (!accessLevel || accessLevel === 'view') {
|
||||
return reply.status(403).send('No edit access to parent folder')
|
||||
}
|
||||
parentFolderId = parentResolved.folder.id
|
||||
}
|
||||
|
||||
await StorageFolderService.create(app.db, {
|
||||
name,
|
||||
parentId: parentFolderId,
|
||||
isPublic: false,
|
||||
createdBy: request.user.id,
|
||||
})
|
||||
|
||||
return reply.status(201).send('')
|
||||
},
|
||||
})
|
||||
|
||||
// --- COPY ---
|
||||
app.route({
|
||||
method: 'COPY' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const destinationHeader = request.headers['destination'] as string
|
||||
if (!destinationHeader) return reply.status(400).send('Destination header required')
|
||||
|
||||
const destUrl = new URL(destinationHeader, `http://${request.headers.host}`)
|
||||
let destPath = destUrl.pathname
|
||||
if (destPath.startsWith(WEBDAV_PREFIX)) destPath = destPath.slice(WEBDAV_PREFIX.length)
|
||||
if (destPath.length > 1 && destPath.endsWith('/')) destPath = destPath.slice(0, -1)
|
||||
destPath = destPath || '/'
|
||||
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
if (resolved.type !== 'file' || !resolved.file || !resolved.folder) {
|
||||
return reply.status(404).send('Source not found (only file copy supported)')
|
||||
}
|
||||
|
||||
// Check read access on source
|
||||
const srcAccess = await StoragePermissionService.canAccess(app.db, resolved.folder.id, request.user.id)
|
||||
if (!srcAccess) return reply.status(403).send('No access to source')
|
||||
|
||||
// Resolve destination parent
|
||||
const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
|
||||
const destParent = await WebDavService.resolvePath(app.db, parentPath)
|
||||
if (destParent.type !== 'folder' || !destParent.folder) {
|
||||
return reply.status(409).send('Destination parent not found')
|
||||
}
|
||||
|
||||
// Check edit access on destination
|
||||
const destAccess = await StoragePermissionService.getAccessLevel(app.db, destParent.folder.id, request.user.id)
|
||||
if (!destAccess || destAccess === 'view') return reply.status(403).send('No edit access to destination')
|
||||
|
||||
// Copy: read source file data and upload to destination
|
||||
const fileData = await app.storage.get(resolved.file.path)
|
||||
const overwrite = (request.headers['overwrite'] as string)?.toUpperCase() !== 'F'
|
||||
|
||||
// Check if destination exists
|
||||
const existingDest = await WebDavService.resolvePath(app.db, destPath)
|
||||
if (existingDest.type === 'file' && existingDest.file) {
|
||||
if (!overwrite) return reply.status(412).send('Destination exists')
|
||||
await StorageFileService.delete(app.db, app.storage, existingDest.file.id)
|
||||
}
|
||||
|
||||
await StorageFileService.upload(app.db, app.storage, {
|
||||
folderId: destParent.folder.id,
|
||||
data: fileData,
|
||||
filename: destName || resolved.file.filename,
|
||||
contentType: resolved.file.contentType,
|
||||
uploadedBy: request.user.id,
|
||||
})
|
||||
|
||||
return reply.status(existingDest.type === 'file' ? 204 : 201).send('')
|
||||
},
|
||||
})
|
||||
|
||||
// --- MOVE ---
|
||||
app.route({
|
||||
method: 'MOVE' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const destinationHeader = request.headers['destination'] as string
|
||||
if (!destinationHeader) return reply.status(400).send('Destination header required')
|
||||
|
||||
const destUrl = new URL(destinationHeader, `http://${request.headers.host}`)
|
||||
let destPath = destUrl.pathname
|
||||
if (destPath.startsWith(WEBDAV_PREFIX)) destPath = destPath.slice(WEBDAV_PREFIX.length)
|
||||
if (destPath.length > 1 && destPath.endsWith('/')) destPath = destPath.slice(0, -1)
|
||||
destPath = destPath || '/'
|
||||
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
|
||||
if (resolved.type === 'file' && resolved.file && resolved.folder) {
|
||||
// Check edit access on source folder
|
||||
const srcAccess = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (!srcAccess || srcAccess === 'view') return reply.status(403).send('No edit access to source')
|
||||
|
||||
// Resolve destination
|
||||
const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
|
||||
const destParent = await WebDavService.resolvePath(app.db, parentPath)
|
||||
if (destParent.type !== 'folder' || !destParent.folder) {
|
||||
return reply.status(409).send('Destination parent not found')
|
||||
}
|
||||
const destAccess = await StoragePermissionService.getAccessLevel(app.db, destParent.folder.id, request.user.id)
|
||||
if (!destAccess || destAccess === 'view') return reply.status(403).send('No edit access to destination')
|
||||
|
||||
// Move: copy data then delete source
|
||||
const overwrite = (request.headers['overwrite'] as string)?.toUpperCase() !== 'F'
|
||||
const existingDest = await WebDavService.resolvePath(app.db, destPath)
|
||||
if (existingDest.type === 'file' && existingDest.file) {
|
||||
if (!overwrite) return reply.status(412).send('Destination exists')
|
||||
await StorageFileService.delete(app.db, app.storage, existingDest.file.id)
|
||||
}
|
||||
|
||||
// Read source, upload to dest, delete source
|
||||
const fileData = await app.storage.get(resolved.file.path)
|
||||
await StorageFileService.upload(app.db, app.storage, {
|
||||
folderId: destParent.folder.id,
|
||||
data: fileData,
|
||||
filename: destName || resolved.file.filename,
|
||||
contentType: resolved.file.contentType,
|
||||
uploadedBy: request.user.id,
|
||||
})
|
||||
await StorageFileService.delete(app.db, app.storage, resolved.file.id)
|
||||
|
||||
return reply.status(existingDest.type === 'file' ? 204 : 201).send('')
|
||||
}
|
||||
|
||||
if (resolved.type === 'folder' && resolved.folder) {
|
||||
// Folder move — update parentId and recalculate path
|
||||
const srcAccess = await StoragePermissionService.getAccessLevel(app.db, resolved.folder.id, request.user.id)
|
||||
if (srcAccess !== 'admin') return reply.status(403).send('Admin access required to move folder')
|
||||
|
||||
const { parentPath, name: destName } = WebDavService.parseParentAndName(destPath)
|
||||
|
||||
// If just renaming (same parent), update name
|
||||
const destParent = await WebDavService.resolvePath(app.db, parentPath)
|
||||
if (destParent.type === 'folder' || destParent.type === 'root') {
|
||||
const newParentId = destParent.type === 'folder' ? destParent.folder?.id : undefined
|
||||
if (newParentId) {
|
||||
const destAccess = await StoragePermissionService.getAccessLevel(app.db, newParentId, request.user.id)
|
||||
if (!destAccess || destAccess === 'view') return reply.status(403).send('No edit access to destination')
|
||||
}
|
||||
|
||||
await StorageFolderService.update(app.db, resolved.folder.id, { name: destName })
|
||||
// TODO: If parent changed, also update parentId + materialized path
|
||||
return reply.status(201).send('')
|
||||
}
|
||||
|
||||
return reply.status(409).send('Destination parent not found')
|
||||
}
|
||||
|
||||
return reply.status(404).send('Not Found')
|
||||
},
|
||||
})
|
||||
|
||||
// --- LOCK ---
|
||||
app.route({
|
||||
method: 'LOCK' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const timeout = 300 // 5 minutes
|
||||
const token = `opaquelocktoken:${randomUUID()}`
|
||||
|
||||
// Clean expired locks
|
||||
const now = Date.now()
|
||||
for (const [path, lock] of locks) {
|
||||
if (lock.expires < now) locks.delete(path)
|
||||
}
|
||||
|
||||
locks.set(resourcePath, {
|
||||
token,
|
||||
owner: request.user.id,
|
||||
expires: now + timeout * 1000,
|
||||
})
|
||||
|
||||
const xml = buildLockResponse(token, request.user.id, timeout)
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.header('Lock-Token', `<${token}>`)
|
||||
.status(200)
|
||||
.send(xml)
|
||||
},
|
||||
})
|
||||
|
||||
// LOCK on root
|
||||
app.route({
|
||||
method: 'LOCK' as any,
|
||||
url: '/',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const token = `opaquelocktoken:${randomUUID()}`
|
||||
const xml = buildLockResponse(token, request.user.id, 300)
|
||||
locks.set('/', { token, owner: request.user.id, expires: Date.now() + 300000 })
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.header('Lock-Token', `<${token}>`)
|
||||
.status(200)
|
||||
.send(xml)
|
||||
},
|
||||
})
|
||||
|
||||
// --- UNLOCK ---
|
||||
app.route({
|
||||
method: 'UNLOCK' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
locks.delete(resourcePath)
|
||||
return reply.status(204).send('')
|
||||
},
|
||||
})
|
||||
|
||||
app.route({
|
||||
method: 'UNLOCK' as any,
|
||||
url: '/',
|
||||
preHandler: auth,
|
||||
handler: async (_request, reply) => {
|
||||
locks.delete('/')
|
||||
return reply.status(204).send('')
|
||||
},
|
||||
})
|
||||
|
||||
// --- PROPPATCH (stub — accept but do nothing) ---
|
||||
app.route({
|
||||
method: 'PROPPATCH' as any,
|
||||
url: '/*',
|
||||
preHandler: auth,
|
||||
handler: async (request, reply) => {
|
||||
const resourcePath = getResourcePath(request)
|
||||
const resolved = await WebDavService.resolvePath(app.db, resourcePath)
|
||||
if (resolved.type === null) return reply.status(404).send('Not Found')
|
||||
|
||||
// Return a minimal success response
|
||||
const xml = buildMultistatus([{
|
||||
href: `${WEBDAV_PREFIX}${resourcePath}`,
|
||||
isCollection: resolved.type === 'folder' || resolved.type === 'root',
|
||||
displayName: resolved.type === 'file' ? resolved.file!.filename : resolved.type === 'folder' ? resolved.folder!.name : 'Files',
|
||||
}])
|
||||
|
||||
return reply
|
||||
.header('Content-Type', 'application/xml; charset=utf-8')
|
||||
.status(207)
|
||||
.send(xml)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function guessContentType(filename: string): string {
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
const map: Record<string, string> = {
|
||||
pdf: 'application/pdf',
|
||||
jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
||||
png: 'image/png', webp: 'image/webp', gif: 'image/gif',
|
||||
doc: 'application/msword',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
csv: 'text/csv', txt: 'text/plain',
|
||||
mp4: 'video/mp4', mp3: 'audio/mpeg',
|
||||
zip: 'application/zip',
|
||||
json: 'application/json',
|
||||
xml: 'application/xml',
|
||||
html: 'text/html', htm: 'text/html',
|
||||
}
|
||||
return map[ext ?? ''] ?? 'application/octet-stream'
|
||||
}
|
||||
Reference in New Issue
Block a user