diff --git a/src/utils/webdav.utils.ts b/src/utils/webdav.utils.ts index ca989fc4..a04d0d01 100644 --- a/src/utils/webdav.utils.ts +++ b/src/utils/webdav.utils.ts @@ -7,7 +7,7 @@ import { DriveDatabaseManager } from '../services/database/drive-database-manage import { DriveFolderService } from '../services/drive/drive-folder.service'; import { DriveFileService } from '../services/drive/drive-file.service'; import { DriveFileItem, DriveFolderItem } from '../types/drive.types'; -import { NotFoundError } from './errors.utils'; +import { ConflictError, NotFoundError } from './errors.utils'; import { webdavLogger } from './logger.utils'; export class WebDavUtils { @@ -34,6 +34,7 @@ export class WebDavUtils { } else { requestUrl = urlObject.url; } + const decodedUrl = decodeURIComponent(requestUrl).replaceAll('/./', '/'); const parsedPath = path.parse(decodedUrl); let parentPath = path.dirname(decodedUrl); @@ -95,8 +96,22 @@ export class WebDavUtils { driveFileService?: DriveFileService, ): Promise { let item: DriveFileItem | DriveFolderItem | undefined = undefined; + if (resource.type === 'folder') { - item = await driveFolderService?.getFolderMetadataByPath(resource.url); + // if resource has a parentPath it means it's a subfolder then try to get it; if it throws an error it means it doesn't + // exist and we should throw a 409 error in compliance with the WebDAV RFC + // catch the error during getting parent folder and throw a 409 error in compliance with the WebDAV RFC + try { + item = await driveFolderService?.getFolderMetadataByPath(resource.url); + } + catch (error: any) { + // if the error is a 404 error, it means the resource doesn't exist + // in this case, throw a 409 error in compliance with the WebDAV RFC + if (error.status === 404) { + throw new ConflictError(`Resource not found on Internxt Drive at ${resource.url}`); + } + throw error; + } } if (resource.type === 'file') { item = await driveFileService?.getFileMetadataByPath(resource.url); diff --git a/src/webdav/handlers/MKCOL.handler.ts b/src/webdav/handlers/MKCOL.handler.ts index 44578f94..6413ff84 100644 --- a/src/webdav/handlers/MKCOL.handler.ts +++ b/src/webdav/handlers/MKCOL.handler.ts @@ -7,6 +7,7 @@ import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { AsyncUtils } from '../../utils/async.utils'; import { DriveFolderItem } from '../../types/drive.types'; +import { MethodNotAllowed } from '../../utils/errors.utils'; export class MKCOLRequestHandler implements WebDavMethodHandler { constructor( @@ -14,7 +15,7 @@ export class MKCOLRequestHandler implements WebDavMethodHandler { driveDatabaseManager: DriveDatabaseManager; driveFolderService: DriveFolderService; }, - ) {} + ) { } handle = async (req: Request, res: Response) => { const { driveDatabaseManager, driveFolderService } = this.dependencies; @@ -30,6 +31,25 @@ export class MKCOLRequestHandler implements WebDavMethodHandler { driveFolderService, })) as DriveFolderItem; + var folderAlreadyExists = true; + // try to get the folder from the drive before creating it + // The method getFolderMetadataByPath will throw an error if the folder does not exist, so we need to catch it + try { + await driveFolderService.getFolderMetadataByPath(resource.url); + } + // In this case a 404 error must spefically catched + catch (error: any) { + // check if the error is a 404 error + if (error.status === 404) { + folderAlreadyExists = false; + } + } + + if (folderAlreadyExists) { + webdavLogger.info(`[MKCOL] ❌ Folder already exists`); + throw new MethodNotAllowed('Folder already exists'); + } + const [createFolder] = driveFolderService.createFolder({ plainName: resource.path.base, parentFolderUuid: parentFolderItem.uuid, diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index 214b9c55..ca85bede 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -154,6 +154,6 @@ export class PUTRequestHandler implements WebDavMethodHandler { await driveDatabaseManager.createFile(file, resource.path.dir + '/'); - res.status(200).send(); + res.status(201).send(); }; } diff --git a/src/webdav/middewares/mkcol.middleware.ts b/src/webdav/middewares/mkcol.middleware.ts new file mode 100644 index 00000000..10342b5a --- /dev/null +++ b/src/webdav/middewares/mkcol.middleware.ts @@ -0,0 +1,24 @@ +import { RequestHandler } from 'express'; + +export const MkcolMiddleware: RequestHandler = (req, _, next) => { + // if request content types are not 'application/xml', 'text/xml' or not defined, return 415 + if (req.method === 'MKCOL') { + if (req.get('Content-Type')) { + if (!req.is('application/xml') && !req.is('text/xml')) { + return next({ + status: 415, + message: 'Unsupported Media Type', + }); + } + } + // body must be empty + if (req.body && Object.keys(req.body).length > 0) { + return next({ + status: 415, + message: 'Unsupported Media Type', + }); + } + } + + next(); +}; diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index 7062f70c..1cbaee3e 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -29,6 +29,7 @@ import { MOVERequestHandler } from './handlers/MOVE.handler'; import { COPYRequestHandler } from './handlers/COPY.handler'; import { TrashService } from '../services/drive/trash.service'; import { Environment } from '@internxt/inxt-js'; +import { MkcolMiddleware } from './middewares/mkcol.middleware'; export class WebDavServer { constructor( @@ -70,6 +71,7 @@ export class WebDavServer { private readonly registerMiddlewares = async () => { this.app.use(bodyParser.text({ type: ['application/xml', 'text/xml'] })); this.app.use(ErrorHandlingMiddleware); + this.app.use(MkcolMiddleware); this.app.use(AuthMiddleware(AuthService.instance)); this.app.use( RequestLoggerMiddleware({ diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index 41b81276..bacb7e9d 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -138,7 +138,7 @@ describe('PUT request handler', () => { const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture); await sut.handle(request, response); - expect(response.status).toHaveBeenCalledWith(200); + expect(response.status).toHaveBeenCalledWith(201); expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); expect(getAuthDetailsStub).toHaveBeenCalledOnce(); @@ -206,7 +206,7 @@ describe('PUT request handler', () => { const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture); await sut.handle(request, response); - expect(response.status).toHaveBeenCalledWith(200); + expect(response.status).toHaveBeenCalledWith(201); expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); expect(getAuthDetailsStub).toHaveBeenCalledOnce();