Skip to content

Commit

Permalink
feat: Add a util for file download
Browse files Browse the repository at this point in the history
  • Loading branch information
iamsivin committed Jan 17, 2025
1 parent 72e7abf commit e09ecd7
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/utils",
"version": "0.0.30",
"version": "0.0.31",
"description": "Chatwoot utils",
"private": false,
"license": "MIT",
Expand Down
53 changes: 53 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,56 @@ export const splitName = (

return { firstName, lastName };
};

interface DownloadFileOptions {
url: string;
type: string;
extension?: string | null;
}
/**
* Downloads a file from a URL with proper file type handling
* @name downloadFile
* @description Downloads file from URL with proper type handling and cleanup
* @param {Object} options Download configuration options
* @param {string} options.url File URL to download
* @param {string} options.type File type identifier
* @param {string} [options.extension] Optional file extension
* @returns {Promise<boolean>} Returns true if download successful, false otherwise
*/
export const downloadFile = async ({
url,
type,
extension = null,
}: DownloadFileOptions): Promise<void> => {
if (!url || !type) return;

try {
const response = await fetch(url);
const blobData = await response.blob();

const contentType = response.headers.get('content-type');

const fileExtension =
extension || (contentType ? contentType.split('/')[1] : type);

const dispositionHeader = response.headers.get('content-disposition');
const filenameMatch = dispositionHeader?.match(/filename="(.*?)"/);

const filename =
filenameMatch?.[1] ?? `attachment_${Date.now()}.${fileExtension}`;

const blobUrl = URL.createObjectURL(blobData);
const link = Object.assign(document.createElement('a'), {
href: blobUrl,
download: filename,
style: 'display: none',
});

document.body.append(link);
link.click();
link.remove();
URL.revokeObjectURL(blobUrl);
} catch (error) {
console.warn('Download failed:', error);
}
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
convertSecondsToTimeUnit,
fileNameWithEllipsis,
splitName,
downloadFile,
} from './helpers';

import { parseBoolean } from './string';
Expand Down Expand Up @@ -40,4 +41,5 @@ export {
sortAsc,
splitName,
trimContent,
downloadFile,
};
89 changes: 89 additions & 0 deletions test/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
convertSecondsToTimeUnit,
fileNameWithEllipsis,
splitName,
downloadFile,
} from '../src/helpers';

describe('#convertSecondsToTimeUnit', () => {
Expand Down Expand Up @@ -128,3 +129,91 @@ describe('splitName', () => {
});
});
});

describe('downloadFile', () => {
let mockFetch: jest.Mock;
let mockCreateObjectURL: jest.Mock;
let mockDOMElement: { [key: string]: jest.Mock | string };

beforeEach(() => {
// Mock fetch
mockFetch = jest.fn();
global.fetch = mockFetch;

// Mock URL methods
mockCreateObjectURL = jest.fn(() => 'blob:mock-url');
URL.createObjectURL = mockCreateObjectURL;
URL.revokeObjectURL = jest.fn();

// Mock DOM element
mockDOMElement = {
click: jest.fn(),
remove: jest.fn(),
href: '',
download: '',
};
document.createElement = jest.fn().mockReturnValue(mockDOMElement);
document.body.append = jest.fn();
});

afterEach(() => jest.clearAllMocks());

describe('successful downloads', () => {
it('should download PDF file', async () => {
const blob = new Blob(['test'], { type: 'application/pdf' });
mockFetch.mockResolvedValueOnce({
ok: true,
blob: () => Promise.resolve(blob),
headers: new Headers({ 'content-type': 'application/pdf' }),
});

await downloadFile({
url: 'test.com/doc.pdf',
type: 'pdf',
extension: 'pdf',
});

expect(mockFetch).toHaveBeenCalledWith('test.com/doc.pdf');
expect(mockCreateObjectURL).toHaveBeenCalledWith(blob);
expect(mockDOMElement.click).toHaveBeenCalled();
});

it('should download image file with content disposition', async () => {
const blob = new Blob(['test'], { type: 'image/png' });
mockFetch.mockResolvedValueOnce({
ok: true,
blob: () => Promise.resolve(blob),
headers: new Headers({
'content-type': 'image/png',
'content-disposition': 'attachment; filename="test.png"',
}),
});

await downloadFile({
url: 'test.com/image.png',
type: 'image',
extension: 'png',
});

expect(mockDOMElement.download).toBe('test.png');
});
});

describe('error handling', () => {
it('should skip if url or type missing', async () => {
await downloadFile({ url: '', type: 'pdf' });
expect(mockFetch).not.toHaveBeenCalled();
});

it('should handle network errors', async () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
mockFetch.mockRejectedValueOnce(new Error('Network error'));

await downloadFile({ url: 'test.com/file', type: 'pdf' });
expect(consoleSpy).toHaveBeenCalledWith(
'Download failed:',
expect.any(Error)
);
});
});
});

0 comments on commit e09ecd7

Please sign in to comment.