1
0
Fork 0
arangodb/js/common/modules/@arangodb/foxx/store.js

539 lines
15 KiB
JavaScript

'use strict';
// //////////////////////////////////////////////////////////////////////////////
// / @brief Foxx service store
// /
// / @file
// /
// / DISCLAIMER
// /
// / Copyright 2015 triagens GmbH, Cologne, Germany
// /
// / 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.
// /
// / Copyright holder is triAGENS GmbH, Cologne, Germany
// /
// / @author Dr. Frank Celler, Michael Hackstein
// / @author Copyright 2015, triAGENS GmbH, Cologne, Germany
// //////////////////////////////////////////////////////////////////////////////
const dd = require('dedent');
const arangodb = require('@arangodb');
const plainServerVersion = arangodb.plainServerVersion;
const db = arangodb.db;
const errors = arangodb.errors;
const ArangoError = arangodb.ArangoError;
const download = require('internal').download;
const fs = require('fs');
const utils = require('@arangodb/foxx/manager-utils');
const semver = require('semver');
// //////////////////////////////////////////////////////////////////////////////
// / @brief returns the fishbowl repository
// //////////////////////////////////////////////////////////////////////////////
function getFishbowlUrl () {
return 'arangodb/foxx-apps';
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief returns the fishbowl collection
// /
// / this will create the collection if it does not exist. this is better than
// / needlessly creating the collection for each database in case it is not
// / used in context of the database.
// //////////////////////////////////////////////////////////////////////////////
var getFishbowlStorage = function () {
var c = db._collection('_fishbowl');
if (c === null) {
c = db._create('_fishbowl', { isSystem: true, distributeShardsLike: '_graphs' });
}
return c;
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief comparator for services
// //////////////////////////////////////////////////////////////////////////////
var compareServices = function (l, r) {
var left = l.name.toLowerCase();
var right = r.name.toLowerCase();
if (left < right) {
return -1;
}
if (right < left) {
return 1;
}
return 0;
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief updates the fishbowl from a zip archive
// //////////////////////////////////////////////////////////////////////////////
var updateFishbowlFromZip = function (filename) {
var i;
var tempPath = fs.getTempPath();
var toSave = [];
try {
fs.makeDirectoryRecursive(tempPath);
var root = fs.join(tempPath, 'foxx-apps-master/applications');
// remove any previous files in the directory
fs.listTree(root).forEach(function (file) {
if (file.match(/\.json$/)) {
try {
fs.remove(fs.join(root, file));
} catch (ignore) {}
}
});
fs.unzipFile(filename, tempPath, false, true);
if (!fs.exists(root)) {
throw new Error("'applications' directory is missing in foxx-apps-master, giving up");
}
var m = fs.listTree(root);
var reSub = /(.*)\.json$/;
var f, match, service, desc;
for (i = 0; i < m.length; ++i) {
f = m[i];
match = reSub.exec(f);
if (match === null) {
continue;
}
service = fs.join(root, f);
try {
desc = JSON.parse(fs.read(service));
} catch (err1) {
arangodb.printf("Cannot parse description for service '" + f + "': %s\n", String(err1));
continue;
}
desc._key = match[1];
if (!desc.hasOwnProperty('name')) {
desc.name = match[1];
}
toSave.push(desc);
}
if (toSave.length > 0) {
var fishbowl = getFishbowlStorage();
db._executeTransaction({
collections: {
exclusive: fishbowl.name()
},
action: function (params) {
var c = require('internal').db._collection(params.collection);
c.truncate();
params.services.forEach(function (service) {
c.save(service);
});
},
params: {
services: toSave,
collection: fishbowl.name()
}
});
require('console').debug('Updated local Foxx repository with ' + toSave.length + ' service(s)');
}
} catch (err) {
if (tempPath !== undefined && tempPath !== '') {
try {
fs.removeDirectoryRecursive(tempPath);
} catch (ignore) {}
}
throw err;
}
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief returns the search result for Foxx services
// //////////////////////////////////////////////////////////////////////////////
var searchJson = function (name) {
var fishbowl = getFishbowlStorage();
if (fishbowl.count() === 0) {
arangodb.print("Repository is empty, please use 'update'");
return [];
}
var docs;
if (name === undefined || (typeof name === 'string' && name.length === 0)) {
docs = fishbowl.toArray();
} else {
name = name.replace(/[^a-zA-Z0-9]/g, ' ');
// get results by looking in "description" attribute
docs = db._query('FOR doc IN @@collection FILTER CONTAINS(doc.description, @name) RETURN doc', { name, '@collection': fishbowl.name() }).toArray();
// build a hash of keys
var i;
var keys = { };
for (i = 0; i < docs.length; ++i) {
keys[docs[i]._key] = 1;
}
// get results by looking in "name" attribute
var docs2 = db._query('FOR doc IN @@collection FILTER CONTAINS(doc.name, @name) RETURN doc', { name, '@collection': fishbowl.name() }).toArray();
// merge the two result sets, avoiding duplicates
for (i = 0; i < docs2.length; ++i) {
if (!keys.hasOwnProperty(docs2[i]._key)) {
docs.push(docs2[i]);
}
}
}
return docs;
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief searchs for an available Foxx services
// //////////////////////////////////////////////////////////////////////////////
var search = function (name) {
var docs = searchJson(name);
arangodb.printTable(
docs.sort(compareServices),
[ 'name', 'author', 'description' ],
{
prettyStrings: true,
totalString: '%s service(s) found',
emptyString: 'no services found',
rename: {
name: 'Name',
author: 'Author',
description: 'Description'
}
}
);
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief extracts the highest version number from the document
// //////////////////////////////////////////////////////////////////////////////
function extractMaxVersion (matchEngine, versionDoc) {
let serverVersion = plainServerVersion();
let versions = Object.keys(versionDoc);
versions.sort(semver.compare).reverse();
let fallback;
for (let version of versions) {
let info = versionDoc[version];
if (!info.engines || Object.keys(info.engines).length === 0) {
// No known compatibility requirements indicated: use as last resort
if (!matchEngine) {
return version;
}
if (!fallback) {
fallback = version;
}
continue;
}
let versionRange = info.engines.arangodb;
if (!versionRange || semver.outside(serverVersion, versionRange, '<')) {
// Explicitly backwards-incompatible with the server version: ignore
continue;
}
if (!matchEngine || semver.satisfies(serverVersion, versionRange)) {
return version;
}
}
return fallback;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief returns all available Foxx services
// //////////////////////////////////////////////////////////////////////////////
function availableJson (matchEngine) {
let fishbowl = getFishbowlStorage();
let cursor = fishbowl.all();
let result = [];
while (cursor.hasNext()) {
let doc = cursor.next();
let latestVersion = extractMaxVersion(matchEngine, doc.versions);
if (latestVersion) {
let serverVersion = plainServerVersion();
let versionInfo = doc.versions[latestVersion];
let legacy = Boolean(
versionInfo.engines &&
versionInfo.engines.arangodb &&
!semver.satisfies(serverVersion, versionInfo.engines.arangodb)
);
let res = {
name: doc.name,
description: doc.description || '',
author: doc.author || '',
latestVersion,
legacy,
location: doc.versions[latestVersion].location,
license: doc.license,
categories: doc.keywords
};
result.push(res);
}
}
return result;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief updates the repository
// //////////////////////////////////////////////////////////////////////////////
var update = function () {
let url = utils.buildGithubUrl(getFishbowlUrl());
let filename = fs.getTempFile('bundles', false);
let internal = require('internal');
if (internal.isFoxxStoreDisabled && internal.isFoxxStoreDisabled()) {
throw new Error('Foxx store is disabled in configuration');
}
try {
var result = download(url, '', {
method: 'get',
followRedirects: true,
timeout: 15
}, filename);
if (result.code < 200 || result.code > 299) {
throw new ArangoError({
errorNum: errors.ERROR_SERVICE_SOURCE_ERROR.code,
errorMessage: dd`
${errors.ERROR_SERVICE_SOURCE_ERROR.message}
URL: ${url}
Status Code: ${result.code}
`
});
}
updateFishbowlFromZip(filename);
filename = undefined;
} catch (e) {
if (filename !== undefined && fs.exists(filename)) {
fs.remove(filename);
}
throw Object.assign(
new Error('Failed to update Foxx store'),
{cause: e}
);
}
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief prints all available Foxx services
// //////////////////////////////////////////////////////////////////////////////
var available = function (matchEngine) {
var list = availableJson(matchEngine);
arangodb.printTable(
list.sort(compareServices),
[ 'name', 'author', 'description', 'latestVersion' ],
{
prettyStrings: true,
totalString: '%s service(s) found',
emptyString: "no services found, please use 'update'",
rename: {
'name': 'Name',
'author': 'Author',
'description': 'Description',
'latestVersion': 'Latest Version'
}
}
);
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief gets json-info for an available Foxx service
// //////////////////////////////////////////////////////////////////////////////
var infoJson = function (name) {
var fishbowl = getFishbowlStorage();
if (fishbowl.count() === 0) {
arangodb.print("Repository is empty, please use 'update'");
return;
}
var desc = db._query(
'FOR u IN @@storage FILTER u.name == @name OR @name in u.aliases RETURN DISTINCT u',
{ '@storage': fishbowl.name(), 'name': name }).toArray();
if (desc.length === 0) {
arangodb.print("No service '" + name + "' available, please try 'search'");
return;
} else if (desc.length > 1) {
arangodb.print("Multiple services are named '" + name + "', please try 'search'");
return;
} else {
return desc[0];
}
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief create a download URL for the given service information
// //////////////////////////////////////////////////////////////////////////////
var installationInfo = function (serviceInfo) {
// TODO Validate
let infoSplit = serviceInfo.split(':');
let name = infoSplit[0];
let version = infoSplit[1];
let storeInfo = infoJson(name);
if (!storeInfo) {
return null;
}
let versions = storeInfo.versions;
let versionInfo;
if (!version) {
let maxVersion = extractMaxVersion(true, versions);
if (!maxVersion) {
maxVersion = extractMaxVersion(false, versions);
}
if (!maxVersion) {
throw new Error('No available version');
}
versionInfo = versions[maxVersion];
} else {
if (!versions.hasOwnProperty(version)) {
throw new Error('Unknown version');
}
versionInfo = versions[version];
}
let url;
if (!versionInfo.type) {
url = versionInfo.location;
} else if (versionInfo.type === 'github') {
url = utils.buildGithubUrl(versionInfo.location, versionInfo.tag);
} else {
throw new Error(`Unknown location type ${versionInfo.type}`);
}
const manifest = {name};
if (serviceInfo.author) {
manifest.author = serviceInfo.author;
}
if (serviceInfo.description) {
manifest.description = serviceInfo.description;
}
if (serviceInfo.license) {
manifest.license = serviceInfo.license;
}
if (serviceInfo.keywords) {
manifest.keywords = serviceInfo.keywords;
}
if (versionInfo.engines) {
manifest.engines = versionInfo.engines;
}
if (versionInfo.provides) {
manifest.provides = versionInfo.provides;
}
return {url, manifest};
};
// //////////////////////////////////////////////////////////////////////////////
// / @brief prints info for an available Foxx service
// //////////////////////////////////////////////////////////////////////////////
var info = function (name) {
var desc = infoJson(name);
arangodb.printf('Name: %s\n', desc.name);
if (desc.hasOwnProperty('author')) {
arangodb.printf('Author: %s\n', desc.author);
}
var isSystem = desc.hasOwnProperty('isSystem') && desc.isSystem;
arangodb.printf('System: %s\n', JSON.stringify(isSystem));
if (desc.hasOwnProperty('description')) {
arangodb.printf('Description: %s\n\n', desc.description);
}
var header = false;
var versions = Object.keys(desc.versions);
versions.sort(semver.compare);
versions.forEach(function (v) {
var version = desc.versions[v];
if (!header) {
arangodb.print('Versions:');
header = true;
}
if (version.type === 'github') {
if (version.hasOwnProperty('tag')) {
arangodb.printf('%s: fetch github "%s" "%s"\n', v, version.location, version.tag);
} else if (v.hasOwnProperty('branch')) {
arangodb.printf('%s: fetch github "%s" "%s"\n', v, version.location, version.branch);
} else {
arangodb.printf('%s: fetch "github" "%s"\n', v, version.location);
}
}
});
arangodb.printf('\n');
};
exports.available = available;
exports.availableJson = availableJson;
exports.installationInfo = installationInfo;
exports.getFishbowlStorage = getFishbowlStorage;
exports.info = info;
exports.search = search;
exports.searchJson = searchJson;
exports.update = update;