Skip to content

Commit

Permalink
Add version overview (#66)
Browse files Browse the repository at this point in the history
* Added tests for Alias class

* Added tests for HttpIncoming class

* Add tests for HttpOutgoing class

* Added tests for Meta class

* Added version overview
  • Loading branch information
trygve-lie authored Dec 9, 2019
1 parent af328b8 commit 9505b4a
Show file tree
Hide file tree
Showing 18 changed files with 696 additions and 69 deletions.
6 changes: 4 additions & 2 deletions doc/file-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ The asset service stores files in the following structure:
├── map
│   └── :name
│   ├── :version.import-map.json
│   └── :major.alias.json
│   ├── :major.alias.json
│ └── versions.json
└── pkg
└── :name
├── :version
│   ├── *
├── :version.package.json
└── :major.alias.json
├── :major.alias.json
└── versions.json
```

Parameters:
Expand Down
79 changes: 78 additions & 1 deletion doc/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Status codes:
- `303` if module is successfully uploaded. `location` is root of module
- `400` if validation in URL parameters or form fields fails
- `401` if user is not authorized
- `409` if module already exist
- `409` if module already exist or version in a major range is not newer than previous version in a major range
- `415` if file format of the uploaded file is unsupported
- `502` if package could not be written to the sink

Expand All @@ -76,6 +76,58 @@ Example:
curl -X PUT -i -F [email protected] http://localhost:4001/finn/pkg/fuzz/8.4.1
```

### Latest Package versions

**Method:** `GET`

Retrieves an overview of the latest major versions of a package.

```bash
https://:assetServerUrl:port/:org/pkg/:name
```

URL parameters:

- `:org` is the name of your organisation. Validator: [`^[a-zA-Z0-9_-]+$`](https://regexper.com/#%5E%5Ba-zA-Z0-9_-%5D%2B%24).
- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name).

Status codes:

- `200` if file is successfully retrieved
- `404` if file is not found

Example:

```bash
curl -X GET http://localhost:4001/finn/pkg/fuzz
```

### Package version overview

**Method:** `GET`

Retrieves an overview of the files of a package version.

```bash
https://:assetServerUrl:port/:org/pkg/:name/:version
```

URL parameters:

- `:org` is the name of your organisation. Validator: [`^[a-zA-Z0-9_-]+$`](https://regexper.com/#%5E%5Ba-zA-Z0-9_-%5D%2B%24).
- `:name` is the name of the package. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name).
- `:version` is the version of the package. Validator: Comply with [semver validation regex](https://semver.org/).

Status codes:

- `200` if file is successfully retrieved
- `404` if file is not found

Example:

```bash
curl -X GET http://localhost:4001/finn/pkg/fuzz
```

## Import Maps

Expand Down Expand Up @@ -151,6 +203,31 @@ Example:
curl -X PUT -i -F [email protected] http://localhost:4001/finn/map/buzz/8.4.1
```

### Latest Import Map versions

**Method:** `GET`

Retrieves an overview of the latest versions of a Import Map.

```bash
https://:assetServerUrl:port/:org/map/:name
```

URL parameters:

- `:org` is the name of your organisation. Validator: [`^[a-zA-Z0-9_-]+$`](https://regexper.com/#%5E%5Ba-zA-Z0-9_-%5D%2B%24).
- `:name` is the name of the import map. Validator: Comply with [npm package names](https://github.com/npm/validate-npm-package-name).

Status codes:

- `200` if file is successfully retrieved
- `404` if file is not found

Example:

```bash
curl -X GET http://localhost:4001/finn/map/buzz
```

## Aliases

Expand Down
17 changes: 16 additions & 1 deletion lib/classes/http-outgoing.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
'use strict';

const { isReadableStream } = require('../utils/utils');

const STATUS_CODES = [
100, 101, 102, 103,
200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
300, 301, 302, 303, 304, 305, 306, 307, 308,
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511,
];

const HttpOutgoing = class HttpOutgoing {
constructor() {
this._statusCode = 200;
Expand All @@ -11,7 +21,11 @@ const HttpOutgoing = class HttpOutgoing {
}

set statusCode(value) {
this._statusCode = value;
if (Number.isInteger(value) && STATUS_CODES.includes(value)) {
this._statusCode = value;
return;
}
throw new Error('Value is not a legal http status code');
}

get statusCode() {
Expand All @@ -35,6 +49,7 @@ const HttpOutgoing = class HttpOutgoing {
}

set stream(value) {
if (!isReadableStream(value)) throw new Error('Value is not a Readable stream');
this._stream = value;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/classes/meta.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const Meta = class Meta {
construct({
constructor({
value = '',
name = '',
} = {}) {
Expand Down
64 changes: 64 additions & 0 deletions lib/classes/versions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

const semver = require('semver');

const Versions = class Versions {
constructor({ versions = [], name = '', org = '' } = {}) {
this._versions = new Map(versions);
this._name = name;
this._org = org;
}

get versions() {
return Array.from(this._versions.entries()).sort((a, b) => {
return a[0] > b[0] ? -1 : 1;
});
}

get name() {
return this._name;
}

get org() {
return this._org;
}

setVersion(version, integrity) {
if (!this.check(version)) {
throw new Error('Semver version is lower than previous version');
}
const major = semver.major(version);
this._versions.set(major, {
version,
integrity,
});
}

getVersion(major) {
return this._versions.get(major);
}

check(version) {
const major = semver.major(version);
const previous = this.getVersion(major);
if (previous) {
if (semver.gte(previous.version, version)) {
return false;
}
}
return true;
}

toJSON() {
return {
versions: this.versions,
name: this.name,
org: this.org,
};
}

get [Symbol.toStringTag]() {
return 'Versions';
}
}
module.exports = Versions;
72 changes: 54 additions & 18 deletions lib/handlers/map.put.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
const { validators } = require('@eik/common');
const HttpError = require('http-errors');
const Busboy = require('busboy');
const crypto = require('crypto');
const abslog = require('abslog');

const { createFilePathToImportMap } = require('../utils/path-builders-fs');
const { createFilePathToImportMap, createFilePathToVersion } = require('../utils/path-builders-fs');
const { createURIPathToImportMap } = require('../utils/path-builders-uri');
const HttpIncoming = require('../classes/http-incoming');
const HttpOutgoing = require('../classes/http-outgoing');
const Versions = require('../classes/versions');
const utils = require('../utils/utils');
const conf = require('../utils/defaults');

Expand All @@ -21,7 +23,6 @@ const MapPut = class MapPut {

_parser(incoming) {
return new Promise((resolve, reject) => {
const pathname = createURIPathToImportMap(incoming);
const path = createFilePathToImportMap(incoming);

const busboy = new Busboy({
Expand All @@ -42,11 +43,14 @@ const MapPut = class MapPut {
return;
}

const hasher = crypto.createHash('sha512');

// Buffer up the incoming file and check if we can
// parse it as JSON or not.
let obj = {};
try {
const str = await utils.streamCollector(file);
hasher.update(str);
obj = JSON.parse(str);
} catch (error) {
this._log.error(`map:put - Import map can not be parsed`);
Expand All @@ -66,35 +70,51 @@ const MapPut = class MapPut {
return;
}
this._log.info(`map:put - Successfully wrote import map to sink - Pathname: ${path}`);
});

busboy.on('finish', () => {
const outgoing = new HttpOutgoing();
outgoing.mimeType = 'text/plain';
outgoing.statusCode = 303;
outgoing.location = pathname;
resolve(outgoing);
const integrity = `sha512-${hasher.digest('base64')}`;

busboy.emit('completed', integrity);
});

busboy.on('error', error => {
reject(error);
});

busboy.once('completed', (integrity) => {
resolve(integrity);
});

// If incoming.request is handeled by stream.pipeline, it will
// close to early for the http framework to handle it. Let the
// http framework handle closing incoming.request
incoming.request.pipe(busboy);
});
}

async _exist (incoming) {
async _readVersions (incoming) {
const path = createFilePathToVersion(incoming);
let versions;
try {
const path = createFilePathToImportMap(incoming);
await this._sink.exist(path);
return true;
const obj = await utils.readJSON(this._sink, path);
versions = new Versions(obj);
this._log.info(`map:put - Successfully read version meta file from sink - Pathname: ${path}`);
} catch (error) {
return false;
// File does not exist, its probably a new package
versions = new Versions(incoming);
this._log.info(`map:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`);
}
return versions;
}

async _writeVersions (incoming, versions) {
const path = createFilePathToVersion(incoming);
await utils.writeJSON(
this._sink,
path,
versions,
'application/json'
);
this._log.info(`map:put - Successfully wrote version meta file to sink - Pathname: ${path}`);
}

async handler (req, org, name, version) {
Expand All @@ -108,18 +128,34 @@ const MapPut = class MapPut {
}

const incoming = new HttpIncoming(req, {
type: 'map',
version,
name,
org,
});

const exist = await this._exist(incoming);
if (exist) {
this._log.info(`map:put - Import map exists - Org: ${org} - Name: ${name} - Version: ${version}`);
const versions = await this._readVersions(incoming);

if (!versions.check(version)) {
this._log.info(`map:put - Semver version is lower than previous version of the package - Org: ${org} - Name: ${name} - Version: ${version}`);
throw new HttpError.Conflict();
}

const outgoing = await this._parser(incoming);
const integrity = await this._parser(incoming);

versions.setVersion(version, integrity);

try {
await this._writeVersions(incoming, versions);
} catch(error) {
throw new HttpError.BadGateway();
}

const outgoing = new HttpOutgoing();
outgoing.mimeType = 'text/plain';
outgoing.statusCode = 303;
outgoing.location = createURIPathToImportMap(incoming);

return outgoing;
}
}
Expand Down
Loading

0 comments on commit 9505b4a

Please sign in to comment.