-
-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ chore(core): Add
downloadFile
function (#897)
- Loading branch information
1 parent
9a338dc
commit 92019e9
Showing
6 changed files
with
1,012 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,3 +18,6 @@ node_modules | |
# Compiled output | ||
dist | ||
types | ||
|
||
# Vitest | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import path from 'node:path' | ||
import axios from 'axios' | ||
import fs from 'fs-extra' | ||
|
||
type DownloaderOptions = { | ||
url: string | ||
outputDir: string | ||
fileName: string | ||
overrideFile?: boolean | ||
} | ||
|
||
type DownloadFileResult = { | ||
filePath: string | ||
downloadSkipped: boolean | ||
} | ||
|
||
export async function downloadFile(options: DownloaderOptions) { | ||
const { url, outputDir, fileName, overrideFile } = options | ||
|
||
const returnPromise = new Promise<DownloadFileResult>((resolve, reject) => { | ||
const filePath = path.join(outputDir, fileName) | ||
|
||
const fileExists = fs.existsSync(filePath) | ||
if (fileExists && !overrideFile) { | ||
resolve({ | ||
filePath, | ||
downloadSkipped: true | ||
}) | ||
} | ||
|
||
axios | ||
.get(url, { | ||
responseType: 'stream' | ||
}) | ||
.then((response) => { | ||
const writer = fs.createWriteStream(filePath) | ||
|
||
response.data.pipe(writer) | ||
|
||
writer.on('finish', () => { | ||
resolve({ | ||
filePath, | ||
downloadSkipped: false | ||
}) | ||
}) | ||
writer.on('error', (error) => { | ||
// TODO: Handle errors in a more sophisticated way | ||
reject(new Error(`[Writer] ${error.message}`)) | ||
}) | ||
}) | ||
.catch((error) => { | ||
// TODO: Handle errors in a more sophisticated way | ||
reject(new Error(`[Axios] ${error.message}`)) | ||
}) | ||
}) | ||
|
||
// TODO: This is a workaround to handle errors from both `writer` and `axios` and get 100% test coverage. | ||
return returnPromise.catch((error: Error) => { | ||
throw new Error( | ||
`[DownloadFile] Error downloading the file - ${error.message}` | ||
) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import axios from 'axios' | ||
import { fs, vol } from 'memfs' | ||
import { rest } from 'msw' | ||
import { setupServer } from 'msw/node' | ||
import { | ||
afterAll, | ||
afterEach, | ||
beforeAll, | ||
beforeEach, | ||
describe, | ||
expect, | ||
it, | ||
vi | ||
} from 'vitest' | ||
import { downloadFile } from '../src/downloadFile' | ||
|
||
const MOCK_OUTPUT_DIR = '/tmp' | ||
const MOCK_FILE_NAME = 'duck.txt' | ||
const MOCK_FILE_CONTENT = 'Quack! 🐾' | ||
const MOCK_FILE_PATH = `${MOCK_OUTPUT_DIR}/${MOCK_FILE_NAME}` | ||
const MOCK_URL = `https://example.com/${MOCK_FILE_NAME}` | ||
|
||
const server = setupServer( | ||
rest.get(MOCK_URL, (_, res, ctx) => { | ||
return res(ctx.text(MOCK_FILE_CONTENT)) | ||
}) | ||
) | ||
|
||
vi.mock('fs-extra', async () => { | ||
return { | ||
default: fs | ||
} | ||
}) | ||
|
||
describe('downloadFile', () => { | ||
beforeAll(() => { | ||
server.listen() | ||
}) | ||
|
||
afterAll(() => { | ||
server.close() | ||
vi.resetAllMocks() | ||
}) | ||
|
||
beforeEach(() => { | ||
vol.mkdirSync(MOCK_OUTPUT_DIR) | ||
}) | ||
|
||
afterEach(() => { | ||
server.resetHandlers() | ||
vol.reset() // Clear the in-memory file system after each test | ||
vi.clearAllMocks() | ||
}) | ||
|
||
it('calls axios.get with the correct arguments', async () => { | ||
const axiosSpy = vi.spyOn(axios, 'get') | ||
|
||
await downloadFile({ | ||
url: MOCK_URL, | ||
outputDir: MOCK_OUTPUT_DIR, | ||
fileName: MOCK_FILE_NAME | ||
}) | ||
|
||
expect(axiosSpy).toHaveBeenCalledOnce() | ||
expect(axiosSpy).toHaveBeenCalledWith(MOCK_URL, { | ||
responseType: 'stream' | ||
}) | ||
}) | ||
|
||
describe('when the file does not exist', () => { | ||
it('downloads a file', async () => { | ||
const result = await downloadFile({ | ||
url: MOCK_URL, | ||
outputDir: MOCK_OUTPUT_DIR, | ||
fileName: MOCK_FILE_NAME | ||
}) | ||
|
||
expect(result).to.deep.equal({ | ||
filePath: MOCK_FILE_PATH, | ||
downloadSkipped: false | ||
}) | ||
expect(fs.existsSync(result.filePath)).toBe(true) | ||
expect(fs.readFileSync(result.filePath, 'utf8')).toBe(MOCK_FILE_CONTENT) | ||
}) | ||
|
||
it('throws an error if the file cannot be downloaded', async () => { | ||
server.use( | ||
rest.get(MOCK_URL, (_, res, ctx) => { | ||
return res( | ||
ctx.status(500), | ||
ctx.json({ message: 'Internal server error' }) | ||
) | ||
}) | ||
) | ||
|
||
const downloadFileFuncCall = downloadFile({ | ||
url: MOCK_URL, | ||
outputDir: MOCK_OUTPUT_DIR, | ||
fileName: MOCK_FILE_NAME | ||
}) | ||
|
||
await expect(() => downloadFileFuncCall).rejects.toThrowError( | ||
'[DownloadFile] Error downloading the file - [Axios] Request failed with status code 500' | ||
) | ||
}) | ||
}) | ||
|
||
describe('when the file already exists', () => { | ||
const existingMockFileContent = 'This is an existing duck file! 🦆' | ||
|
||
beforeEach(() => { | ||
fs.writeFileSync(MOCK_FILE_PATH, existingMockFileContent) | ||
expect(fs.existsSync(MOCK_FILE_PATH)).toBe(true) | ||
}) | ||
|
||
it('skips download if the file exists', async () => { | ||
const result = await downloadFile({ | ||
url: MOCK_URL, | ||
outputDir: MOCK_OUTPUT_DIR, | ||
fileName: MOCK_FILE_NAME | ||
}) | ||
|
||
expect(result).to.deep.equal({ | ||
filePath: MOCK_FILE_PATH, | ||
downloadSkipped: true | ||
}) | ||
expect(fs.existsSync(result.filePath)).toBe(true) | ||
expect(fs.readFileSync(result.filePath, 'utf8')).toBe( | ||
existingMockFileContent | ||
) | ||
}) | ||
|
||
it('overwrites the existing file if the `overrideFile` flag is present', async () => { | ||
const result = await downloadFile({ | ||
url: MOCK_URL, | ||
outputDir: MOCK_OUTPUT_DIR, | ||
fileName: MOCK_FILE_NAME, | ||
overrideFile: true | ||
}) | ||
|
||
expect(result).to.deep.equal({ | ||
filePath: MOCK_FILE_PATH, | ||
downloadSkipped: false | ||
}) | ||
expect(fs.existsSync(result.filePath)).toBe(true) | ||
expect(fs.readFileSync(result.filePath, 'utf8')).toBe(MOCK_FILE_CONTENT) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.