From 0a9ac12e9d0ecf8b2c7e5bf7a642f7ae4681d562 Mon Sep 17 00:00:00 2001 From: Cesare Date: Sun, 26 Jul 2026 10:45:55 +0000 Subject: [PATCH] Optimize WebDAV replacement writes --- src/services/drive/drive-file.service.ts | 26 +++++++++++++++++++ src/webdav/handlers/PUT.handler.ts | 32 ++++++++++++++++++------ test/webdav/handlers/PUT.handler.test.ts | 14 +++++++++-- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/services/drive/drive-file.service.ts b/src/services/drive/drive-file.service.ts index 3d33aed8..58f69d00 100644 --- a/src/services/drive/drive-file.service.ts +++ b/src/services/drive/drive-file.service.ts @@ -31,6 +31,32 @@ export class DriveFileService { return driveFileItem; }; + public replaceFile = async (uuid: string, payload: StorageTypes.FileEntryByUuid): Promise => { + const storageClient = SdkManager.instance.getStorage(); + const driveFile = await storageClient.replaceFile(uuid, { + fileId: payload.fileId ?? '', + size: payload.size, + }); + + const driveFileItem: DriveFileItem = { + itemType: 'file', + name: driveFile.plainName ?? driveFile.name, + uuid: driveFile.uuid, + size: driveFile.size, + bucket: driveFile.bucket, + createdAt: new Date(driveFile.createdAt), + updatedAt: new Date(driveFile.updatedAt), + fileId: driveFile.fileId ?? null, + type: driveFile.type ?? null, + status: driveFile.status as DriveFileItem['status'], + folderUuid: driveFile.folderUuid, + creationTime: new Date(driveFile.creationTime ?? driveFile.createdAt), + modificationTime: new Date(driveFile.modificationTime ?? driveFile.updatedAt), + }; + + return driveFileItem; + }; + private readonly createDriveFileEntry = async ( payload: StorageTypes.FileEntryByUuid, ): Promise => { diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index c8899801..a7d61ac8 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -52,12 +52,6 @@ export class PUTRequestHandler implements WebDavMethodHandler { webdavLogger.info( `[PUT] File '${resource.name}' already exists in '${resource.path.dir}', it will be replaced...`, ); - try { - await WebDavUtils.deleteOrTrashItem(driveFileItem); - await DriveItemRepository.instance.delete([driveFileItem.uuid]); - } catch { - //noop - } } const { user } = await AuthService.instance.getAuthDetails(); @@ -104,7 +98,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { } const driveTimer = CLIUtils.timer(); - const file = await DriveFileService.instance.createFile({ + const filePayload = { plainName: resource.path.name, type: fileType, size: contentLength, @@ -112,7 +106,29 @@ export class PUTRequestHandler implements WebDavMethodHandler { fileId, bucket, encryptVersion: EncryptionVersion.Aes03, - }); + }; + + let file; + if (driveFileItem?.itemType === 'file' && contentLength > 0) { + try { + file = await DriveFileService.instance.replaceFile(driveFileItem.uuid, filePayload); + } catch (error) { + webdavLogger.warn( + `[PUT] File replace failed for '${resource.url}', falling back to delete and create: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + await WebDavUtils.deleteOrTrashItem(driveFileItem); + await DriveItemRepository.instance.delete([driveFileItem.uuid]); + file = await DriveFileService.instance.createFile(filePayload); + } + } else { + if (driveFileItem?.itemType === 'file') { + await WebDavUtils.deleteOrTrashItem(driveFileItem); + await DriveItemRepository.instance.delete([driveFileItem.uuid]); + } + file = await DriveFileService.instance.createFile(filePayload); + } timings.driveUpload = driveTimer.stop(); await DriveItemRepository.instance.createOrUpdate([ diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index ee20c94f..a9363464 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -168,6 +168,7 @@ describe('PUT request handler', () => { .mockResolvedValue(UserCredentialsFixture); const uploadStub = vi.spyOn(networkFacade, 'uploadFile').mockResolvedValue('uploaded-file-id'); const createDriveFileStub = vi.spyOn(DriveFileService.instance, 'createFile').mockResolvedValue(fileFixture); + const replaceDriveFileStub = vi.spyOn(DriveFileService.instance, 'replaceFile').mockResolvedValue(fileFixture); await sut.handle(request, response); expect(response.status).toHaveBeenCalledWith(204); @@ -176,7 +177,16 @@ describe('PUT request handler', () => { expect(getDriveFolderFromResourceStub).toHaveBeenCalledOnce(); expect(getAuthDetailsStub).toHaveBeenCalledOnce(); expect(uploadStub).toHaveBeenCalledOnce(); - expect(createDriveFileStub).toHaveBeenCalledOnce(); - expect(deleteDriveFileStub).toHaveBeenCalledOnce(); + expect(replaceDriveFileStub).toHaveBeenCalledWith( + fileFixture.uuid, + expect.objectContaining({ + fileId: 'uploaded-file-id', + folderUuid: folderFixture.uuid, + plainName: requestedFileResource.path.name, + size: 100, + }), + ); + expect(createDriveFileStub).not.toHaveBeenCalled(); + expect(deleteDriveFileStub).not.toHaveBeenCalled(); }); });