mirror of https://gitee.com/bigwinds/arangodb
539 lines
15 KiB
JavaScript
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;
|