From 9c7aa5a292743bddd4c13b22e0ad61ed0910ab57 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Fri, 19 Dec 2025 12:56:40 +0100 Subject: [PATCH 1/6] feat: enable empty file upload support --- src/commands/upload-file.ts | 106 +++++++++--------- .../network/upload/upload-file.service.ts | 64 ++++++----- src/utils/thumbnail.utils.ts | 4 +- src/webdav/handlers/PUT.handler.ts | 97 ++++++++-------- .../upload/upload-file.service.test.ts | 44 +++++--- test/webdav/handlers/PUT.handler.test.ts | 57 +++++++--- 6 files changed, 206 insertions(+), 166 deletions(-) diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index 1ed0756..9bf9e10 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -6,13 +6,11 @@ import { CLIUtils } from '../utils/cli.utils'; import { ConfigService } from '../services/config.service'; import path from 'node:path'; import { DriveFileService } from '../services/drive/drive-file.service'; -import { ErrorUtils } from '../utils/errors.utils'; import { NotValidDirectoryError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; -import { ThumbnailService } from '../services/thumbnail.service'; import { BufferStream } from '../utils/stream.utils'; -import { isFileThumbnailable } from '../utils/thumbnail.utils'; +import { isFileThumbnailable, tryUploadThumbnail } from '../utils/thumbnail.utils'; import { Readable } from 'node:stream'; export default class UploadFile extends Command { @@ -46,9 +44,6 @@ export default class UploadFile extends Command { const filePath = await this.getFilePath(flags['file'], nonInteractive); const stats = await stat(filePath); - if (!stats.size) { - throw new Error('The file is empty. Uploading empty files is not allowed.'); - } const fileInfo = path.parse(filePath); const fileType = fileInfo.ext.replaceAll('.', ''); @@ -62,11 +57,9 @@ export default class UploadFile extends Command { reporter: this.log.bind(this), })) ?? user.rootFolderId; - // 1. Prepare the network - const networkFacade = await CLIUtils.prepareNetwork({ loginUserDetails: user, jsonFlag: flags['json'] }); + // Prepare the network + const networkFacade = CLIUtils.prepareNetwork({ loginUserDetails: user, jsonFlag: flags['json'] }); - // 2. Upload file to the Network - const readStream = createReadStream(filePath); const timer = CLIUtils.timer(); const progressBar = CLIUtils.progress( { @@ -77,42 +70,50 @@ export default class UploadFile extends Command { ); progressBar?.start(100, 0); + let fileId: string | undefined; let bufferStream: BufferStream | undefined; - let fileStream: Readable = readStream; const isThumbnailable = isFileThumbnailable(fileType); - if (isThumbnailable) { - bufferStream = new BufferStream(); - fileStream = readStream.pipe(bufferStream); - } + const fileSize = stats.size ?? 0; - const progressCallback = (progress: number) => { - progressBar?.update(progress * 100 * 0.99); - }; + if (fileSize > 0) { + // Upload file to the Network + const readStream = createReadStream(filePath); + let fileStream: Readable = readStream; - const fileId = await new Promise((resolve: (fileId: string) => void, reject) => { - const state = networkFacade.uploadFile( - fileStream, - stats.size, - user.bucket, - (err: Error | null, res: string | null) => { - if (err) { - return reject(err); - } - resolve(res as string); - }, - progressCallback, - ); - process.on('SIGINT', () => { - state.stop(); - process.exit(1); + if (isThumbnailable) { + bufferStream = new BufferStream(); + fileStream = readStream.pipe(bufferStream); + } + + const progressCallback = (progress: number) => { + progressBar?.update(progress * 100 * 0.99); + }; + + fileId = await new Promise((resolve: (fileId: string) => void, reject) => { + const state = networkFacade.uploadFile( + fileStream, + fileSize, + user.bucket, + (err: Error | null, res: string | null) => { + if (err) { + return reject(err); + } + resolve(res as string); + }, + progressCallback, + ); + process.on('SIGINT', () => { + state.stop(); + process.exit(1); + }); }); - }); + } - // 3. Create the file in Drive + // Create the file in Drive const createdDriveFile = await DriveFileService.instance.createFile({ plainName: fileInfo.name, type: fileType, - size: stats.size, + size: fileSize, folderUuid: destinationFolderUuid, fileId: fileId, bucket: user.bucket, @@ -121,22 +122,14 @@ export default class UploadFile extends Command { modificationTime: stats.mtime?.toISOString(), }); - try { - if (isThumbnailable && bufferStream) { - const thumbnailBuffer = bufferStream.getBuffer(); - - if (thumbnailBuffer) { - await ThumbnailService.instance.uploadThumbnail( - thumbnailBuffer, - fileType, - user.bucket, - createdDriveFile.uuid, - networkFacade, - ); - } - } - } catch (error) { - ErrorUtils.report(error, { command: this.id }); + if (fileSize > 0 && isThumbnailable && bufferStream) { + await tryUploadThumbnail({ + bufferStream, + fileType, + userBucket: user.bucket, + fileUuid: createdDriveFile.uuid, + networkFacade, + }); } progressBar?.update(100); @@ -144,15 +137,16 @@ export default class UploadFile extends Command { const uploadTime = timer.stop(); this.log('\n'); - // eslint-disable-next-line max-len - const message = `File uploaded in ${uploadTime}ms, view it at ${ConfigService.instance.get('DRIVE_WEB_URL')}/file/${createdDriveFile.uuid}`; + const message = + `File uploaded in ${uploadTime}ms, view it at ` + + `${ConfigService.instance.get('DRIVE_WEB_URL')}/file/${createdDriveFile.uuid}`; CLIUtils.success(this.log.bind(this), message); return { success: true, message, file: { ...createdDriveFile, - plainName: fileInfo.name, + plainName: createdDriveFile.name, }, }; }; diff --git a/src/services/network/upload/upload-file.service.ts b/src/services/network/upload/upload-file.service.ts index b0e8acc..375f558 100644 --- a/src/services/network/upload/upload-file.service.ts +++ b/src/services/network/upload/upload-file.service.ts @@ -12,6 +12,8 @@ import { isAlreadyExistsError } from '../../../utils/errors.utils'; import { stat } from 'node:fs/promises'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { createFileStreamWithBuffer, tryUploadThumbnail } from '../../../utils/thumbnail.utils'; +import { BufferStream } from '../../../utils/stream.utils'; +import { DriveFileItem } from '../../../types/drive.types'; export class UploadFileService { static readonly instance = new UploadFileService(); @@ -63,40 +65,45 @@ export class UploadFileService { network, bucket, parentFolderUuid, - }: UploadFileWithRetryParams): Promise { + }: UploadFileWithRetryParams): Promise { for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { try { const stats = await stat(file.absolutePath); - if (!stats.size) { - logger.warn(`Skipping empty file: ${file.relativePath}`); - return null; - } + const fileSize = stats.size ?? 0; const fileType = extname(file.absolutePath).replaceAll('.', ''); - const { fileStream, bufferStream } = createFileStreamWithBuffer({ - path: file.absolutePath, - fileType, - }); - const fileId = await new Promise((resolve, reject) => { - network.uploadFile( - fileStream, - stats.size, - bucket, - (err: Error | null, res: string | null) => { - if (err) { - return reject(err); - } - resolve(res as string); - }, - () => {}, - ); - }); + let fileId: string | undefined; + let thumbnailStream: BufferStream | undefined; + + if (fileSize > 0) { + const { fileStream, bufferStream } = createFileStreamWithBuffer({ + path: file.absolutePath, + fileType, + }); + + thumbnailStream = bufferStream; + + fileId = await new Promise((resolve, reject) => { + network.uploadFile( + fileStream, + fileSize, + bucket, + (err: Error | null, res: string | null) => { + if (err) { + return reject(err); + } + resolve(res as string); + }, + () => {}, + ); + }); + } const createdDriveFile = await DriveFileService.instance.createFile({ plainName: file.name, type: fileType, - size: stats.size, + size: fileSize, folderUuid: parentFolderUuid, fileId, bucket, @@ -105,9 +112,9 @@ export class UploadFileService { modificationTime: stats.mtime?.toISOString(), }); - if (bufferStream) { - void tryUploadThumbnail({ - bufferStream, + if (thumbnailStream && fileSize > 0) { + await tryUploadThumbnail({ + bufferStream: thumbnailStream, fileType, userBucket: bucket, fileUuid: createdDriveFile.uuid, @@ -115,7 +122,7 @@ export class UploadFileService { }); } - return createdDriveFile.fileId; + return createdDriveFile; } catch (error: unknown) { if (isAlreadyExistsError(error)) { const msg = `File ${file.name} already exists, skipping...`; @@ -136,6 +143,7 @@ export class UploadFileService { } return null; } + private chunkArray(array: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { diff --git a/src/utils/thumbnail.utils.ts b/src/utils/thumbnail.utils.ts index 1ae7529..33c80e2 100644 --- a/src/utils/thumbnail.utils.ts +++ b/src/utils/thumbnail.utils.ts @@ -56,14 +56,14 @@ export const tryUploadThumbnail = async ({ fileUuid, networkFacade, }: { - bufferStream: BufferStream; + bufferStream?: BufferStream; fileType: string; userBucket: string; fileUuid: string; networkFacade: NetworkFacade; }) => { try { - const thumbnailBuffer = bufferStream.getBuffer(); + const thumbnailBuffer = bufferStream?.getBuffer(); if (thumbnailBuffer) { await ThumbnailService.instance.uploadThumbnail(thumbnailBuffer, fileType, userBucket, fileUuid, networkFacade); } diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index e685845..669d4b8 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -3,7 +3,7 @@ import { DriveFileService } from '../../services/drive/drive-file.service'; import { NetworkFacade } from '../../services/network/network-facade.service'; import { AuthService } from '../../services/auth.service'; import { WebDavMethodHandler } from '../../types/webdav.types'; -import { NotFoundError, UnsupportedMediaTypeError } from '../../utils/errors.utils'; +import { NotFoundError } from '../../utils/errors.utils'; import { WebDavUtils } from '../../utils/webdav.utils'; import { webdavLogger } from '../../utils/logger.utils'; import { TrashService } from '../../services/drive/trash.service'; @@ -11,8 +11,7 @@ import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { CLIUtils } from '../../utils/cli.utils'; import { BufferStream } from '../../utils/stream.utils'; import { Readable } from 'node:stream'; -import { isFileThumbnailable } from '../../utils/thumbnail.utils'; -import { ThumbnailService } from '../../services/thumbnail.service'; +import { isFileThumbnailable, tryUploadThumbnail } from '../../utils/thumbnail.utils'; import { WebDavFolderService } from '../services/webdav-folder.service'; import { AsyncUtils } from '../../utils/async.utils'; import { DriveFolderService } from '../../services/drive/drive-folder.service'; @@ -30,9 +29,9 @@ export class PUTRequestHandler implements WebDavMethodHandler { ) {} handle = async (req: Request, res: Response) => { - const contentLength = Number(req.headers['content-length']); + let contentLength = Number(req.headers['content-length']); if (!contentLength || isNaN(contentLength) || contentLength <= 0) { - throw new UnsupportedMediaTypeError('Empty files are not supported'); + contentLength = 0; } const resource = await WebDavUtils.getRequestedResource(req.url); @@ -79,40 +78,44 @@ export class PUTRequestHandler implements WebDavMethodHandler { fileStream = req.pipe(bufferStream); } - let uploaded = false, - aborted = false; + let fileId: string | undefined; - const progressCallback = (progress: number) => { - if (!uploaded && !aborted) { - webdavLogger.info(`[PUT] Upload progress for file ${resource.name}: ${(progress * 100).toFixed(2)}%`); - } - }; - - const fileId = await new Promise((resolve: (fileId: string) => void, reject) => { - const state = this.dependencies.networkFacade.uploadFile( - fileStream, - contentLength, - user.bucket, - (err: Error | null, res: string | null) => { - if (err) { - aborted = true; - return reject(err); - } - resolve(res as string); - }, - progressCallback, - ); - res.on('close', async () => { - aborted = true; - if (!uploaded) { - webdavLogger.info('[PUT] ❌ HTTP Client has been disconnected, res has been closed.'); - state.stop(); + if (contentLength > 0) { + let uploaded = false, + aborted = false; + + const progressCallback = (progress: number) => { + if (!uploaded && !aborted) { + webdavLogger.info(`[PUT] Upload progress for file ${resource.name}: ${(progress * 100).toFixed(2)}%`); } + }; + + fileId = await new Promise((resolve: (fileId: string) => void, reject) => { + const state = this.dependencies.networkFacade.uploadFile( + fileStream, + contentLength, + user.bucket, + (err: Error | null, res: string | null) => { + if (err) { + aborted = true; + return reject(err); + } + resolve(res as string); + }, + progressCallback, + ); + res.on('close', async () => { + aborted = true; + if (!uploaded) { + webdavLogger.info('[PUT] ❌ HTTP Client has been disconnected, res has been closed.'); + state.stop(); + } + }); }); - }); - uploaded = true; + uploaded = true; - webdavLogger.info('[PUT] ✅ File uploaded to network'); + webdavLogger.info('[PUT] ✅ File uploaded to network'); + } const file = await DriveFileService.instance.createFile({ plainName: resource.path.name, @@ -124,22 +127,14 @@ export class PUTRequestHandler implements WebDavMethodHandler { encryptVersion: EncryptionVersion.Aes03, }); - try { - if (isThumbnailable && bufferStream) { - const thumbnailBuffer = bufferStream.getBuffer(); - - if (thumbnailBuffer) { - await ThumbnailService.instance.uploadThumbnail( - thumbnailBuffer, - fileType, - user.bucket, - file.uuid, - this.dependencies.networkFacade, - ); - } - } - } catch (error) { - webdavLogger.info(`[PUT] ❌ File thumbnail upload failed ${(error as Error).message}`); + if (contentLength > 0 && isThumbnailable && bufferStream) { + await tryUploadThumbnail({ + bufferStream, + fileType, + userBucket: user.bucket, + fileUuid: file.uuid, + networkFacade: this.dependencies.networkFacade, + }); } const uploadTime = timer.stop(); diff --git a/test/services/network/upload/upload-file.service.test.ts b/test/services/network/upload/upload-file.service.test.ts index 6cf58e3..c44237e 100644 --- a/test/services/network/upload/upload-file.service.test.ts +++ b/test/services/network/upload/upload-file.service.test.ts @@ -17,6 +17,7 @@ import { createMockStats, createProgressFixtures, } from './upload.service.helpers'; +import { newFileItem } from '../../../fixtures/drive.fixture'; vi.mock('fs', () => ({ createReadStream: vi.fn(), @@ -64,6 +65,7 @@ vi.mock('../../../../src/utils/errors.utils', async (importOriginal) => { describe('UploadFileService', () => { let sut: UploadFileService; + const mockFile = newFileItem(); const mockNetworkFacade = { uploadFile: vi.fn((_stream, _size, _bucket, callback) => { @@ -84,10 +86,7 @@ describe('UploadFileService', () => { bufferStream: undefined, }); vi.mocked(tryUploadThumbnail).mockResolvedValue(undefined); - vi.mocked(DriveFileService.instance.createFile).mockResolvedValue({ - uuid: 'mock-file-uuid', - fileId: 'mock-file-id', - } as Awaited>); + vi.mocked(DriveFileService.instance.createFile).mockResolvedValue(mockFile); }); describe('uploadFilesInChunks', () => { @@ -103,7 +102,7 @@ describe('UploadFileService', () => { ]; const { currentProgress, emitProgress } = createProgressFixtures(); - const uploadFileWithRetrySpy = vi.spyOn(sut, 'uploadFileWithRetry').mockResolvedValue('mock-file-id'); + const uploadFileWithRetrySpy = vi.spyOn(sut, 'uploadFileWithRetry').mockResolvedValue(mockFile); const result = await sut.uploadFilesInChunks({ network: mockNetworkFacade, @@ -131,7 +130,7 @@ describe('UploadFileService', () => { ); const { currentProgress, emitProgress } = createProgressFixtures(); - const uploadFileWithRetrySpy = vi.spyOn(sut, 'uploadFileWithRetry').mockResolvedValue('mock-file-id'); + const uploadFileWithRetrySpy = vi.spyOn(sut, 'uploadFileWithRetry').mockResolvedValue(mockFile); await sut.uploadFilesInChunks({ network: mockNetworkFacade, @@ -154,7 +153,7 @@ describe('UploadFileService', () => { ]; const { currentProgress, emitProgress } = createProgressFixtures(); - const uploadFileWithRetrySpy = vi.spyOn(sut, 'uploadFileWithRetry').mockResolvedValue('mock-file-id'); + const uploadFileWithRetrySpy = vi.spyOn(sut, 'uploadFileWithRetry').mockResolvedValue(mockFile); await sut.uploadFilesInChunks({ network: mockNetworkFacade, @@ -204,11 +203,12 @@ describe('UploadFileService', () => { uploadFileWithRetrySpy.mockRestore(); }); }); + describe('uploadFileWithRetry', () => { const bucket = 'test-bucket'; const destinationFolderUuid = 'dest-uuid'; - it('should properly create a file and return the created file id', async () => { + it('should properly create a file and return the created file uuid', async () => { const file = createFileSystemNodeFixture({ type: 'file', name: 'test', @@ -224,7 +224,7 @@ describe('UploadFileService', () => { parentFolderUuid: destinationFolderUuid, }); - expect(result).toBe('mock-file-id'); + expect(result).toBe(mockFile); expect(stat).toHaveBeenCalledWith(file.absolutePath); expect(mockNetworkFacade.uploadFile).toHaveBeenCalledWith( expect.anything(), @@ -266,7 +266,7 @@ describe('UploadFileService', () => { return { stop: vi.fn() } as unknown as ReturnType; }) .mockImplementationOnce((_stream, _size, _bucket, callback) => { - callback(null, 'success-file-id'); + callback(null, 'success-file-uuid'); return { stop: vi.fn() } as unknown as ReturnType; }); @@ -281,19 +281,20 @@ describe('UploadFileService', () => { const result = await resultPromise; - expect(result).toBe('mock-file-id'); + expect(result).toBe(mockFile); expect(mockNetworkFacade.uploadFile).toHaveBeenCalledTimes(3); expect(logger.warn).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); - it('should skip empty files and return null', async () => { + it('should upload empty files and return its uuid', async () => { const file = createFileSystemNodeFixture({ type: 'file', - name: 'empty.txt', + name: 'empty', relativePath: 'empty.txt', size: 0, + absolutePath: '/path/to/empty.txt', }); vi.mocked(stat).mockResolvedValue(createMockStats(0) as Awaited>); @@ -305,9 +306,20 @@ describe('UploadFileService', () => { parentFolderUuid: destinationFolderUuid, }); - expect(result).toBeNull(); - expect(logger.warn).toHaveBeenCalledWith('Skipping empty file: empty.txt'); + expect(result).toBe(mockFile); + expect(stat).toHaveBeenCalledWith(file.absolutePath); expect(mockNetworkFacade.uploadFile).not.toHaveBeenCalled(); + expect(DriveFileService.instance.createFile).toHaveBeenCalledWith( + expect.objectContaining({ + fileId: undefined, + plainName: 'empty', + type: 'txt', + size: 0, + folderUuid: destinationFolderUuid, + bucket, + encryptVersion: '03-aes', + }), + ); }); it('should call tryUploadThumbnail when bufferStream is present', async () => { @@ -335,7 +347,7 @@ describe('UploadFileService', () => { bufferStream: mockBufferStream, fileType: 'png', userBucket: bucket, - fileUuid: 'mock-file-uuid', + fileUuid: mockFile.uuid, networkFacade: mockNetworkFacade, }); }); diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index f6c82bb..1643924 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -10,11 +10,9 @@ import { DriveFolderService } from '../../../src/services/drive/drive-folder.ser import { CryptoService } from '../../../src/services/crypto.service'; import { DownloadService } from '../../../src/services/network/download.service'; import { AuthService } from '../../../src/services/auth.service'; -import { UnsupportedMediaTypeError } from '../../../src/utils/errors.utils'; import { SdkManager } from '../../../src/services/sdk-manager.service'; import { NetworkFacade } from '../../../src/services/network/network-facade.service'; import { PUTRequestHandler } from '../../../src/webdav/handlers/PUT.handler'; -import { fail } from 'node:assert'; import { WebDavFolderService } from '../../../src/webdav/services/webdav-folder.service'; import { TrashService } from '../../../src/services/drive/trash.service'; import { WebDavRequestedResource } from '../../../src/types/webdav.types'; @@ -67,10 +65,23 @@ describe('PUT request handler', () => { }); }); - it('When the content-length request is 0, then it should throw an UnsupportedMediaTypeError', async () => { + it('should upload an empty file when the content-length request is 0', async () => { + const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); + const requestedParentFolderResource: WebDavRequestedResource = getRequestedFolderResource({ + parentFolder: '/', + folderName: '', + }); + const folderFixture = newFolderItem({ name: requestedParentFolderResource.name }); + const fileFixture = newDriveFile({ + folderId: folderFixture.id, + folderUuid: folderFixture.uuid, + size: 0, + fileId: undefined, + }); + const request = createWebDavRequestFixture({ method: 'PUT', - url: '/file.txt', + url: requestedFileResource.url, headers: { 'content-length': '0', }, @@ -80,15 +91,35 @@ describe('PUT request handler', () => { status: vi.fn().mockReturnValue({ send: vi.fn() }), }); - try { - await sut.handle(request, response); - fail('Expected function to throw an error, but it did not.'); - } catch (error) { - expect(error).to.be.instanceOf(UnsupportedMediaTypeError); - } + const getRequestedResourceStub = vi + .spyOn(WebDavUtils, 'getRequestedResource') + .mockResolvedValueOnce(requestedFileResource) + .mockResolvedValueOnce(requestedParentFolderResource); + const getAndSearchItemFromResourceStub = vi + .spyOn(WebDavUtils, 'getDriveItemFromResource') + .mockResolvedValue(undefined); + const getDriveFolderFromResourceStub = vi + .spyOn(WebDavUtils, 'getDriveFolderFromResource') + .mockResolvedValue(folderFixture); + const getAuthDetailsStub = vi + .spyOn(AuthService.instance, 'getAuthDetails') + .mockResolvedValue(UserCredentialsFixture); + const uploadStub = vi.spyOn(networkFacade, 'uploadFile'); + const createDriveFileStub = vi + .spyOn(DriveFileService.instance, 'createFile') + .mockResolvedValue(fileFixture.toItem()); + + await sut.handle(request, response); + expect(response.status).toHaveBeenCalledWith(201); + expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); + expect(getAndSearchItemFromResourceStub).toHaveBeenCalledOnce(); + expect(getDriveFolderFromResourceStub).toHaveBeenCalledOnce(); + expect(getAuthDetailsStub).toHaveBeenCalledOnce(); + expect(uploadStub).not.toHaveBeenCalled(); + expect(createDriveFileStub).toHaveBeenCalledOnce(); }); - it('When the Drive destination folder is found, then it should upload the file to the folder', async () => { + it('should upload the file to the folder when the Drive destination folder is found', async () => { const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); const requestedParentFolderResource: WebDavRequestedResource = getRequestedFolderResource({ parentFolder: '/', @@ -142,7 +173,7 @@ describe('PUT request handler', () => { expect(createDriveFileStub).toHaveBeenCalledOnce(); }); - it('When the file already exists, then it should upload and replace the file to the folder', async () => { + it('it should upload and replace the file to the folder when the file already exists', async () => { const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); const requestedParentFolderResource: WebDavRequestedResource = getRequestedFolderResource({ parentFolder: '/', @@ -198,7 +229,7 @@ describe('PUT request handler', () => { expect(deleteDriveFileStub).toHaveBeenCalledOnce(); }); - it('When file is uploaded, then it should wait 500ms for backend propagation before returning 201', async () => { + it('should wait 500ms for backend propagation before returning 201 when a file is uploaded', async () => { const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); const requestedParentFolderResource: WebDavRequestedResource = getRequestedFolderResource({ parentFolder: '/', From ea78f0da42806c68b9726db325c3472c044cc96d Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Fri, 19 Dec 2025 15:23:05 +0100 Subject: [PATCH 2/6] chore: removed unused tests tsconfig --- test/tsconfig.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 test/tsconfig.json diff --git a/test/tsconfig.json b/test/tsconfig.json deleted file mode 100644 index 6be1c43..0000000 --- a/test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig", - "compilerOptions": { - "noEmit": true, - "rootDir": "." - }, - "include": ["**/*"], - "references": [{ "path": ".." }] -} From 800735b74291a55b77a1b0efb1fd6c11587d176a Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Fri, 19 Dec 2025 15:23:30 +0100 Subject: [PATCH 3/6] test: update upload file service tests to improve clarity in descriptions --- test/services/network/upload/upload-file.service.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/services/network/upload/upload-file.service.test.ts b/test/services/network/upload/upload-file.service.test.ts index c44237e..4014a6f 100644 --- a/test/services/network/upload/upload-file.service.test.ts +++ b/test/services/network/upload/upload-file.service.test.ts @@ -170,6 +170,7 @@ describe('UploadFileService', () => { expect(emitProgress).toHaveBeenCalledTimes(2); uploadFileWithRetrySpy.mockRestore(); }); + it('should skip files when parent folder is not found in folderMap', async () => { const bucket = 'test-bucket'; const destinationFolderUuid = 'dest-uuid'; @@ -208,7 +209,7 @@ describe('UploadFileService', () => { const bucket = 'test-bucket'; const destinationFolderUuid = 'dest-uuid'; - it('should properly create a file and return the created file uuid', async () => { + it('should properly create a file and return the created file', async () => { const file = createFileSystemNodeFixture({ type: 'file', name: 'test', @@ -288,7 +289,7 @@ describe('UploadFileService', () => { vi.useRealTimers(); }); - it('should upload empty files and return its uuid', async () => { + it('should upload empty files and return the created file', async () => { const file = createFileSystemNodeFixture({ type: 'file', name: 'empty', From bde5b29ee923e2180d8f7ab6a9663c85ff456253 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Fri, 19 Dec 2025 15:26:10 +0100 Subject: [PATCH 4/6] tests: revert calback file id value --- test/services/network/upload/upload-file.service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/services/network/upload/upload-file.service.test.ts b/test/services/network/upload/upload-file.service.test.ts index 4014a6f..1255a02 100644 --- a/test/services/network/upload/upload-file.service.test.ts +++ b/test/services/network/upload/upload-file.service.test.ts @@ -267,7 +267,7 @@ describe('UploadFileService', () => { return { stop: vi.fn() } as unknown as ReturnType; }) .mockImplementationOnce((_stream, _size, _bucket, callback) => { - callback(null, 'success-file-uuid'); + callback(null, 'success-file-id'); return { stop: vi.fn() } as unknown as ReturnType; }); From 0f5e19d47af271dc1101bc55135d779d84c3aa5e Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 23 Dec 2025 12:30:37 +0100 Subject: [PATCH 5/6] refactor: change await to void for tryUploadThumbnail calls in upload file handling --- src/commands/upload-file.ts | 2 +- src/services/network/upload/upload-file.service.ts | 2 +- src/webdav/handlers/PUT.handler.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index 9bf9e10..876cc26 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -123,7 +123,7 @@ export default class UploadFile extends Command { }); if (fileSize > 0 && isThumbnailable && bufferStream) { - await tryUploadThumbnail({ + void tryUploadThumbnail({ bufferStream, fileType, userBucket: user.bucket, diff --git a/src/services/network/upload/upload-file.service.ts b/src/services/network/upload/upload-file.service.ts index 375f558..6c3389d 100644 --- a/src/services/network/upload/upload-file.service.ts +++ b/src/services/network/upload/upload-file.service.ts @@ -113,7 +113,7 @@ export class UploadFileService { }); if (thumbnailStream && fileSize > 0) { - await tryUploadThumbnail({ + void tryUploadThumbnail({ bufferStream: thumbnailStream, fileType, userBucket: bucket, diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index 669d4b8..2c9e192 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -128,7 +128,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { }); if (contentLength > 0 && isThumbnailable && bufferStream) { - await tryUploadThumbnail({ + void tryUploadThumbnail({ bufferStream, fileType, userBucket: user.bucket, From c0e770cfc8b91b65e02a40a11e2e689f69c092e9 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 23 Dec 2025 12:31:48 +0100 Subject: [PATCH 6/6] fix: remove NotFoundError for files starting with '._' --- src/webdav/handlers/GET.handler.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webdav/handlers/GET.handler.ts b/src/webdav/handlers/GET.handler.ts index 22ab2ea..d529589 100644 --- a/src/webdav/handlers/GET.handler.ts +++ b/src/webdav/handlers/GET.handler.ts @@ -26,8 +26,6 @@ export class GETRequestHandler implements WebDavMethodHandler { const { driveFileService, authService, networkFacade } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); - if (resource.name.startsWith('._')) throw new NotFoundError('File not found'); - webdavLogger.info(`[GET] Request received item at ${resource.url}`); const driveFile = await WebDavUtils.getDriveFileFromResource({ url: resource.url,