2012-11-07 16:25:09 -08:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This file contains a client API for the Bagheera data storage service.
|
|
|
|
*
|
|
|
|
* Information about Bagheera is available at
|
|
|
|
* https://github.com/mozilla-metrics/bagheera
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2013-01-27 11:26:48 -08:00
|
|
|
#ifndef MERGED_COMPARTMENT
|
|
|
|
|
2012-11-07 16:25:09 -08:00
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
|
|
"BagheeraClient",
|
|
|
|
"BagheeraClientRequestResult",
|
|
|
|
];
|
|
|
|
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
|
2013-01-27 11:26:48 -08:00
|
|
|
#endif
|
|
|
|
|
2013-02-01 11:43:15 -08:00
|
|
|
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
|
2012-12-05 14:36:27 -08:00
|
|
|
Cu.import("resource://services-common/log4moz.js");
|
2013-02-05 20:25:57 -08:00
|
|
|
Cu.import("resource://services-common/rest.js");
|
2012-12-05 14:36:27 -08:00
|
|
|
Cu.import("resource://services-common/utils.js");
|
2012-11-07 16:25:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents the result of a Bagheera request.
|
|
|
|
*/
|
|
|
|
this.BagheeraClientRequestResult = function BagheeraClientRequestResult() {
|
|
|
|
this.transportSuccess = false;
|
|
|
|
this.serverSuccess = false;
|
|
|
|
this.request = null;
|
2013-02-05 20:25:57 -08:00
|
|
|
};
|
2012-11-07 16:25:09 -08:00
|
|
|
|
|
|
|
Object.freeze(BagheeraClientRequestResult.prototype);
|
|
|
|
|
2013-02-05 20:25:57 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Wrapper around RESTRequest so logging is sane.
|
|
|
|
*/
|
|
|
|
function BagheeraRequest(uri) {
|
|
|
|
RESTRequest.call(this, uri);
|
|
|
|
|
|
|
|
this._log = Log4Moz.repository.getLogger("Services.BagheeraClient");
|
|
|
|
this._log.level = Log4Moz.Level.Debug;
|
|
|
|
}
|
|
|
|
|
|
|
|
BagheeraRequest.prototype = Object.freeze({
|
|
|
|
__proto__: RESTRequest.prototype,
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2012-11-07 16:25:09 -08:00
|
|
|
/**
|
|
|
|
* Create a new Bagheera client instance.
|
|
|
|
*
|
|
|
|
* Each client is associated with a specific Bagheera HTTP URI endpoint.
|
|
|
|
*
|
|
|
|
* @param baseURI
|
|
|
|
* (string) The base URI of the Bagheera HTTP endpoint.
|
|
|
|
*/
|
|
|
|
this.BagheeraClient = function BagheeraClient(baseURI) {
|
|
|
|
if (!baseURI) {
|
|
|
|
throw new Error("baseURI argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
this._log = Log4Moz.repository.getLogger("Services.BagheeraClient");
|
2013-02-05 20:25:57 -08:00
|
|
|
this._log.level = Log4Moz.Level.Debug;
|
2012-11-07 16:25:09 -08:00
|
|
|
|
|
|
|
this.baseURI = baseURI;
|
|
|
|
|
|
|
|
if (!baseURI.endsWith("/")) {
|
|
|
|
this.baseURI += "/";
|
|
|
|
}
|
2013-02-05 20:25:57 -08:00
|
|
|
};
|
2012-11-07 16:25:09 -08:00
|
|
|
|
2013-02-05 20:25:57 -08:00
|
|
|
BagheeraClient.prototype = Object.freeze({
|
2012-11-07 16:25:09 -08:00
|
|
|
/**
|
|
|
|
* Channel load flags for all requests.
|
|
|
|
*
|
|
|
|
* Caching is not applicable, so we bypass and disable it. We also
|
|
|
|
* ignore any cookies that may be present for the domain because
|
|
|
|
* Bagheera does not utilize cookies and the release of cookies may
|
|
|
|
* inadvertantly constitute unncessary information disclosure.
|
|
|
|
*/
|
|
|
|
_loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE |
|
|
|
|
Ci.nsIRequest.INHIBIT_CACHING |
|
|
|
|
Ci.nsIRequest.LOAD_ANONYMOUS,
|
|
|
|
|
|
|
|
DEFAULT_TIMEOUT_MSEC: 5 * 60 * 1000, // 5 minutes.
|
|
|
|
|
|
|
|
_RE_URI_IDENTIFIER: /^[a-zA-Z0-9_-]+$/,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Upload a JSON payload to the server.
|
|
|
|
*
|
|
|
|
* The return value is a Promise which will be resolved with a
|
|
|
|
* BagheeraClientRequestResult when the request has finished.
|
|
|
|
*
|
|
|
|
* @param namespace
|
|
|
|
* (string) The namespace to post this data to.
|
|
|
|
* @param id
|
|
|
|
* (string) The ID of the document being uploaded. This is typically
|
|
|
|
* a UUID in hex form.
|
|
|
|
* @param payload
|
|
|
|
* (string|object) Data to upload. Can be specified as a string (which
|
|
|
|
* is assumed to be JSON) or an object. If an object, it will be fed into
|
|
|
|
* JSON.stringify() for serialization.
|
|
|
|
* @param deleteOldID
|
|
|
|
* (string) Old document ID to delete as part of upload. If not
|
|
|
|
* specified, no old documents will be deleted as part of upload. The
|
|
|
|
* string value is typically a UUID in hex form.
|
|
|
|
*
|
|
|
|
* @return Promise<BagheeraClientRequestResult>
|
|
|
|
*/
|
|
|
|
uploadJSON: function uploadJSON(namespace, id, payload, deleteOldID=null) {
|
|
|
|
if (!namespace) {
|
|
|
|
throw new Error("namespace argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!id) {
|
|
|
|
throw new Error("id argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!payload) {
|
|
|
|
throw new Error("payload argument must be defined.");
|
|
|
|
}
|
|
|
|
|
|
|
|
let uri = this._submitURI(namespace, id);
|
|
|
|
|
|
|
|
let data = payload;
|
|
|
|
|
|
|
|
if (typeof(payload) == "object") {
|
|
|
|
data = JSON.stringify(payload);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof(data) != "string") {
|
|
|
|
throw new Error("Unknown type for payload: " + typeof(data));
|
|
|
|
}
|
|
|
|
|
|
|
|
this._log.info("Uploading data to " + uri);
|
|
|
|
|
2013-02-05 20:25:57 -08:00
|
|
|
let request = new BagheeraRequest(uri);
|
2012-11-07 16:25:09 -08:00
|
|
|
request.loadFlags = this._loadFlags;
|
|
|
|
request.timeout = this.DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
|
|
|
if (deleteOldID) {
|
|
|
|
request.setHeader("X-Obsolete-Document", deleteOldID);
|
|
|
|
}
|
|
|
|
|
|
|
|
let deferred = Promise.defer();
|
|
|
|
|
|
|
|
data = CommonUtils.convertString(data, "uncompressed", "deflate");
|
|
|
|
// TODO proper header per bug 807134.
|
|
|
|
request.setHeader("Content-Type", "application/json+zlib; charset=utf-8");
|
|
|
|
|
|
|
|
this._log.info("Request body length: " + data.length);
|
|
|
|
|
|
|
|
let result = new BagheeraClientRequestResult();
|
|
|
|
result.namespace = namespace;
|
|
|
|
result.id = id;
|
|
|
|
|
|
|
|
request.onComplete = this._onComplete.bind(this, request, deferred, result);
|
|
|
|
request.post(data);
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete the specified document.
|
|
|
|
*
|
|
|
|
* @param namespace
|
|
|
|
* (string) Namespace from which to delete the document.
|
|
|
|
* @param id
|
|
|
|
* (string) ID of document to delete.
|
|
|
|
*
|
|
|
|
* @return Promise<BagheeraClientRequestResult>
|
|
|
|
*/
|
|
|
|
deleteDocument: function deleteDocument(namespace, id) {
|
|
|
|
let uri = this._submitURI(namespace, id);
|
|
|
|
|
2013-02-05 20:25:57 -08:00
|
|
|
let request = new BagheeraRequest(uri);
|
2012-11-07 16:25:09 -08:00
|
|
|
request.loadFlags = this._loadFlags;
|
|
|
|
request.timeout = this.DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
|
|
|
let result = new BagheeraClientRequestResult();
|
|
|
|
result.namespace = namespace;
|
|
|
|
result.id = id;
|
|
|
|
let deferred = Promise.defer();
|
|
|
|
|
|
|
|
request.onComplete = this._onComplete.bind(this, request, deferred, result);
|
|
|
|
request.delete();
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
},
|
|
|
|
|
|
|
|
_submitURI: function _submitURI(namespace, id) {
|
|
|
|
if (!this._RE_URI_IDENTIFIER.test(namespace)) {
|
|
|
|
throw new Error("Illegal namespace name. Must be alphanumeric + [_-]: " +
|
|
|
|
namespace);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this._RE_URI_IDENTIFIER.test(id)) {
|
|
|
|
throw new Error("Illegal id value. Must be alphanumeric + [_-]: " + id);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.baseURI + "1.0/submit/" + namespace + "/" + id;
|
|
|
|
},
|
|
|
|
|
|
|
|
_onComplete: function _onComplete(request, deferred, result, error) {
|
|
|
|
result.request = request;
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
this._log.info("Transport failure on request: " +
|
|
|
|
CommonUtils.exceptionStr(error));
|
|
|
|
result.transportSuccess = false;
|
|
|
|
deferred.resolve(result);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
result.transportSuccess = true;
|
|
|
|
|
|
|
|
let response = request.response;
|
|
|
|
|
|
|
|
switch (response.status) {
|
|
|
|
case 200:
|
|
|
|
case 201:
|
|
|
|
result.serverSuccess = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
result.serverSuccess = false;
|
|
|
|
|
|
|
|
this._log.info("Received unexpected status code: " + response.status);
|
|
|
|
this._log.debug("Response body: " + response.body);
|
|
|
|
}
|
|
|
|
|
|
|
|
deferred.resolve(result);
|
|
|
|
},
|
2013-02-05 20:25:57 -08:00
|
|
|
});
|
2012-11-07 16:25:09 -08:00
|
|
|
|