Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1281,11 +1281,13 @@ Upload a file to Internxt Drive

```
USAGE
$ internxt upload-file [--json] [-x] [--debug] [-f <value>] [-i <value>]
$ internxt upload-file [--json] [-x] [--debug] [-f <value>] [-i <value>] [-o]

FLAGS
-f, --file=<value> The path to the file on your system.
-i, --destination=<value> 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
Expand Down Expand Up @@ -1347,11 +1349,13 @@ Upload a file to Internxt Drive

```
USAGE
$ internxt upload file [--json] [-x] [--debug] [-f <value>] [-i <value>]
$ internxt upload file [--json] [-x] [--debug] [-f <value>] [-i <value>] [-o]

FLAGS
-f, --file=<value> The path to the file on your system.
-i, --destination=<value> 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
Expand Down
23 changes: 20 additions & 3 deletions src/commands/upload-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand All @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions src/services/drive/drive-file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ export class DriveFileService {
return driveFileItem;
};

public findExistentFile = async (
folderUuid: string,
file: StorageTypes.FileStructure,
): Promise<DriveFileItem | undefined> => {
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<StorageTypes.DriveFileData> => {
Expand Down
122 changes: 122 additions & 0 deletions test/commands/upload-file.test.ts
Original file line number Diff line number Diff line change
@@ -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<LoginCredentials>>;
let validateFileExistsSpy: MockInstance<(path: string) => Promise<boolean>>;
let getDestinationFolderUuidSpy: MockInstance<() => Promise<string>>;
let findExistentFileSpy: MockInstance<typeof DriveFileService.instance.findExistentFile>;
let createFileSpy: MockInstance<typeof DriveFileService.instance.createFile>;
let replaceFileSpy: MockInstance<typeof DriveFileService.instance.replaceFile>;
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<ReturnType<typeof stat>>);
vi.mocked(createReadStream).mockReturnValue(createMockReadStream() as ReturnType<typeof createReadStream>);
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'),
);
});
});
Loading