Skip to content

Commit

Permalink
feat(models): download to .tmp file (#458)
Browse files Browse the repository at this point in the history
* feat(models): download to tmp file

Signed-off-by: axel7083 <[email protected]>

* fix(downloader): rm not necessary

Signed-off-by: axel7083 <[email protected]>

* fix: prettier

Signed-off-by: axel7083 <[email protected]>

---------

Signed-off-by: axel7083 <[email protected]>
  • Loading branch information
axel7083 authored Mar 7, 2024
1 parent 2bf2349 commit b0ff03e
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 16 deletions.
128 changes: 128 additions & 0 deletions packages/backend/src/utils/downloader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { vi, test, expect, beforeEach } from 'vitest';
import { Downloader } from './downloader';
import { EventEmitter } from '@podman-desktop/api';
import { createWriteStream, promises, type WriteStream } from 'node:fs';

vi.mock('@podman-desktop/api', () => {
return {
EventEmitter: vi.fn(),
};
});

vi.mock('node:https', () => {
return {
default: {
get: vi.fn(),
},
};
});

vi.mock('node:fs', () => {
return {
createWriteStream: vi.fn(),
existsSync: vi.fn(),
promises: {
rename: vi.fn(),
},
};
});

beforeEach(() => {
const listeners: ((value: unknown) => void)[] = [];

vi.mocked(EventEmitter).mockReturnValue({
event: vi.fn().mockImplementation(callback => {
listeners.push(callback);
}),
fire: vi.fn().mockImplementation((content: unknown) => {
listeners.forEach(listener => listener(content));
}),
} as unknown as EventEmitter<unknown>);
});

test('Downloader constructor', async () => {
const downloader = new Downloader('dummyUrl', 'dummyTarget');
expect(downloader.getTarget()).toBe('dummyTarget');
});

test('perform download failed', async () => {
const downloader = new Downloader('dummyUrl', 'dummyTarget');

const closeMock = vi.fn();
const onMock = vi.fn();
vi.mocked(createWriteStream).mockReturnValue({
close: closeMock,
on: onMock,
} as unknown as WriteStream);

onMock.mockImplementation((event: string, callback: () => void) => {
if (event === 'error') {
callback();
}
});
// capture downloader event(s)
const listenerMock = vi.fn();
downloader.onEvent(listenerMock);

// perform download logic
await downloader.perform('followUpId');

expect(downloader.completed).toBeTruthy();
expect(listenerMock).toHaveBeenCalledWith({
id: 'followUpId',
message: expect.anything(),
status: 'error',
});
});

test('perform download successfully', async () => {
const downloader = new Downloader('dummyUrl', 'dummyTarget');
vi.spyOn(promises, 'rename').mockResolvedValue(undefined);

const closeMock = vi.fn();
const onMock = vi.fn();
vi.mocked(createWriteStream).mockReturnValue({
close: closeMock,
on: onMock,
} as unknown as WriteStream);

onMock.mockImplementation((event: string, callback: () => void) => {
if (event === 'finish') {
callback();
}
});

// capture downloader event(s)
const listenerMock = vi.fn();
downloader.onEvent(listenerMock);

// perform download logic
await downloader.perform('followUpId');

expect(promises.rename).toHaveBeenCalledWith('dummyTarget.tmp', 'dummyTarget');
expect(downloader.completed).toBeTruthy();
expect(listenerMock).toHaveBeenCalledWith({
id: 'followUpId',
duration: expect.anything(),
message: expect.anything(),
status: 'completed',
});
});
39 changes: 23 additions & 16 deletions packages/backend/src/utils/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
***********************************************************************/

import { getDurationSecondsSince } from './utils';
import fs from 'fs';
import { createWriteStream, promises } from 'node:fs';
import https from 'node:https';
import { EventEmitter, type Event } from '@podman-desktop/api';

Expand Down Expand Up @@ -117,7 +117,27 @@ export class Downloader {
}

private followRedirects(url: string, callback: (message: { ok?: boolean; error?: string }) => void) {
const file = fs.createWriteStream(this.target);
const tmpFile = `${this.target}.tmp`;
const stream = createWriteStream(tmpFile);

stream.on('finish', () => {
stream.close();
// Rename from tmp to expected file name.
promises
.rename(tmpFile, this.target)
.then(() => {
callback({ ok: true });
})
.catch((err: unknown) => {
callback({ error: `Something went wrong while trying to rename downloaded file: ${String(err)}.` });
});
});
stream.on('error', e => {
callback({
error: e.message,
});
});

let totalFileSize = 0;
let progress = 0;
https.get(url, { signal: this.abortSignal }, resp => {
Expand All @@ -143,21 +163,8 @@ export class Downloader {
value: progressValue,
} as ProgressEvent);
}

// send progress in percentage (ex. 1.2%, 2.6%, 80.1%) to frontend
if (progressValue === 100) {
callback({ ok: true });
}
});
file.on('finish', () => {
file.close();
});
file.on('error', e => {
callback({
error: e.message,
});
});
resp.pipe(file);
resp.pipe(stream);
});
}
}

0 comments on commit b0ff03e

Please sign in to comment.