From 2a03e657d812a7cf5506c7e0d0e0608401d0343b Mon Sep 17 00:00:00 2001 From: larryrider Date: Fri, 31 Jul 2026 13:27:28 +0200 Subject: [PATCH 1/2] feat: add method to find existing files in a folder --- src/services/drive/drive-file.service.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/services/drive/drive-file.service.ts b/src/services/drive/drive-file.service.ts index 58f69d00..1459086c 100644 --- a/src/services/drive/drive-file.service.ts +++ b/src/services/drive/drive-file.service.ts @@ -57,6 +57,36 @@ export class DriveFileService { return driveFileItem; }; + public findExistentFile = async ( + folderUuid: string, + file: StorageTypes.FileStructure, + ): Promise => { + const storageClient = SdkManager.instance.getStorage(); + const { existentFiles } = await storageClient.checkDuplicatedFiles({ + folderUuid, + filesList: [file], + }); + + const existentFile = existentFiles[0]; + if (!existentFile) return undefined; + + return { + itemType: 'file', + name: existentFile.plainName ?? existentFile.name, + uuid: existentFile.uuid, + size: existentFile.size, + bucket: existentFile.bucket, + createdAt: new Date(existentFile.createdAt), + updatedAt: new Date(existentFile.updatedAt), + fileId: existentFile.fileId ?? null, + type: existentFile.type ?? null, + status: existentFile.status as DriveFileItem['status'], + folderUuid: existentFile.folderUuid, + creationTime: new Date(existentFile.creationTime ?? existentFile.createdAt), + modificationTime: new Date(existentFile.modificationTime ?? existentFile.updatedAt), + }; + }; + private readonly createDriveFileEntry = async ( payload: StorageTypes.FileEntryByUuid, ): Promise => { From 39b25622540fb6d6a485bfe2524bd73dace96243 Mon Sep 17 00:00:00 2001 From: larryrider Date: Fri, 31 Jul 2026 13:33:38 +0200 Subject: [PATCH 2/2] feat: add overwrite flag to upload-file command for replacing existing files --- README.md | 8 +- src/commands/upload-file.ts | 23 +++++- test/commands/upload-file.test.ts | 122 ++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 test/commands/upload-file.test.ts diff --git a/README.md b/README.md index 964caf6b..d2644fec 100644 --- a/README.md +++ b/README.md @@ -1284,11 +1284,13 @@ Upload a file to Internxt Drive ``` USAGE - $ internxt upload-file [--json] [-x] [--debug] [-f ] [-i ] + $ internxt upload-file [--json] [-x] [--debug] [-f ] [-i ] [-o] FLAGS -f, --file= The path to the file on your system. -i, --destination= The folder id where the file is going to be uploaded to. Leave empty for the root folder. + -o, --overwrite Overwrite the file if a file with the same name already exists in the cloud destination + folder. HELPER FLAGS -x, --non-interactive [env: INXT_NONINTERACTIVE] Prevents the CLI from being interactive. When enabled, the CLI will @@ -1350,11 +1352,13 @@ Upload a file to Internxt Drive ``` USAGE - $ internxt upload file [--json] [-x] [--debug] [-f ] [-i ] + $ internxt upload file [--json] [-x] [--debug] [-f ] [-i ] [-o] FLAGS -f, --file= The path to the file on your system. -i, --destination= The folder id where the file is going to be uploaded to. Leave empty for the root folder. + -o, --overwrite Overwrite the file if a file with the same name already exists in the cloud destination + folder. HELPER FLAGS -x, --non-interactive [env: INXT_NONINTERACTIVE] Prevents the CLI from being interactive. When enabled, the CLI will diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index eb7ff520..50f24821 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -30,6 +30,12 @@ export default class UploadFile extends Command { required: false, parse: CLIUtils.parseEmpty, }), + overwrite: Flags.boolean({ + char: 'o', + description: 'Overwrite the file if a file with the same name already exists in the cloud destination folder.', + required: false, + default: false, + }), }; static readonly enableJsonFlag = true; @@ -58,6 +64,13 @@ export default class UploadFile extends Command { }); const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(destinationFolderUuidFromFlag); + const existingFile = flags['overwrite'] + ? await DriveFileService.instance.findExistentFile(destinationFolderUuid, { + plainName: fileInfo.name, + type: fileType, + }) + : undefined; + const timings = { networkUpload: 0, driveUpload: 0, @@ -109,7 +122,7 @@ export default class UploadFile extends Command { // Create the file in Drive const driveUploadTimer = CLIUtils.timer(); - const createdDriveFile = await DriveFileService.instance.createFile({ + const filePayload = { plainName: fileInfo.name, type: fileType, size: fileSize, @@ -119,7 +132,10 @@ export default class UploadFile extends Command { encryptVersion: EncryptionVersion.Aes03, creationTime: stats.birthtime?.toISOString(), modificationTime: stats.mtime?.toISOString(), - }); + }; + const createdDriveFile = existingFile + ? await DriveFileService.instance.replaceFile(existingFile.uuid, filePayload) + : await DriveFileService.instance.createFile(filePayload); timings.driveUpload = driveUploadTimer.stop(); const thumbnailTimer = CLIUtils.timer(); @@ -144,8 +160,9 @@ export default class UploadFile extends Command { const workspace = await AuthService.instance.getCurrentWorkspace(); const workspaceId = workspace?.workspaceData.workspace.id; + const uploadVerb = existingFile ? 'overwritten' : 'uploaded'; const message = - `File uploaded successfully in ${CLIUtils.formatDuration(totalTime)}, view it at ` + + `File ${uploadVerb} successfully in ${CLIUtils.formatDuration(totalTime)}, view it at ` + `${ConfigService.instance.get('DRIVE_WEB_URL')}/file/${createdDriveFile.uuid}` + `${workspaceId ? `?workspaceid=${workspaceId}` : ''}`; CLIUtils.success(reporter, message); diff --git a/test/commands/upload-file.test.ts b/test/commands/upload-file.test.ts new file mode 100644 index 00000000..be48a17f --- /dev/null +++ b/test/commands/upload-file.test.ts @@ -0,0 +1,122 @@ +import { beforeEach, describe, expect, test, MockInstance, vi } from 'vitest'; +import { stat } from 'fs/promises'; +import { createReadStream } from 'fs'; +import UploadFile from '../../src/commands/upload-file'; +import { LoginCredentials } from '../../src/types/command.types'; +import { ValidationService } from '../../src/services/validation.service'; +import { UserFixture } from '../fixtures/auth.fixture'; +import { newFileItem } from '../fixtures/drive.fixture'; +import { CLIUtils } from '../../src/utils/cli.utils'; +import { ConfigService } from '../../src/services/config.service'; +import { DriveFileService } from '../../src/services/drive/drive-file.service'; +import { ThumbnailService } from '../../src/services/thumbnail.service'; +import { AuthService } from '../../src/services/auth.service'; +import { NetworkFacade } from '../../src/services/network/network-facade.service'; +import { createMockStats, createMockReadStream } from '../services/network/upload/upload.service.helpers'; + +vi.mock('fs', () => ({ + createReadStream: vi.fn(), +})); + +vi.mock('fs/promises', () => ({ + stat: vi.fn(), +})); + +describe('Upload File Command', () => { + const destinationFolderUuid = 'dest-folder-uuid'; + const bucket = 'test-bucket'; + const mockNetworkFacade = { + uploadFile: vi.fn().mockResolvedValue('mock-network-file-id'), + } as unknown as NetworkFacade; + + let configReadUserSpy: MockInstance<() => Promise>; + let validateFileExistsSpy: MockInstance<(path: string) => Promise>; + let getDestinationFolderUuidSpy: MockInstance<() => Promise>; + let findExistentFileSpy: MockInstance; + let createFileSpy: MockInstance; + let replaceFileSpy: MockInstance; + let cliSuccessSpy: MockInstance<() => void>; + + const createdFile = newFileItem({ name: 'report' }); + + beforeEach(() => { + configReadUserSpy = vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue({ + user: UserFixture, + token: 'mock-token', + }); + validateFileExistsSpy = vi.spyOn(ValidationService.instance, 'validateFileExists').mockResolvedValue(true); + getDestinationFolderUuidSpy = vi + .spyOn(CLIUtils, 'getDestinationFolderUuid') + .mockResolvedValue(destinationFolderUuid); + vi.spyOn(CLIUtils, 'prepareNetwork').mockResolvedValue({ networkFacade: mockNetworkFacade, bucket, mnemonic: '' }); + vi.mocked(stat).mockResolvedValue(createMockStats(1024) as Awaited>); + vi.mocked(createReadStream).mockReturnValue(createMockReadStream() as ReturnType); + findExistentFileSpy = vi.spyOn(DriveFileService.instance, 'findExistentFile').mockResolvedValue(undefined); + createFileSpy = vi.spyOn(DriveFileService.instance, 'createFile').mockResolvedValue(createdFile); + replaceFileSpy = vi.spyOn(DriveFileService.instance, 'replaceFile').mockResolvedValue(createdFile); + vi.spyOn(ThumbnailService.instance, 'tryUploadThumbnail').mockResolvedValue(undefined); + vi.spyOn(AuthService.instance, 'getCurrentWorkspace').mockResolvedValue(undefined); + cliSuccessSpy = vi.spyOn(CLIUtils, 'success').mockImplementation(() => {}); + }); + + test('when the overwrite flag is not set, then the file is uploaded as a new file without checking for duplicates', async () => { + await UploadFile.run(['--file=/path/to/report.txt']); + + expect(configReadUserSpy).toHaveBeenCalledOnce(); + expect(validateFileExistsSpy).toHaveBeenCalledWith('/path/to/report.txt'); + expect(getDestinationFolderUuidSpy).toHaveBeenCalledOnce(); + expect(findExistentFileSpy).not.toHaveBeenCalled(); + expect(createFileSpy).toHaveBeenCalledWith( + expect.objectContaining({ + plainName: 'report', + type: 'txt', + folderUuid: destinationFolderUuid, + }), + ); + expect(replaceFileSpy).not.toHaveBeenCalled(); + expect(cliSuccessSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.stringContaining('File uploaded successfully'), + ); + }); + + test('when the overwrite flag is set but no file with the same name exists, then a new file is created', async () => { + await UploadFile.run(['--file=/path/to/report.txt', '--overwrite']); + + expect(findExistentFileSpy).toHaveBeenCalledWith(destinationFolderUuid, { + plainName: 'report', + type: 'txt', + }); + expect(createFileSpy).toHaveBeenCalledOnce(); + expect(replaceFileSpy).not.toHaveBeenCalled(); + expect(cliSuccessSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.stringContaining('File uploaded successfully'), + ); + }); + + test('when the overwrite flag is set and a file with the same name exists, then the existing file is replaced', async () => { + const existingFile = newFileItem({ name: 'report', uuid: 'existing-file-uuid' }); + findExistentFileSpy.mockResolvedValue(existingFile); + + await UploadFile.run(['--file=/path/to/report.txt', '--overwrite']); + + expect(findExistentFileSpy).toHaveBeenCalledWith(destinationFolderUuid, { + plainName: 'report', + type: 'txt', + }); + expect(replaceFileSpy).toHaveBeenCalledWith( + 'existing-file-uuid', + expect.objectContaining({ + plainName: 'report', + type: 'txt', + folderUuid: destinationFolderUuid, + }), + ); + expect(createFileSpy).not.toHaveBeenCalled(); + expect(cliSuccessSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.stringContaining('File overwritten successfully'), + ); + }); +});