From bddf5765bd0f9ac8feea5669ef7b58ee6a3c3cee Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Fri, 10 Apr 2020 22:13:05 +0200 Subject: [PATCH 01/13] Added metadata reading API endpoint (#28) This implements an '/api/packages/metadata' which reads the global package manifest as well as the user's in VFS. Currently only system based VFS adapters is supported. --- src/packages.js | 36 ++++++++++++++++++++++++++++++++++++ src/providers/packages.js | 20 ++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/packages.js b/src/packages.js index 072c12f..94dd06b 100644 --- a/src/packages.js +++ b/src/packages.js @@ -41,6 +41,12 @@ const readOrDefault = filename => fs.existsSync(filename) ? fs.readJsonSync(filename) : []; +/** + * @typedef InstallPackageOptions + * @param {boolean} system + * @param {object} [auth] + */ + /** * OS.js Package Management */ @@ -133,6 +139,36 @@ class Packages { }, 500); } + /** + * Installs a package from given url + * @param {string} url + * @param {InstallPackageOptions} options + */ + async installPackage(url, options) { + throw new Error('Not implemented yet'); + } + + /** + * Reads package manifests + * @return {Package[]} List of packages + */ + async readPackageManifests(user) { + const {realpath} = this.core.make('osjs/vfs'); + const {manifestFile} = this.options; + const homePath = await realpath('home:/.packages/metadata.json', user); + + const systemManifest = await readOrDefault(manifestFile); + const userManifest = await readOrDefault(homePath); + + return [ + ...systemManifest, + ...userManifest.map(pkg => Object.assign({}, pkg, { + _user: true, + server: null + })) + ]; + } + /** * Loads package data * @param {string} filename Filename diff --git a/src/providers/packages.js b/src/providers/packages.js index 938bac3..828130b 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -55,6 +55,12 @@ class PackageServiceProvider extends ServiceProvider { }); } + depends() { + return [ + 'osjs/express' + ]; + } + provides() { return [ 'osjs/packages' @@ -62,8 +68,22 @@ class PackageServiceProvider extends ServiceProvider { } init() { + const {routeAuthenticated} = this.core.make('osjs/express'); + this.core.singleton('osjs/packages', () => this.packages); + routeAuthenticated('GET', '/api/packages/manifest', (req, res) => { + this.packages.readPackageManifests(req.session.user) + .then(json => res.json(json)) + .catch(error => res.status(400).json({error})); + }); + + routeAuthenticated('POST', '/api/packages/install', (req, res) => { + this.packages.installPackage(req.body.url, req.body.options) + .then(() => res.json({success: true})) + .catch(error => res.status(400).json({error})); + }); + return this.packages.init(); } From 547eb7e195b7e4fffe1862535732678a7bdc9ce3 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Fri, 10 Apr 2020 22:37:00 +0200 Subject: [PATCH 02/13] Support metadata loading from list in API (#28) --- src/packages.js | 22 ++++++++++++++-------- src/providers/packages.js | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/packages.js b/src/packages.js index 94dd06b..e02814a 100644 --- a/src/packages.js +++ b/src/packages.js @@ -150,22 +150,28 @@ class Packages { /** * Reads package manifests + * @param {string[]} paths + * @param {object} user * @return {Package[]} List of packages */ - async readPackageManifests(user) { + async readPackageManifests(paths, user) { const {realpath} = this.core.make('osjs/vfs'); const {manifestFile} = this.options; - const homePath = await realpath('home:/.packages/metadata.json', user); - const systemManifest = await readOrDefault(manifestFile); - const userManifest = await readOrDefault(homePath); + + const userManifests = await Promise.all(paths.map(async p => { + const real = await realpath(`${p}/metadata.json`, user); + const list = await readOrDefault(real); + + return list.map(pkg => Object.assign({}, pkg, { + _vfs: p, + server: null + })); + })); return [ ...systemManifest, - ...userManifest.map(pkg => Object.assign({}, pkg, { - _user: true, - server: null - })) + ...[].concat(...userManifests) ]; } diff --git a/src/providers/packages.js b/src/providers/packages.js index 828130b..71e7457 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -73,7 +73,7 @@ class PackageServiceProvider extends ServiceProvider { this.core.singleton('osjs/packages', () => this.packages); routeAuthenticated('GET', '/api/packages/manifest', (req, res) => { - this.packages.readPackageManifests(req.session.user) + this.packages.readPackageManifests(req.query.root || [], req.session.user) .then(json => res.json(json)) .catch(error => res.status(400).json({error})); }); From 35876fb34cc94ab9dacd47a327c73981cab15157 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Fri, 10 Apr 2020 22:58:04 +0200 Subject: [PATCH 03/13] Only allow user packages on system VFS (#28) --- src/packages.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/packages.js b/src/packages.js index e02814a..5cdf1e8 100644 --- a/src/packages.js +++ b/src/packages.js @@ -31,8 +31,9 @@ const fs = require('fs-extra'); const fg = require('fast-glob'); const path = require('path'); -const Package = require('./package.js'); const consola = require('consola'); +const Package = require('./package.js'); +const {getPrefix} = require('./utils/vfs.js'); const logger = consola.withTag('Packages'); const relative = filename => filename.replace(process.cwd(), ''); @@ -155,11 +156,17 @@ class Packages { * @return {Package[]} List of packages */ async readPackageManifests(paths, user) { - const {realpath} = this.core.make('osjs/vfs'); + const {realpath, mountpoints} = this.core.make('osjs/vfs'); const {manifestFile} = this.options; const systemManifest = await readOrDefault(manifestFile); - const userManifests = await Promise.all(paths.map(async p => { + const isValidVfs = p => { + const prefix = getPrefix(p); + const mount = mountpoints.find(m => m.name === prefix); + return mount && mount.attributes.root; + }; + + const userManifests = await Promise.all(paths.filter(isValidVfs).map(async p => { const real = await realpath(`${p}/metadata.json`, user); const list = await readOrDefault(real); From 101c222c520d9dda22ba16ed903bcdf39b807f10 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Fri, 10 Apr 2020 23:52:20 +0200 Subject: [PATCH 04/13] Implemented package installation from URL (#28) This also adds in manifest generation --- package.json | 2 ++ src/packages.js | 44 ++++++++++++++++++++++++++++++++++++--- src/providers/packages.js | 7 +++++-- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5e6cc54..b76ff34 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "homepage": "https://github.com/os-js/osjs-server#readme", "dependencies": { "@osjs/common": "^3.0.8", + "bent": "^7.1.2", "body-parser": "^1.19.0", "chokidar": "^3.3.1", "connect-loki": "^1.1.0", @@ -53,6 +54,7 @@ "morgan": "^1.9.1", "nocache": "^2.1.0", "sanitize-filename": "^1.6.3", + "tar": "^6.0.1", "uuid": "^3.4.0" }, "devDependencies": { diff --git a/src/packages.js b/src/packages.js index 5cdf1e8..057ec25 100644 --- a/src/packages.js +++ b/src/packages.js @@ -32,6 +32,8 @@ const fs = require('fs-extra'); const fg = require('fast-glob'); const path = require('path'); const consola = require('consola'); +const bent = require('bent'); +const tar = require('tar'); const Package = require('./package.js'); const {getPrefix} = require('./utils/vfs.js'); const logger = consola.withTag('Packages'); @@ -42,10 +44,17 @@ const readOrDefault = filename => fs.existsSync(filename) ? fs.readJsonSync(filename) : []; +const extract = (stream, target) => new Promise((resolve, reject) => { + stream.once('end', () => resolve()); + stream.once('error', error => reject(error)); + stream.pipe(tar.extract({C: target})); +}); + /** * @typedef InstallPackageOptions + * @param {string} root * @param {boolean} system - * @param {object} [auth] + * @param {object} [headers] */ /** @@ -144,9 +153,38 @@ class Packages { * Installs a package from given url * @param {string} url * @param {InstallPackageOptions} options + * @param {object} user */ - async installPackage(url, options) { - throw new Error('Not implemented yet'); + async installPackage(url, options, user) { + const {realpath} = this.core.make('osjs/vfs'); + + const name = path.basename(url.split('?')[0]) + .replace(/\.[^/.]+$/, ''); + + const stream = await bent()(url, null, { + headers: options.headers || {} + }); + + const userRoot = options.root || 'home:/.packages'; // FIXME: Client-side + const target = await realpath(`${userRoot}/${name}`, user); + const root = await realpath(userRoot, user); + const manifest = await realpath(`${userRoot}/metadata.json`, user); + + if (await fs.exists(target)) { + throw new Error('Target already exists'); + } + + if (options.system) { + throw new Error('System packages not yet implemented'); + } + + await fs.mkdir(target); + await extract(stream, target); + + const filenames = await fg(root + '/*/metadata.json'); + const metadatas = await Promise.all(filenames.map(f => fs.readJson(f))); + + await fs.writeJson(manifest, metadatas); } /** diff --git a/src/providers/packages.js b/src/providers/packages.js index 71e7457..31afff3 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -79,9 +79,12 @@ class PackageServiceProvider extends ServiceProvider { }); routeAuthenticated('POST', '/api/packages/install', (req, res) => { - this.packages.installPackage(req.body.url, req.body.options) + this.packages.installPackage(req.body.url, req.body.options, req.session.user) .then(() => res.json({success: true})) - .catch(error => res.status(400).json({error})); + .catch((error) => { + console.error(error); + res.status(400).json({error: 'Package installation failed'}); + }); }); return this.packages.init(); From dfead97054b8497cdd2e1a602807cb3302e09948 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 00:02:03 +0200 Subject: [PATCH 05/13] Check for valid package installation archive (#28) --- src/packages.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/packages.js b/src/packages.js index 057ec25..a857045 100644 --- a/src/packages.js +++ b/src/packages.js @@ -161,10 +161,6 @@ class Packages { const name = path.basename(url.split('?')[0]) .replace(/\.[^/.]+$/, ''); - const stream = await bent()(url, null, { - headers: options.headers || {} - }); - const userRoot = options.root || 'home:/.packages'; // FIXME: Client-side const target = await realpath(`${userRoot}/${name}`, user); const root = await realpath(userRoot, user); @@ -178,10 +174,23 @@ class Packages { throw new Error('System packages not yet implemented'); } + const stream = await bent()(url, null, { + headers: options.headers || {} + }); + await fs.mkdir(target); await extract(stream, target); - const filenames = await fg(root + '/*/metadata.json'); + // FIXME: npm packages have a 'package' subdirectory + if (!await fs.exists(path.resolve(target, 'metadata.json'))) { + await fs.unlink(target); + + throw new Error('Invalid package'); + } + + // TODO: Check conflicts ? + + const filenames = await fg(root + '/*/metadata.json'); // FIXME: Windows! const metadatas = await Promise.all(filenames.map(f => fs.readJson(f))); await fs.writeJson(manifest, metadatas); From cb0bdbdc65432610a14b9ed90a6c2caf2fd2e07c Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 00:12:00 +0200 Subject: [PATCH 06/13] Depend on client-side package installation root (#28) --- src/packages.js | 10 +++++++++- src/providers/packages.js | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/packages.js b/src/packages.js index a857045..9ff4d46 100644 --- a/src/packages.js +++ b/src/packages.js @@ -156,12 +156,16 @@ class Packages { * @param {object} user */ async installPackage(url, options, user) { + if (!options.root) { + throw new Error('Missing package installation root path'); + } + const {realpath} = this.core.make('osjs/vfs'); const name = path.basename(url.split('?')[0]) .replace(/\.[^/.]+$/, ''); - const userRoot = options.root || 'home:/.packages'; // FIXME: Client-side + const userRoot = options.root; const target = await realpath(`${userRoot}/${name}`, user); const root = await realpath(userRoot, user); const manifest = await realpath(`${userRoot}/metadata.json`, user); @@ -194,6 +198,10 @@ class Packages { const metadatas = await Promise.all(filenames.map(f => fs.readJson(f))); await fs.writeJson(manifest, metadatas); + + return { + reload: !options.system + }; } /** diff --git a/src/providers/packages.js b/src/providers/packages.js index 31afff3..6845bec 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -80,7 +80,7 @@ class PackageServiceProvider extends ServiceProvider { routeAuthenticated('POST', '/api/packages/install', (req, res) => { this.packages.installPackage(req.body.url, req.body.options, req.session.user) - .then(() => res.json({success: true})) + .then(body => res.json(body)) .catch((error) => { console.error(error); res.status(400).json({error: 'Package installation failed'}); From 930ce038951b3ec22a20810b23bf9d3506c2c6d6 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 00:13:21 +0200 Subject: [PATCH 07/13] Updated package installation failure cleanup (#28) --- src/packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages.js b/src/packages.js index 9ff4d46..ac5ead1 100644 --- a/src/packages.js +++ b/src/packages.js @@ -187,7 +187,7 @@ class Packages { // FIXME: npm packages have a 'package' subdirectory if (!await fs.exists(path.resolve(target, 'metadata.json'))) { - await fs.unlink(target); + await fs.remove(target); throw new Error('Invalid package'); } From 2fed1fbf21c1f7e671133b9f178357bb7f8178c8 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 00:31:01 +0200 Subject: [PATCH 08/13] Added user package uninstallation (#28) --- src/packages.js | 50 ++++++++++++++++++++++++++++++++++----- src/providers/packages.js | 9 +++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/packages.js b/src/packages.js index ac5ead1..eb8e353 100644 --- a/src/packages.js +++ b/src/packages.js @@ -167,8 +167,6 @@ class Packages { const userRoot = options.root; const target = await realpath(`${userRoot}/${name}`, user); - const root = await realpath(userRoot, user); - const manifest = await realpath(`${userRoot}/metadata.json`, user); if (await fs.exists(target)) { throw new Error('Target already exists'); @@ -192,18 +190,58 @@ class Packages { throw new Error('Invalid package'); } - // TODO: Check conflicts ? + await this.writeUserManifest(userRoot, user); - const filenames = await fg(root + '/*/metadata.json'); // FIXME: Windows! - const metadatas = await Promise.all(filenames.map(f => fs.readJson(f))); + return { + reload: !options.system + }; + } - await fs.writeJson(manifest, metadatas); + /** + * Uninstalls a package by name + * @param {string} name + * @param {InstallPackageOptions} options + * @param {object} user + */ + async uninstallPackage(name, options, user) { + const {realpath} = this.core.make('osjs/vfs'); + + if (!options.root) { + throw new Error('Missing package installation root path'); + } + + const userRoot = options.root; + const target = await realpath(`${userRoot}/${name}`, user); + + if (await fs.exists(target)) { + await fs.remove(target); + await this.writeUserManifest(userRoot, user); + } else { + throw new Error('Package not found in root directory'); + } return { reload: !options.system }; } + /** + * Writes user installed package manifest + * @param {string} userRoot + * @param {object} user + */ + async writeUserManifest(userRoot, user) { + const {realpath} = this.core.make('osjs/vfs'); + + // TODO: Check conflicts ? + const root = await realpath(userRoot, user); + const manifest = await realpath(`${userRoot}/metadata.json`, user); + const filenames = await fg(root + '/*/metadata.json'); // FIXME: Windows! + const metadatas = await Promise.all(filenames.map(f => fs.readJson(f))); + + await fs.writeJson(manifest, metadatas); + } + /** * Reads package manifests * @param {string[]} paths diff --git a/src/providers/packages.js b/src/providers/packages.js index 6845bec..2bf13a0 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -87,6 +87,15 @@ class PackageServiceProvider extends ServiceProvider { }); }); + routeAuthenticated('POST', '/api/packages/uninstall', (req, res) => { + this.packages.uninstallPackage(req.body.name, req.body.options, req.session.user) + .then(body => res.json(body)) + .catch((error) => { + console.error(error); + res.status(400).json({error: 'Package uninstallation failed'}); + }); + }); + return this.packages.init(); } From 3dbe6bbaf410d396b87569b57368046243e84b06 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 00:41:17 +0200 Subject: [PATCH 09/13] Updated package manifest fetch endpoint (#28) --- src/providers/packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/packages.js b/src/providers/packages.js index 2bf13a0..53feac6 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -72,7 +72,7 @@ class PackageServiceProvider extends ServiceProvider { this.core.singleton('osjs/packages', () => this.packages); - routeAuthenticated('GET', '/api/packages/manifest', (req, res) => { + routeAuthenticated('GET', '/api/packages/metadata', (req, res) => { this.packages.readPackageManifests(req.query.root || [], req.session.user) .then(json => res.json(json)) .catch(error => res.status(400).json({error})); From 317a736614c6691a83a756fa469f0fe5aa1a7d1a Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 00:59:44 +0200 Subject: [PATCH 10/13] Minor cleanup in package manager API endpoints (#28) --- src/providers/packages.js | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/providers/packages.js b/src/providers/packages.js index 53feac6..8bc2d55 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -72,29 +72,24 @@ class PackageServiceProvider extends ServiceProvider { this.core.singleton('osjs/packages', () => this.packages); - routeAuthenticated('GET', '/api/packages/metadata', (req, res) => { - this.packages.readPackageManifests(req.query.root || [], req.session.user) - .then(json => res.json(json)) - .catch(error => res.status(400).json({error})); - }); + const usingPackageManager = cb => (req, res) => cb(req, res) + .then(json => res.json(json)) + .catch((error) => { + console.error(error); + res.status(400).json({error: 'Action failed'}); + }); - routeAuthenticated('POST', '/api/packages/install', (req, res) => { - this.packages.installPackage(req.body.url, req.body.options, req.session.user) - .then(body => res.json(body)) - .catch((error) => { - console.error(error); - res.status(400).json({error: 'Package installation failed'}); - }); - }); + routeAuthenticated('GET', '/api/packages/metadata', usingPackageManager((req, res) => { + return this.packages.readPackageManifests(req.query.root || [], req.session.user); + })); - routeAuthenticated('POST', '/api/packages/uninstall', (req, res) => { - this.packages.uninstallPackage(req.body.name, req.body.options, req.session.user) - .then(body => res.json(body)) - .catch((error) => { - console.error(error); - res.status(400).json({error: 'Package uninstallation failed'}); - }); - }); + routeAuthenticated('POST', '/api/packages/install', usingPackageManager((req, res) => { + return this.packages.installPackage(req.body.url, req.body.options, req.session.user); + })); + + routeAuthenticated('POST', '/api/packages/uninstall', usingPackageManager((req, res) => { + return this.packages.uninstallPackage(req.body.name, req.body.options, req.session.user); + })); return this.packages.init(); } From 14163576e63a3a092d03a2464b8ab249bcac72df Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 01:12:40 +0200 Subject: [PATCH 11/13] Abstracted package manager utility functions (#28) --- src/packages.js | 44 ++++++++++----------------- src/providers/packages.js | 28 ++++++++++------- src/utils/packages.js | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 src/utils/packages.js diff --git a/src/packages.js b/src/packages.js index eb8e353..bf5a32b 100644 --- a/src/packages.js +++ b/src/packages.js @@ -32,23 +32,17 @@ const fs = require('fs-extra'); const fg = require('fast-glob'); const path = require('path'); const consola = require('consola'); -const bent = require('bent'); -const tar = require('tar'); const Package = require('./package.js'); const {getPrefix} = require('./utils/vfs.js'); -const logger = consola.withTag('Packages'); - -const relative = filename => filename.replace(process.cwd(), ''); +const { + relative, + archiveName, + fetchSteam, + readOrDefault, + extract +} = require('./utils/packages.js'); -const readOrDefault = filename => fs.existsSync(filename) - ? fs.readJsonSync(filename) - : []; - -const extract = (stream, target) => new Promise((resolve, reject) => { - stream.once('end', () => resolve()); - stream.once('error', error => reject(error)); - stream.pipe(tar.extract({C: target})); -}); +const logger = consola.withTag('Packages'); /** * @typedef InstallPackageOptions @@ -156,30 +150,22 @@ class Packages { * @param {object} user */ async installPackage(url, options, user) { + const {realpath} = this.core.make('osjs/vfs'); + if (!options.root) { throw new Error('Missing package installation root path'); } - const {realpath} = this.core.make('osjs/vfs'); - - const name = path.basename(url.split('?')[0]) - .replace(/\.[^/.]+$/, ''); - - const userRoot = options.root; - const target = await realpath(`${userRoot}/${name}`, user); + const name = archiveName(url); + const target = await realpath(`${options.root}/${name}`, user); if (await fs.exists(target)) { throw new Error('Target already exists'); - } - - if (options.system) { + } else if (options.system) { throw new Error('System packages not yet implemented'); } - const stream = await bent()(url, null, { - headers: options.headers || {} - }); - + const stream = await fetchSteam(url, options); await fs.mkdir(target); await extract(stream, target); @@ -190,7 +176,7 @@ class Packages { throw new Error('Invalid package'); } - await this.writeUserManifest(userRoot, user); + await this.writeUserManifest(options.root, user); return { reload: !options.system diff --git a/src/providers/packages.js b/src/providers/packages.js index 8bc2d55..3a02ad4 100644 --- a/src/providers/packages.js +++ b/src/providers/packages.js @@ -79,17 +79,23 @@ class PackageServiceProvider extends ServiceProvider { res.status(400).json({error: 'Action failed'}); }); - routeAuthenticated('GET', '/api/packages/metadata', usingPackageManager((req, res) => { - return this.packages.readPackageManifests(req.query.root || [], req.session.user); - })); - - routeAuthenticated('POST', '/api/packages/install', usingPackageManager((req, res) => { - return this.packages.installPackage(req.body.url, req.body.options, req.session.user); - })); - - routeAuthenticated('POST', '/api/packages/uninstall', usingPackageManager((req, res) => { - return this.packages.uninstallPackage(req.body.name, req.body.options, req.session.user); - })); + routeAuthenticated( + 'GET', + '/api/packages/metadata', + usingPackageManager(req => this.packages.readPackageManifests(req.query.root || [], req.session.user)) + ); + + routeAuthenticated( + 'POST', + '/api/packages/install', + usingPackageManager(req => this.packages.installPackage(req.body.url, req.body.options, req.session.user)) + ); + + routeAuthenticated( + 'POST', + '/api/packages/uninstall', + usingPackageManager(req => this.packages.uninstallPackage(req.body.name, req.body.options, req.session.user)) + ); return this.packages.init(); } diff --git a/src/utils/packages.js b/src/utils/packages.js new file mode 100644 index 0000000..0a88355 --- /dev/null +++ b/src/utils/packages.js @@ -0,0 +1,63 @@ +/* + * OS.js - JavaScript Cloud/Web Desktop Platform + * + * Copyright (c) 2011-2020, Anders Evenrud + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Anders Evenrud + * @licence Simplified BSD License + */ + +const bent = require('bent'); +const tar = require('tar'); +const path = require('path'); +const fs = require('fs-extra'); + +const relative = filename => filename + .replace(process.cwd(), ''); + +const archiveName = url => path + .basename(url.split('?')[0]) + .replace(/\.[^/.]+$/, ''); + +const fetchSteam = (url, options) => bent()(url, null, { + headers: options.headers || {} +}); + +const readOrDefault = filename => fs.existsSync(filename) + ? fs.readJsonSync(filename) + : []; + +const extract = (stream, target) => new Promise((resolve, reject) => { + stream.once('end', () => resolve()); + stream.once('error', error => reject(error)); + stream.pipe(tar.extract({C: target})); +}); + +module.exports = { + relative, + archiveName, + fetchSteam, + readOrDefault, + extract +}; From a63d2bf44949ddb2fb3c7992a691661798e7e5e1 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 12:55:27 +0200 Subject: [PATCH 12/13] Updated unit tests (#28) --- __mocks__/bent.js | 13 +++++++++++++ .../user-installable-package/metadata.json | 3 +++ __tests__/packages.js | 18 ++++++++++++++++++ src/packages.js | 10 +++++++--- 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 __mocks__/bent.js create mode 100644 __mocks__/user-installable-package/metadata.json diff --git a/__mocks__/bent.js b/__mocks__/bent.js new file mode 100644 index 0000000..e3a3591 --- /dev/null +++ b/__mocks__/bent.js @@ -0,0 +1,13 @@ +const fs = require('fs-extra'); +const tar = require('tar'); +const path = require('path'); + +module.exports = () => async () => { + const root = path.resolve(__dirname, 'user-installable-package'); + const files = await fs.readdir(root); + + return tar.c({ + gzip: true, + cwd: root + }, files); +}; diff --git a/__mocks__/user-installable-package/metadata.json b/__mocks__/user-installable-package/metadata.json new file mode 100644 index 0000000..455dabd --- /dev/null +++ b/__mocks__/user-installable-package/metadata.json @@ -0,0 +1,3 @@ +{ + "name": "UserInstallablePackage" +} diff --git a/__tests__/packages.js b/__tests__/packages.js index 940c477..bbb7644 100644 --- a/__tests__/packages.js +++ b/__tests__/packages.js @@ -2,6 +2,8 @@ const osjs = require('osjs'); const path = require('path'); const Packages = require('../src/packages.js'); +jest.mock('bent'); + describe('Packages', () => { let core; let packages; @@ -25,6 +27,22 @@ describe('Packages', () => { .toBe(true); }); + test('#installPackage', async () => { + await expect(packages.installPackage('jest:/UserInstallablePackage.tgz?redacted', { + root: 'home:/.packages' + }, {username: 'packages'})).resolves.toEqual({ + reload: true + }); + }); + + test('#uninstallPackage', async () => { + await expect(packages.uninstallPackage('UserInstallablePackage', { + root: 'home:/.packages' + }, {username: 'packages'})).resolves.toEqual({ + reload: true + }); + }); + test('#handleMessage', () => { const params = [{ pid: 1, diff --git a/src/packages.js b/src/packages.js index bf5a32b..e2870c4 100644 --- a/src/packages.js +++ b/src/packages.js @@ -159,18 +159,22 @@ class Packages { const name = archiveName(url); const target = await realpath(`${options.root}/${name}`, user); - if (await fs.exists(target)) { + if (path.resolve(target) === path.resolve(options.root)) { + throw new Error('Invalid package source'); + } else if (await fs.exists(target)) { throw new Error('Target already exists'); } else if (options.system) { throw new Error('System packages not yet implemented'); } const stream = await fetchSteam(url, options); - await fs.mkdir(target); + + await fs.mkdirp(target); await extract(stream, target); // FIXME: npm packages have a 'package' subdirectory - if (!await fs.exists(path.resolve(target, 'metadata.json'))) { + const exists = await fs.exists(path.resolve(target, 'metadata.json')); + if (!exists) { await fs.remove(target); throw new Error('Invalid package'); From c926aa8c5713393c74fb1cddaedebb9918ae7479 Mon Sep 17 00:00:00 2001 From: Anders Evenrud Date: Sat, 11 Apr 2020 22:35:17 +0200 Subject: [PATCH 13/13] Updated package installation stream handling (#28) --- src/packages.js | 9 +++++---- src/utils/packages.js | 12 ++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/packages.js b/src/packages.js index e2870c4..07ef235 100644 --- a/src/packages.js +++ b/src/packages.js @@ -98,11 +98,11 @@ class Packages { * Loads all packages * @return {Promise} */ - createLoader() { + async createLoader() { let result = []; const {discoveredFile, manifestFile} = this.options; - const discovered = readOrDefault(discoveredFile); - const manifest = readOrDefault(manifestFile); + const discovered = await readOrDefault(discoveredFile); + const manifest = await readOrDefault(manifestFile); const sources = discovered.map(d => path.join(d, 'metadata.json')); logger.info('Using package discovery file', relative(discoveredFile)); @@ -204,6 +204,7 @@ class Packages { const target = await realpath(`${userRoot}/${name}`, user); if (await fs.exists(target)) { + // FIXME: Secure this await fs.remove(target); await this.writeUserManifest(userRoot, user); } else { @@ -226,7 +227,7 @@ class Packages { // TODO: Check conflicts ? const root = await realpath(userRoot, user); const manifest = await realpath(`${userRoot}/metadata.json`, user); - const filenames = await fg(root + '/*/metadata.json'); // FIXME: Windows! + const filenames = await fg(root.replace(/\\/g, '/') + '/*/metadata.json'); const metadatas = await Promise.all(filenames.map(f => fs.readJson(f))); await fs.writeJson(manifest, metadatas); diff --git a/src/utils/packages.js b/src/utils/packages.js index 0a88355..5c4f677 100644 --- a/src/utils/packages.js +++ b/src/utils/packages.js @@ -44,14 +44,14 @@ const fetchSteam = (url, options) => bent()(url, null, { headers: options.headers || {} }); -const readOrDefault = filename => fs.existsSync(filename) - ? fs.readJsonSync(filename) - : []; +const readOrDefault = async (filename) => await fs.exists(filename) + ? fs.readJson(filename) + : Promise.resolve([]); const extract = (stream, target) => new Promise((resolve, reject) => { - stream.once('end', () => resolve()); - stream.once('error', error => reject(error)); - stream.pipe(tar.extract({C: target})); + const s = stream.pipe(tar.extract({C: target})); + s.once('end', () => resolve()); + s.once('error', error => reject(error)); }); module.exports = {