'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 //////////////////////////////////////////////////////////////////////////////// var checkedFishBowl = false; const arangodb = require("@arangodb"); const plainServerVersion = arangodb.plainServerVersion; const db = arangodb.db; const download = require("internal").download; const fs = require("fs"); const throwDownloadError = arangodb.throwDownloadError; 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 }); } if (c !== null && ! checkedFishBowl) { // ensure indexes c.ensureFulltextIndex("description"); c.ensureFulltextIndex("name"); checkedFishBowl = true; } 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: { write: 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() } }); arangodb.printf("Updated local repository information with %d service(s)\n", toSave.length); } } 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 = fishbowl.fulltext("description", "prefix:" + 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= fishbowl.fulltext("name", "prefix:" + 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 }; result.push(res); } } return result; } //////////////////////////////////////////////////////////////////////////////// /// @brief updates the repository //////////////////////////////////////////////////////////////////////////////// var update = function() { var url = utils.buildGithubUrl(getFishbowlUrl()); var filename = fs.getTempFile("downloads", false); var path = fs.getTempFile("zip", false); try { var result = download(url, "", { method: "get", followRedirects: true, timeout: 30 }, filename); if (result.code < 200 || result.code > 299) { throwDownloadError("Github download from '" + url + "' failed with error code " + result.code); } updateFishbowlFromZip(filename); filename = undefined; } catch (err) { if (filename !== undefined && fs.exists(filename)) { fs.remove(filename); } try { fs.removeDirectoryRecursive(path); } catch (ignore) { } throw err; } }; //////////////////////////////////////////////////////////////////////////////// /// @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) { utils.validateServiceName(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 === undefined) { throw new Error('Service not found'); } let versions = storeInfo.versions; let versionInfo; if (version === undefined) { 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;