1
0
Fork 0
arangodb/js/client/modules/org/arangodb/foxx-manager.js

1030 lines
28 KiB
JavaScript

/*jslint indent: 2, nomen: true, maxlen: 120, vars: true, white: true, plusplus: true, nonpropdel: true, continue: true, regexp: true */
/*global require, exports */
////////////////////////////////////////////////////////////////////////////////
/// @brief ArangoDB Application Launcher
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2013 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 Jan Steemann
/// @author Dr. Frank Celler
/// @author Copyright 2013, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
var internal = require("internal");
var console = require("console");
var fs = require("fs");
var arangodb = require("org/arangodb");
var arangosh = require("org/arangodb/arangosh");
var errors = arangodb.errors;
var ArangoError = arangodb.ArangoError;
var arango = internal.arango;
var db = arangodb.db;
// -----------------------------------------------------------------------------
// --SECTION-- private functions
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @brief returns the aal collection
////////////////////////////////////////////////////////////////////////////////
function getStorage () {
'use strict';
return db._collection('_aal');
}
////////////////////////////////////////////////////////////////////////////////
/// @brief returns the aal collection
////////////////////////////////////////////////////////////////////////////////
function getFishbowlStorage () {
'use strict';
return db._collection('_fishbowl');
}
////////////////////////////////////////////////////////////////////////////////
/// @brief returns the fishbow repository
////////////////////////////////////////////////////////////////////////////////
function getFishbowlUrl (version) {
'use strict';
return "triAGENS/foxx-apps";
}
////////////////////////////////////////////////////////////////////////////////
/// @brief builds a github repository URL
////////////////////////////////////////////////////////////////////////////////
function buildGithubUrl (repository, version) {
'use strict';
if (typeof version === "undefined") {
version = "master";
}
return 'https://github.com/' + repository + '/archive/' + version + '.zip';
}
////////////////////////////////////////////////////////////////////////////////
/// @brief builds a github repository URL
////////////////////////////////////////////////////////////////////////////////
function buildGithubFishbowlUrl (name) {
'use strict';
return "https://raw.github.com/" + getFishbowlUrl() + "/master/applications/" + name + ".json";
}
////////////////////////////////////////////////////////////////////////////////
/// @brief thrown an error in case a download failed
////////////////////////////////////////////////////////////////////////////////
function throwDownloadError (msg) {
'use strict';
throw new ArangoError({
errorNum: errors.ERROR_APPLICATION_DOWNLOAD_FAILED.code,
errorMessage: errors.ERROR_APPLICATION_DOWNLOAD_FAILED.message + ': ' + String(msg)
});
}
////////////////////////////////////////////////////////////////////////////////
/// @brief thrown an error in case of missing file
////////////////////////////////////////////////////////////////////////////////
function throwFileNoteFound (msg) {
'use strict';
throw new ArangoError({
errorNum: errors.ERROR_FILE_NOT_FOUND.code,
errorMessage: errors.ERROR_FILE_NOT_FOUND.message + ': ' + String(msg)
});
}
////////////////////////////////////////////////////////////////////////////////
/// @brief thrown an error in case of a bad parameter
////////////////////////////////////////////////////////////////////////////////
function throwBadParameter (msg) {
'use strict';
throw new ArangoError({
errorNum: errors.ERROR_BAD_PARAMETER.code,
errorMessage: errors.ERROR_BAD_PARAMETER.message + ': ' + String(msg)
});
}
////////////////////////////////////////////////////////////////////////////////
/// @brief validate an app name and fail if it is invalid
////////////////////////////////////////////////////////////////////////////////
function validateAppName (name) {
'use strict';
if (typeof name === 'string' && name.length > 0) {
return;
}
throw new ArangoError({
errorNum: errors.ERROR_APPLICATION_INVALID_NAME.code,
errorMessage: errors.ERROR_APPLICATION_INVALID_NAME.message + ': ' + String(name)
});
}
////////////////////////////////////////////////////////////////////////////////
/// @brief validate a mount and fail if it is invalid
////////////////////////////////////////////////////////////////////////////////
function validateMount (mnt) {
'use strict';
if (typeof mnt === 'string' && mnt.substr(0, 1) === '/') {
return;
}
throw new ArangoError({
errorNum: errors.ERROR_APPLICATION_INVALID_MOUNT.code,
errorMessage: errors.ERROR_APPLICATION_INVALID_MOUNT.message + ': ' + String(mnt)
});
}
////////////////////////////////////////////////////////////////////////////////
/// @brief extracts the name and version from a manifest file
////////////////////////////////////////////////////////////////////////////////
function extractNameAndVersionManifest (source, filename) {
'use strict';
var manifest = JSON.parse(fs.read(filename));
source.name = manifest.name;
source.version = manifest.version;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief processes files in a directory
////////////////////////////////////////////////////////////////////////////////
function processDirectory (source) {
'use strict';
var location = source.location;
if (! fs.exists(location) || ! fs.isDirectory(location)) {
throwFileNoteFound("'" + String(location) + "' is not a directory");
}
// .............................................................................
// extract name and version from manifest
// .............................................................................
extractNameAndVersionManifest(source, fs.join(location, "manifest.json"));
// .............................................................................
// extract name and version from manifest
// .............................................................................
var tree = fs.listTree(location);
var files = [];
var i;
for (i = 0; i < tree.length; ++i) {
var filename = fs.join(location, tree[i]);
if (fs.isFile(filename)) {
files.push(tree[i]);
}
}
if (files.length === 0) {
throwFileNoteFound("directory '" + String(location) + "' is empty");
}
var tempFile = fs.getTempFile("downloads", false);
source.filename = tempFile;
source.removeFile = true;
fs.zipFile(tempFile, location, files);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief extracts the name and version from a zip
////////////////////////////////////////////////////////////////////////////////
function repackZipFile (source) {
'use strict';
var i;
var filename = source.filename;
var path = fs.getTempFile("zip", false);
fs.unzipFile(filename, path, false, true);
// .............................................................................
// locate the manifest file
// .............................................................................
var tree = fs.listTree(path).sort(function(a,b) { return a.length - b.length; });
var found;
var mf = "manifest.json";
var re = /\/manifest\.json$/;
for (i = 0; i < tree.length && found === undefined; ++i) {
var tf = tree[i];
if (re.test(tf) || tf === mf) {
found = tf;
}
}
if (found === "undefined") {
throwFileNoteFound("cannot find manifest file '" + filename + "'");
}
var mp;
if (found === mf) {
mp = ".";
}
else {
mp = found.substr(0, found.length - mf.length - 1);
}
// .............................................................................
// throw away source file if necessary
// .............................................................................
if (source.removeFile && source.filename !== '') {
try {
fs.remove(source.filename);
}
catch (err1) {
console.warn("cannot remove temporary file '%s'", source.filename);
}
}
delete source.filename;
delete source.removeFile;
// .............................................................................
// repack the zip file
// .............................................................................
var newSource = { location: fs.join(path, mp) };
processDirectory(newSource);
source.name = newSource.name;
source.version = newSource.version;
source.filename = newSource.filename;
source.removeFile = newSource.removeFile;
// .............................................................................
// cleanup temporary paths
// .............................................................................
try {
fs.removeDirectoryRecursive(path);
}
catch (err2) {
console.warn("cannot remove temporary directory '%s'", path);
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief processes files in a zip file
////////////////////////////////////////////////////////////////////////////////
function processZip (source) {
'use strict';
var location = source.location;
if (! fs.exists(location) || ! fs.isFile(location)) {
throwFileNoteFound("cannot find zip file '" + String(location) + "'");
}
source.filename = source.location;
source.removeFile = false;
repackZipFile(source);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief processes files from a github repository
////////////////////////////////////////////////////////////////////////////////
function processGithubRepository (source) {
'use strict';
var url = buildGithubUrl(source.location, source.version);
var tempFile = fs.getTempFile("downloads", false);
try {
var result = internal.download(url, "", { method: "get", followRedirects: true, timeout: 30 }, tempFile);
if (result.code >= 200 && result.code <= 299) {
source.filename = tempFile;
source.removeFile = true;
}
else {
throwDownloadError("could not download from repository '" + url + "'");
}
}
catch (err) {
throwDownloadError("could not download from repository '" + url + "': " + String(err));
}
repackZipFile(source);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief processes a source declaration
////////////////////////////////////////////////////////////////////////////////
function processSource (src) {
'use strict';
if (src.type === "zip") {
processZip(src);
}
else if (src.type === "directory") {
processDirectory(src);
}
else if (src.type === "github") {
processGithubRepository(src);
}
else {
throwBadParameter("unknown application type '" + src.type + "'");
}
// upload file to the server
var response = internal.arango.SEND_FILE("/_api/upload", src.filename);
if (src.removeFile && src.filename !== '') {
try {
fs.remove(src.filename);
}
catch (err2) {
console.warn("cannot remove temporary file '%s'", src.filename);
}
}
if (! response.filename) {
throw new ArangoError({
errorNum: errors.ERROR_APPLICATION_UPLOAD_FAILED.code,
errorMessage: errors.ERROR_APPLICATION_UPLOAD_FAILED.message
+ ": " + String(response.errorMessage)
});
}
return response.filename;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief updates the fishbowl from a zip archive
////////////////////////////////////////////////////////////////////////////////
function updateFishbowlFromZip (filename) {
'use strict';
var i;
var tempPath = fs.getTempPath();
var fishbowl = getFishbowlStorage();
try {
fs.makeDirectoryRecursive(tempPath);
fs.unzipFile(filename, tempPath, false, true);
var root = fs.join(tempPath, "foxx-apps-master/applications");
if (! fs.exists(root)) {
throw new Error("applications diectory is missing in foxx-apps, giving up");
}
var m = fs.listTree(root);
var reSub = /(.*)\.json$/;
for (i = 0; i < m.length; ++i) {
var f = m[i];
var match = reSub.exec(f);
if (match === null) {
continue;
}
var app = fs.join(root, f);
var desc;
try {
desc = JSON.parse(fs.read(app));
}
catch (err1) {
console.error("cannot parse description for app '" + f + "': %s", String(err1));
continue;
}
desc._key = match[1];
if (! desc.hasOwnProperty("name")) {
desc.name = match[1];
}
try {
try {
fishbowl.save(desc);
}
catch (err3) {
fishbowl.replace(desc._key, desc);
}
}
catch (err2) {
console.error("cannot save description for app '" + f + "': %s", String(err2));
continue;
}
}
}
catch (err) {
if (tempPath !== undefined && tempPath !== "") {
fs.removeDirectoryRecursive(tempPath);
}
throw err;
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief downloads the fishbowl repository
////////////////////////////////////////////////////////////////////////////////
function updateFishbowl () {
'use strict';
var i;
var url = buildGithubUrl(getFishbowlUrl());
var filename = fs.getTempFile("downloads", false);
var path = fs.getTempFile("zip", false);
try {
var result = internal.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);
}
fs.removeDirectoryRecursive(path);
throw err;
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief comparator for applications
////////////////////////////////////////////////////////////////////////////////
function compareApps (l, r) {
'use strict';
var left = l.name.toLowerCase(), right = r.name.toLowerCase();
if (left < right) {
return -1;
}
if (right < left) {
return 1;
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief prints out usage message for the command-line tool
////////////////////////////////////////////////////////////////////////////////
function cmdUsage () {
var printf = arangodb.printf;
var fm = "foxx-manager";
printf("Example usage:\n");
printf("%s install <foxx> <mount-point>\n", fm);
printf("%s uninstall <mount-point>\n\n", fm);
printf("Further help:\n");
printf("%s help\n", fm);
}
// -----------------------------------------------------------------------------
// --SECTION-- public functions
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @brief command line dispatcher
////////////////////////////////////////////////////////////////////////////////
exports.run = function (args) {
if (typeof args === 'undefined' || args.length === 0) {
console.error("expecting a command, please try: ");
cmdUsage();
return;
}
var type = args[0];
try {
if (type === 'fetch') {
exports.fecth(args[1], args[2], args[3]);
}
else if (type === 'mount') {
if (3 < args.length) {
exports.mount(args[1], args[2], JSON.parse(args[3]));
}
else {
exports.mount(args[1], args[2]);
}
}
else if (type === 'unmount') {
exports.unmount(args[1]);
}
else if (type === 'uninstall') {
exports.uninstall(args[1]);
}
else if (type === 'list') {
if (1 < args.length && args[1] === "prefix") {
exports.list(true);
}
else {
exports.list();
}
}
else if (type === 'fetched') {
exports.fetched();
}
else if (type === 'available') {
exports.available();
}
else if (type === 'info') {
exports.info(args[1]);
}
else if (type === 'search') {
exports.search(args[1]);
}
else if (type === 'update') {
exports.update();
}
else {
console.error("unknown command '%s', please try: ", type);
cmdUsage();
return;
}
}
catch (err) {
if (err instanceof ArangoError) {
console.error("%s", err.errorMessage);
}
else {
console.error("%s", err.message);
}
return;
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief fetches a FOXX application
////////////////////////////////////////////////////////////////////////////////
exports.fetch = function (type, location, version) {
'use strict';
var usage = ", usage: fetch(<type>, <location>, [<version>])";
if (typeof type === "undefined") {
throwBadParameter("type missing" + usage);
}
if (typeof location === "undefined") {
throwBadParameter("location missing" + usage);
}
var source = { type: type, location: location, version: version };
var filename = processSource(source);
if (typeof source.name === "undefined") {
throwBadParameter("name missing for '" + JSON.stringify(source) + "'");
}
if (typeof source.version === "undefined") {
throwBadParameter("version missing for '" + JSON.stringify(source) + "'");
}
var req = {
name: source.name,
version: source.version,
filename: filename
};
var res = arango.POST("/_admin/foxx/fetch", JSON.stringify(req));
arangosh.checkRequestResult(res);
return { path: res.path };
};
////////////////////////////////////////////////////////////////////////////////
/// @brief mounts a FOXX application
////////////////////////////////////////////////////////////////////////////////
exports.mount = function (appId, mount, options) {
'use strict';
var usage = ", usage: mount(<appId>, <mount>, [<options>])";
if (typeof appId === "undefined") {
throwBadParameter("appId missing" + usage);
}
if (typeof mount === "undefined") {
throwBadParameter("mount missing" + usage);
}
var req = {
appId: appId,
mount: mount,
options: options
};
validateAppName(appId);
validateMount(mount);
var res = arango.POST("/_admin/foxx/mount", JSON.stringify(req));
arangosh.checkRequestResult(res);
return { appId: res.appId, mountId: res.mountId };
};
////////////////////////////////////////////////////////////////////////////////
/// @brief unmounts a FOXX application
////////////////////////////////////////////////////////////////////////////////
exports.unmount = exports.uninstall = function (key) {
'use strict';
var usage = ", usage: unmount(<mount>)";
if (typeof key === "undefined") {
throwBadParameter("mount point or mount key missing" + usage);
}
var req = {
key: key
};
validateAppName(key);
var res = arango.POST("/_admin/foxx/unmount", JSON.stringify(req));
arangosh.checkRequestResult(res);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief lists all installed FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.listRaw = function (showPrefix) {
'use strict';
var aal = getStorage();
var cursor = aal.byExample({ type: "mount" });
var result = [];
while (cursor.hasNext()) {
var doc = cursor.next();
var res;
if (showPrefix) {
res = {
MountID: doc._key,
AppID: doc.app,
CollectionPrefix: doc.collectionPrefix,
Active: doc.active ? "yes" : "no"
};
}
else {
res = {
MountID: doc._key,
AppID: doc.app,
Mount: doc.mount,
Active: doc.active ? "yes" : "no"
};
}
result.push(res);
}
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief prints all installed FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.list = function (showPrefix) {
'use strict';
var list = exports.listRaw(showPrefix);
if (showPrefix) {
arangodb.printTable(
list,
["MountID", "AppID", "CollectionPrefix", "Active"]);
}
else {
arangodb.printTable(
list,
["MountID", "AppID", "Mount", "Active"]);
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief lists all fetched FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.fetchedRaw = function () {
'use strict';
var aal = getStorage();
var cursor = aal.byExample({ type: "app" });
var result = [];
while (cursor.hasNext()) {
var doc = cursor.next();
var res = {
AppID: doc.app,
Name: doc.name,
Description: doc.description || "",
Version: doc.version,
Path: doc.path
};
result.push(res);
}
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief prints all fetched FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.fetched = function () {
'use strict';
var list = exports.fetchedRaw();
arangodb.printTable(
list,
["AppID", "Name", "Description", "Version", "Path"]);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief lists all available FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.availableRaw = function () {
'use strict';
var fishbowl = getFishbowlStorage();
var cursor = fishbowl.all();
var result = [];
while (cursor.hasNext()) {
var doc = cursor.next();
var res = {
name: doc.name,
description: doc.description || "",
author: doc.author
};
result.push(res);
}
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief prints all available FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.available = function () {
'use strict';
var list = exports.availableRaw();
if (list.length === 0) {
arangodb.print("Repository is empty, please use 'update'");
}
else {
arangodb.printTable(
list.sort(compareApps),
[ "name", "author", "description" ]);
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief info for an available FOXX application
////////////////////////////////////////////////////////////////////////////////
exports.info = function (name) {
'use strict';
validateAppName(name);
var fishbowl = getFishbowlStorage();
if (fishbowl.count() === 0) {
arangodb.print("Repository is empty, please use 'update'");
return;
}
var desc;
try {
desc = fishbowl.document(name);
}
catch (err) {
arangodb.print("No '" + name + "' available, please try 'search'");
return;
}
internal.printf("Name: %s\n", desc.name);
if (desc.hasOwnProperty('author')) {
internal.printf("Author: %s\n", desc.author);
}
if (desc.hasOwnProperty('description')) {
internal.printf("\nDescription:\n%s\n\n", desc.description);
}
var header = false;
var i;
for (i in desc.versions) {
if (desc.versions.hasOwnProperty(i)) {
var v = desc.versions[i];
if (! header) {
arangodb.print("Versions:");
header = true;
}
if (v.type === "github") {
if (v.hasOwnProperty("tag")) {
internal.printf('%s: fetch github "%s" "%s"\n', i, v.location, v.tag);
}
else if (v.hasOwnProperty("branch")) {
internal.printf('%s: fetch github "%s" "%s"\n', i, v.location, v.branch);
}
else {
internal.printf('%s: fetch "github" "%s"\n', i, v.location);
}
}
}
}
internal.printf("\n");
};
////////////////////////////////////////////////////////////////////////////////
/// @brief searchs for an available FOXX applications
////////////////////////////////////////////////////////////////////////////////
exports.search = function (name) {
'use strict';
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 {
// 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)) {
continue;
}
docs.push(docs2[i]);
}
}
arangodb.printTable(
docs.sort(compareApps),
[ "name", "author", "description" ]);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief updates the repository
////////////////////////////////////////////////////////////////////////////////
exports.update = updateFishbowl;
////////////////////////////////////////////////////////////////////////////////
/// @brief sets up a FOXX dev application
////////////////////////////////////////////////////////////////////////////////
exports.devSetup = function (name) {
'use strict';
var res;
var req = {
name: name
};
res = arango.POST("/_admin/foxx/dev-setup", JSON.stringify(req));
arangosh.checkRequestResult(res);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief tears down a FOXX dev application
////////////////////////////////////////////////////////////////////////////////
exports.devTeardown = function (name) {
'use strict';
var res;
var req = {
name: name
};
res = arango.POST("/_admin/foxx/dev-teardown", JSON.stringify(req));
arangosh.checkRequestResult(res);
};
// -----------------------------------------------------------------------------
// --SECTION-- END-OF-FILE
// -----------------------------------------------------------------------------
// Local Variables:
// mode: outline-minor
// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint"
// End: