Skip to content

Commit 464a9bf

Browse files
authored
[PB-5111] Feat: Upload Facade (#432)
* Feat: Upload Facade * Add Sleep 500ms + add node:path to fix sonarcloud issue
1 parent d706a98 commit 464a9bf

File tree

3 files changed

+261
-1
lines changed

3 files changed

+261
-1
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { basename } from 'node:path';
2+
import { CLIUtils } from '../../../utils/cli.utils';
3+
import { logger } from '../../../utils/logger.utils';
4+
import { LocalFilesystemService } from '../../local-filesystem/local-filesystem.service';
5+
import { UploadFolderParams } from './upload.types';
6+
import { UploadFolderService } from './upload-folder.service';
7+
import { UploadFileService } from './upload-file.service';
8+
import { AsyncUtils } from '../../../utils/async.utils';
9+
10+
export class UploadFacade {
11+
static readonly instance = new UploadFacade();
12+
async uploadFolder({ localPath, destinationFolderUuid, loginUserDetails, jsonFlag, onProgress }: UploadFolderParams) {
13+
const timer = CLIUtils.timer();
14+
const network = CLIUtils.prepareNetwork({ jsonFlag, loginUserDetails });
15+
const scanResult = await LocalFilesystemService.instance.scanLocalDirectory(localPath);
16+
logger.info(
17+
`Scanned folder ${localPath}: found ${scanResult.totalItems} items, total size ${scanResult.totalBytes} bytes.`,
18+
);
19+
20+
const currentProgress = { itemsUploaded: 0, bytesUploaded: 0 };
21+
const emitProgress = () => {
22+
const itemProgress = currentProgress.itemsUploaded / scanResult.totalItems;
23+
const sizeProgress = currentProgress.bytesUploaded / scanResult.totalBytes;
24+
const percentage = Math.floor((itemProgress * 0.5 + sizeProgress * 0.5) * 100);
25+
onProgress({ percentage });
26+
};
27+
28+
const folderMap = await UploadFolderService.instance.createFolders({
29+
foldersToCreate: scanResult.folders,
30+
destinationFolderUuid,
31+
currentProgress,
32+
emitProgress,
33+
});
34+
35+
if (folderMap.size === 0) {
36+
return { error: new Error('Failed to create folders, cannot upload files') };
37+
}
38+
// This aims to prevent this issue: https://inxt.atlassian.net/browse/PB-1446
39+
await AsyncUtils.sleep(500);
40+
41+
const totalBytes = await UploadFileService.instance.uploadFilesInChunks({
42+
network,
43+
filesToUpload: scanResult.files,
44+
folderMap,
45+
bucket: loginUserDetails.bucket,
46+
destinationFolderUuid,
47+
currentProgress,
48+
emitProgress,
49+
});
50+
51+
const rootFolderName = basename(localPath);
52+
const rootFolderId = folderMap.get(rootFolderName) ?? '';
53+
return {
54+
data: {
55+
totalBytes,
56+
rootFolderId,
57+
uploadTimeMs: timer.stop(),
58+
},
59+
};
60+
}
61+
}

src/services/network/upload/upload.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface UploadResult {
88
uploadTimeMs: number;
99
}
1010

11-
export interface UploadFolderHandlerParams {
11+
export interface UploadFolderParams {
1212
localPath: string;
1313
destinationFolderUuid: string;
1414
loginUserDetails: LoginUserDetails;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { beforeEach, describe, it, vi, expect } from 'vitest';
2+
import { UploadFacade } from '../../../../src/services/network/upload/upload-facade.service';
3+
import { CLIUtils } from '../../../../src/utils/cli.utils';
4+
import { logger } from '../../../../src/utils/logger.utils';
5+
import { LocalFilesystemService } from '../../../../src/services/local-filesystem/local-filesystem.service';
6+
import { UploadFolderService } from '../../../../src/services/network/upload/upload-folder.service';
7+
import { UploadFileService } from '../../../../src/services/network/upload/upload-file.service';
8+
import { NetworkFacade } from '../../../../src/services/network/network-facade.service';
9+
import { LoginUserDetails } from '../../../../src/types/command.types';
10+
import { createFileSystemNodeFixture } from './upload.service.helpers';
11+
import { AsyncUtils } from '../../../../src/utils/async.utils';
12+
13+
vi.mock('../../../../src/utils/cli.utils', () => ({
14+
CLIUtils: {
15+
timer: vi.fn(),
16+
prepareNetwork: vi.fn(),
17+
},
18+
}));
19+
20+
vi.mock('../../../../src/utils/logger.utils', () => ({
21+
logger: {
22+
info: vi.fn(),
23+
},
24+
}));
25+
26+
vi.mock('../../../../src/services/local-filesystem/local-filesystem.service', () => ({
27+
LocalFilesystemService: {
28+
instance: {
29+
scanLocalDirectory: vi.fn(),
30+
},
31+
},
32+
}));
33+
34+
vi.mock('../../../../src/services/network/upload/upload-folder.service', () => ({
35+
UploadFolderService: {
36+
instance: {
37+
createFolders: vi.fn(),
38+
},
39+
},
40+
}));
41+
42+
vi.mock('../../../../src/services/network/upload/upload-file.service', () => ({
43+
UploadFileService: {
44+
instance: {
45+
uploadFilesInChunks: vi.fn(),
46+
},
47+
},
48+
}));
49+
50+
vi.mock('../../../../src/utils/async.utils', () => ({
51+
AsyncUtils: {
52+
sleep: vi.fn().mockResolvedValue(undefined),
53+
},
54+
}));
55+
56+
describe('UploadFacade', () => {
57+
let sut: UploadFacade;
58+
59+
const mockNetworkFacade = {} as NetworkFacade;
60+
61+
const mockLoginUserDetails = {
62+
bridgeUser: 'test-bridge-user',
63+
userId: 'test-user-id',
64+
mnemonic: 'test-mnemonic',
65+
bucket: 'test-bucket',
66+
} as LoginUserDetails;
67+
const folderName = 'test-folder';
68+
const folderMap = new Map([[folderName, 'folder-uuid-123']]);
69+
beforeEach(() => {
70+
vi.clearAllMocks();
71+
sut = UploadFacade.instance;
72+
vi.mocked(CLIUtils.prepareNetwork).mockReturnValue(mockNetworkFacade);
73+
vi.mocked(LocalFilesystemService.instance.scanLocalDirectory).mockResolvedValue({
74+
folders: [createFileSystemNodeFixture({ type: 'folder', name: folderName, relativePath: folderName })],
75+
files: [
76+
createFileSystemNodeFixture({
77+
type: 'file',
78+
name: 'file1.txt',
79+
relativePath: `${folderName}/file1.txt`,
80+
size: 500,
81+
}),
82+
],
83+
totalItems: 2,
84+
totalBytes: 500,
85+
});
86+
vi.mocked(UploadFolderService.instance.createFolders).mockResolvedValue(folderMap);
87+
vi.mocked(UploadFileService.instance.uploadFilesInChunks).mockResolvedValue(500);
88+
vi.mocked(CLIUtils.timer).mockReturnValue({
89+
stop: vi.fn().mockReturnValue(1000),
90+
});
91+
});
92+
93+
describe('uploadFolder', () => {
94+
const localPath = '/local/test-folder';
95+
const destinationFolderUuid = 'dest-uuid';
96+
const onProgress = vi.fn();
97+
98+
it('should properly return an error if createFolders returns an empty map', async () => {
99+
vi.mocked(LocalFilesystemService.instance.scanLocalDirectory).mockResolvedValue({
100+
folders: [createFileSystemNodeFixture({ type: 'folder', name: 'test', relativePath: 'test' })],
101+
files: [],
102+
totalItems: 1,
103+
totalBytes: 0,
104+
});
105+
106+
vi.mocked(UploadFolderService.instance.createFolders).mockResolvedValue(new Map());
107+
108+
const result = await sut.uploadFolder({
109+
localPath,
110+
destinationFolderUuid,
111+
loginUserDetails: mockLoginUserDetails,
112+
jsonFlag: false,
113+
onProgress,
114+
});
115+
116+
expect(result.error).toBeInstanceOf(Error);
117+
expect(result.error?.message).toBe('Failed to create folders, cannot upload files');
118+
expect(UploadFolderService.instance.createFolders).toHaveBeenCalled();
119+
expect(UploadFileService.instance.uploadFilesInChunks).not.toHaveBeenCalled();
120+
});
121+
122+
it('should properly handle the upload of folder and the creation of file and return proper result', async () => {
123+
const result = await sut.uploadFolder({
124+
localPath,
125+
destinationFolderUuid,
126+
loginUserDetails: mockLoginUserDetails,
127+
jsonFlag: false,
128+
onProgress,
129+
});
130+
131+
expect(result.error).toBeUndefined();
132+
expect(result.data).toBeDefined();
133+
expect(result.data?.totalBytes).toBe(500);
134+
expect(result.data?.rootFolderId).toBe('folder-uuid-123');
135+
expect(result.data?.uploadTimeMs).toBe(1000);
136+
expect(UploadFolderService.instance.createFolders).toHaveBeenCalled();
137+
expect(UploadFileService.instance.uploadFilesInChunks).toHaveBeenCalled();
138+
expect(logger.info).toHaveBeenCalledWith(`Scanned folder ${localPath}: found 2 items, total size 500 bytes.`);
139+
});
140+
141+
it('should report progress correctly during upload', async () => {
142+
const folderMap = new Map([[folderName, 'folder-uuid-123']]);
143+
144+
vi.mocked(UploadFolderService.instance.createFolders).mockImplementation(
145+
async ({ currentProgress, emitProgress }) => {
146+
currentProgress.itemsUploaded = 1;
147+
emitProgress();
148+
return folderMap;
149+
},
150+
);
151+
152+
vi.mocked(UploadFileService.instance.uploadFilesInChunks).mockImplementation(
153+
async ({ currentProgress, emitProgress }) => {
154+
currentProgress.itemsUploaded = 2;
155+
currentProgress.bytesUploaded = 500;
156+
emitProgress();
157+
return 500;
158+
},
159+
);
160+
161+
await sut.uploadFolder({
162+
localPath,
163+
destinationFolderUuid,
164+
loginUserDetails: mockLoginUserDetails,
165+
jsonFlag: false,
166+
onProgress,
167+
});
168+
169+
expect(onProgress).toHaveBeenCalledTimes(2);
170+
expect(onProgress).toHaveBeenNthCalledWith(1, { percentage: 25 });
171+
expect(onProgress).toHaveBeenNthCalledWith(2, { percentage: 100 });
172+
});
173+
174+
it('should wait 500ms between folder creation and file upload to prevent backend indexing issues', async () => {
175+
vi.useFakeTimers();
176+
177+
vi.mocked(UploadFolderService.instance.createFolders).mockResolvedValue(folderMap);
178+
vi.mocked(UploadFileService.instance.uploadFilesInChunks).mockResolvedValue(100);
179+
180+
const uploadPromise = sut.uploadFolder({
181+
localPath,
182+
destinationFolderUuid,
183+
loginUserDetails: mockLoginUserDetails,
184+
jsonFlag: false,
185+
onProgress,
186+
});
187+
188+
await vi.advanceTimersByTimeAsync(500);
189+
await uploadPromise;
190+
191+
expect(AsyncUtils.sleep).toHaveBeenCalledWith(500);
192+
expect(AsyncUtils.sleep).toHaveBeenCalledTimes(1);
193+
expect(UploadFolderService.instance.createFolders).toHaveBeenCalled();
194+
expect(UploadFileService.instance.uploadFilesInChunks).toHaveBeenCalled();
195+
196+
vi.useRealTimers();
197+
});
198+
});
199+
});

0 commit comments

Comments
 (0)