diff --git a/package.json b/package.json index 398eff127c..f0170c86d8 100644 --- a/package.json +++ b/package.json @@ -357,6 +357,7 @@ "jest-mock-extended": "^1.0.18", "make-plural": "^6.2.2", "mini-css-extract-plugin": "^1.6.2", + "nock": "^13.2.4", "node-gyp": "7.1.2", "node-loader": "^1.0.3", "nodemon": "^2.0.15", diff --git a/src/main/extension-updater/__tests__/extension-updater.test.ts b/src/main/extension-updater/__tests__/extension-updater.test.ts new file mode 100644 index 0000000000..8493b13a71 --- /dev/null +++ b/src/main/extension-updater/__tests__/extension-updater.test.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { BundledExtensionUpdater } from "../bundled-extension-updater"; +import fs from "fs"; +import mockFs from "mock-fs"; +import nock from "nock"; + +const mockOpts = { + "some-user-data-directory": { + "some-file.tgz": "file content here", + }, + "extension-updates": { + "file.txt": "text", + }, +}; + +describe("BundledExtensionUpdater", () => { + afterEach(() => { + mockFs.restore(); + }); + + it("Should download file from server", async () => { + mockFs(mockOpts); + + const scope = nock("http://my-example-url.com") + .get("/node-menu-0.0.1.tgz") + .replyWithFile(200, `./some-user-data-directory/some-file.tgz`, { + "Content-Type": "application/tar", + }); + + await new BundledExtensionUpdater({ + name: "node-menu", + version: "0.0.1", + downloadUrl: "http://my-example-url.com/node-menu-0.0.1.tgz", + }, "./extension-updates").update(); + + const exist = await fs.promises.access(mockFs.bypass(() => "./extension-updates/node-menu-0.0.1.tgz"), fs.constants.F_OK); + + expect(exist).toBeTruthy(); + }); +}); diff --git a/src/main/extension-updater/bundled-extension-updater.ts b/src/main/extension-updater/bundled-extension-updater.ts new file mode 100644 index 0000000000..c66ecdac10 --- /dev/null +++ b/src/main/extension-updater/bundled-extension-updater.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import fs from "fs"; +import { ensureDir } from "fs-extra"; +import request from "request"; +import logger from "../logger"; +import path from "path"; +import { noop } from "../../common/utils"; + +type Extension = { + name: string + version: string + downloadUrl: string +}; + +export class BundledExtensionUpdater { + private extension: Extension; + private updateFolderPath: string; + + constructor(extension: Extension, updateFolderPath: string) { + this.extension = extension; + this.updateFolderPath = updateFolderPath; + } + + public async update() { + await this.download(); + } + + private get filePath() { + return `${this.updateFolderPath}/${this.extension.name}-${this.extension.version}.tgz`; + } + + private async download() { + const { downloadUrl, name } = this.extension; + + await ensureDir(path.dirname(this.updateFolderPath), 0o755); + + const file = fs.createWriteStream(this.filePath); + + logger.info(`[EXTENSION-UPDATER]: Downloading extension ${name} from ${downloadUrl} to ${this.filePath}`); + const requestOpts: request.UriOptions & request.CoreOptions = { + uri: downloadUrl, + gzip: true, + }; + const stream = request.get(requestOpts); + + stream.on("complete", () => { + logger.info(`[EXTENSION-UPDATER]: Download extension ${name} tgz file completed`); + file.end(noop); + }); + + stream.on("error", (error) => { + logger.error(error); + fs.unlink(this.filePath, noop); + throw error; + }); + + return new Promise((resolve, reject) => { + file.on("close", () => { + logger.info(`[EXTENSION-UPDATER]: Download extension ${name} tgz file closed`); + fs.chmod(downloadUrl, 0o755, (err) => { + if (err) reject(err); + }); + resolve(); + }); + stream.pipe(file); + }); + } +} diff --git a/yarn.lock b/yarn.lock index 26caa86d58..189cde5c6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9066,6 +9066,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -9742,6 +9747,16 @@ no-case@^3.0.3: lower-case "^2.0.1" tslib "^1.10.0" +nock@^13.2.4: + version "13.2.4" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.4.tgz#43a309d93143ee5cdcca91358614e7bde56d20e1" + integrity sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-abi@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.5.0.tgz#26e8b7b251c3260a5ac5ba5aef3b4345a0229248" @@ -11252,6 +11267,11 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + proper-lockfile@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-1.2.0.tgz#ceff5dd89d3e5f10fb75e1e8e76bc75801a59c34"