Skip to content

Commit 5f35043

Browse files
committed
add new method to get predefined url for upload
1 parent 01d6628 commit 5f35043

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

lib/object-storage/object-storage.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { Bucket, File, Storage } from '@google-cloud/storage';
22

33
import { InternalServerError } from 'errors/apps-sdk-error';
4+
import { TIME_IN_MILLISECOND } from 'lib/utils/time-enum';
45
import {
56
DeleteFileResponse,
67
DownloadFileResponse,
78
FileInfo,
89
GetFileInfoResponse,
910
ListFilesOptions,
1011
ListFilesResponse,
12+
PresignedUrlOptions,
13+
PresignedUrlResponse,
1114
UploadFileOptions,
1215
UploadFileResponse
1316
} from 'types/object-storage';
@@ -210,4 +213,39 @@ export class ObjectStorage {
210213
};
211214
}
212215
}
216+
217+
async getPresignedUploadUrl(fileName: string, options: PresignedUrlOptions = {}): Promise<PresignedUrlResponse> {
218+
try {
219+
const bucket = this.getBucket();
220+
const file: File = bucket.file(fileName);
221+
222+
const fifteenMinutesFromNow = new Date(Date.now() + TIME_IN_MILLISECOND.MINUTE * 15);
223+
const expires = options.expires || fifteenMinutesFromNow;
224+
225+
const signedUrlOptions = {
226+
version: 'v4' as const,
227+
action: 'write' as const,
228+
expires,
229+
...(options.contentType && {
230+
contentType: options.contentType
231+
})
232+
};
233+
234+
const [presignedUrl] = await file.getSignedUrl(signedUrlOptions);
235+
236+
logger.info(`Presigned upload URL generated for file: ${fileName}`);
237+
238+
return {
239+
success: true,
240+
presignedUrl,
241+
fileName
242+
};
243+
} catch (error) {
244+
const { errorMessage } = this.handleError(error, 'generate presigned upload URL');
245+
return {
246+
success: false,
247+
error: `Failed to generate presigned upload URL: ${errorMessage}`
248+
};
249+
}
250+
}
213251
}

lib/types/object-storage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,13 @@ export type ListFilesResponse = BaseResponse & {
4343
export type GetFileInfoResponse = BaseResponse & {
4444
fileInfo?: FileInfo;
4545
}
46+
47+
export type PresignedUrlOptions = {
48+
expires?: Date;
49+
contentType?: string;
50+
}
51+
52+
export type PresignedUrlResponse = BaseResponse & {
53+
presignedUrl?: string;
54+
fileName?: string;
55+
}

tests/object-storage/object-storage.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const mockFile = {
77
download: jest.fn(),
88
getMetadata: jest.fn(),
99
delete: jest.fn(),
10+
getSignedUrl: jest.fn(),
1011
name: 'test-file.txt',
1112
};
1213

@@ -65,6 +66,9 @@ describe('ObjectStorage', () => {
6566
},
6667
]);
6768
mockFile.delete.mockResolvedValue(undefined);
69+
mockFile.getSignedUrl.mockResolvedValue([
70+
'https://storage.googleapis.com/test-bucket/test-file.txt?signed-url-params',
71+
]);
6872

6973
mockBucket.getFiles.mockResolvedValue([
7074
[
@@ -281,4 +285,136 @@ describe('ObjectStorage', () => {
281285
expect(result.error).toBe('File not found');
282286
});
283287
});
288+
289+
describe('getPresignedUploadUrl', () => {
290+
it('should generate a presigned upload URL successfully', async () => {
291+
const fileName = 'upload-file.txt';
292+
const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params';
293+
294+
mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]);
295+
296+
const result = await objectStorage.getPresignedUploadUrl(fileName);
297+
298+
expect(result.success).toBe(true);
299+
expect(result.presignedUrl).toBe(expectedUrl);
300+
expect(result.fileName).toBe(fileName);
301+
expect(mockFile.getSignedUrl).toHaveBeenCalledWith({
302+
version: 'v4',
303+
action: 'write',
304+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
305+
expires: expect.any(Date),
306+
});
307+
});
308+
309+
it('should generate a presigned upload URL with custom expiration', async () => {
310+
const fileName = 'upload-file.txt';
311+
const customExpires = new Date('2024-12-31T23:59:59Z');
312+
const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params';
313+
314+
mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]);
315+
316+
const result = await objectStorage.getPresignedUploadUrl(fileName, { expires: customExpires });
317+
318+
expect(result.success).toBe(true);
319+
expect(result.presignedUrl).toBe(expectedUrl);
320+
expect(result.fileName).toBe(fileName);
321+
expect(mockFile.getSignedUrl).toHaveBeenCalledWith({
322+
version: 'v4',
323+
action: 'write',
324+
expires: customExpires,
325+
});
326+
});
327+
328+
it('should generate a presigned upload URL with content type restriction', async () => {
329+
const fileName = 'upload-file.txt';
330+
const contentType = 'text/plain';
331+
const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params';
332+
333+
mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]);
334+
335+
const result = await objectStorage.getPresignedUploadUrl(fileName, { contentType });
336+
337+
expect(result.success).toBe(true);
338+
expect(result.presignedUrl).toBe(expectedUrl);
339+
expect(result.fileName).toBe(fileName);
340+
expect(mockFile.getSignedUrl).toHaveBeenCalledWith({
341+
version: 'v4',
342+
action: 'write',
343+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
344+
expires: expect.any(Date),
345+
contentType: 'text/plain',
346+
});
347+
});
348+
349+
it('should generate a presigned upload URL with all options', async () => {
350+
const fileName = 'upload-file.txt';
351+
const customExpires = new Date('2024-12-31T23:59:59Z');
352+
const contentType = 'application/json';
353+
const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params';
354+
355+
mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]);
356+
357+
const result = await objectStorage.getPresignedUploadUrl(fileName, {
358+
expires: customExpires,
359+
contentType,
360+
});
361+
362+
expect(result.success).toBe(true);
363+
expect(result.presignedUrl).toBe(expectedUrl);
364+
expect(result.fileName).toBe(fileName);
365+
expect(mockFile.getSignedUrl).toHaveBeenCalledWith({
366+
version: 'v4',
367+
action: 'write',
368+
expires: customExpires,
369+
contentType: 'application/json',
370+
});
371+
});
372+
373+
it('should use default expiration when no expires option is provided', async () => {
374+
const fileName = 'upload-file.txt';
375+
const expectedUrl = 'https://storage.googleapis.com/test-bucket/upload-file.txt?signed-url-params';
376+
377+
mockFile.getSignedUrl.mockResolvedValueOnce([expectedUrl]);
378+
379+
// Mock Date.now to have predictable test results
380+
const mockNow = new Date('2023-01-01T12:00:00Z').getTime();
381+
const originalDateNow = Date.now;
382+
Date.now = jest.fn(() => mockNow);
383+
384+
const result = await objectStorage.getPresignedUploadUrl(fileName);
385+
386+
expect(result.success).toBe(true);
387+
expect(mockFile.getSignedUrl).toHaveBeenCalledWith({
388+
version: 'v4',
389+
action: 'write',
390+
expires: new Date(mockNow + 15 * 60 * 1000), // 15 minutes from mockNow
391+
});
392+
393+
// Restore original Date.now
394+
Date.now = originalDateNow;
395+
});
396+
397+
it('should handle presigned URL generation failure', async () => {
398+
const fileName = 'upload-file.txt';
399+
400+
mockFile.getSignedUrl.mockRejectedValueOnce(new Error('Signing failed'));
401+
402+
const result = await objectStorage.getPresignedUploadUrl(fileName);
403+
404+
expect(result.success).toBe(false);
405+
expect(result.error).toContain('Failed to generate presigned upload URL');
406+
expect(result.presignedUrl).toBeUndefined();
407+
});
408+
409+
it('should handle empty file name gracefully', async () => {
410+
const fileName = '';
411+
412+
mockFile.getSignedUrl.mockRejectedValueOnce(new Error('Invalid file name'));
413+
414+
const result = await objectStorage.getPresignedUploadUrl(fileName);
415+
416+
expect(result.success).toBe(false);
417+
expect(result.error).toContain('Failed to generate presigned upload URL');
418+
});
419+
});
284420
});

0 commit comments

Comments
 (0)