perkeep/server/camlistored/ui/server_connection.js

609 lines
21 KiB
JavaScript

goog.provide('camlistore.ServerConnection');
goog.require('goog.string');
goog.require('goog.net.XhrIo');
goog.require('goog.Uri'); // because goog.net.XhrIo forgot to include it.
goog.require('goog.debug.ErrorHandler'); // because goog.net.Xhrio forgot to include it.
goog.require('goog.uri.utils');
goog.require('camlistore.blob');
goog.require('camlistore.ServerType');
goog.require('camlistore.WorkerMessageRouter');
// @fileoverview Connection to the blob server and API for the RPCs it provides. All blob index UI code should use this connection to contact the server.
// @param {camlistore.ServerType.DiscoveryDocument} config Discovery document for the current server.
// @param {Function=} opt_sendXhr Function for sending XHRs for testing.
// @constructor
camlistore.ServerConnection = function(config, opt_sendXhr) {
this.config_ = config;
this.sendXhr_ = opt_sendXhr || goog.net.XhrIo.send;
this.worker_ = null;
};
camlistore.ServerConnection.prototype.getWorker_ = function() {
if (!this.worker_) {
var r = new Date().getTime(); // For cachebusting the worker. Sigh. We need content stamping.
this.worker_ = new camlistore.WorkerMessageRouter(new Worker('hash_worker.js?r=' + r));
}
return this.worker_;
};
camlistore.ServerConnection.prototype.getConfig = function() {
return this.config_;
};
// @param {?Function|undefined} fail Fail func to call if exists.
// @return {Function}
camlistore.ServerConnection.prototype.safeFail_ = function(fail) {
return fail || function(msg) {
throw new Error(msg);
};
};
// @param {Function} success Success callback.
// @param {?Function} fail Optional fail callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.handleXhrResponseText_ = function(success, fail, e) {
var xhr = e.target;
var error = !xhr.isSuccess();
var result = null;
if (!error) {
result = xhr.getResponseText();
error = !result;
}
if (error) {
if (fail) {
fail(xhr.getLastError())
} else {
// TODO(bslatkin): Add a default failure event handler to this class.
console.log('Failed XHR (text) in ServerConnection');
}
return;
}
success(result);
};
// @param {string} blobref blobref whose contents we want.
// @param {Function} success callback with data.
// @param {?Function} opt_fail optional failure calback
camlistore.ServerConnection.prototype.getBlobContents = function(blobref, success, opt_fail) {
var path = goog.uri.utils.appendPath(
this.config_.blobRoot, 'camli/' + blobref
);
this.sendXhr_(path,
goog.bind(this.handleXhrResponseText_, this,
success, this.safeFail_(opt_fail)
)
);
};
// TODO(mpl): set a global timeout ?
// Brett, would it be worth to use the XhrIo send instance method, with listeners,
// instead of the send() utility function ?
// @param {Function} success Success callback.
// @param {?Function} fail Optional fail callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.handleXhrResponseJson_ = function(success, fail, e) {
var xhr = e.target;
var error = !xhr.isSuccess();
var result = null;
try {
result = xhr.getResponseJson();
} catch(err) {
result = "Response was not valid JSON: " + xhr.getResponseText();
}
if (error) {
fail(result.error || result);
} else {
success(result);
}
};
// @param {Function} success callback with data.
// @param {?Function} opt_fail optional failure calback
camlistore.ServerConnection.prototype.discoSignRoot = function(success, opt_fail) {
var path = goog.uri.utils.appendPath(this.config_.jsonSignRoot, '/camli/sig/discovery');
this.sendXhr_(path, goog.bind(this.handleXhrResponseJson_, this, success, this.safeFail_(opt_fail)));
};
// @param {function(camlistore.ServerType.StatusResponse)} success.
// @param {?Function} opt_fail optional failure calback
camlistore.ServerConnection.prototype.serverStatus = function(success, opt_fail) {
var path = goog.uri.utils.appendPath(this.config_.statusRoot, 'status.json');
this.sendXhr_(path,
goog.bind(this.handleXhrResponseJson_, this, success, function(msg) {
console.log("serverStatus error: " + msg);
}));
};
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.genericHandleSearch_ = function(success, opt_fail, e) {
this.handleXhrResponseJson_(success, this.safeFail_(opt_fail), e);
};
// @param {string} blobref root of the tree
// @param {Function} success callback with data.
// @param {?Function} opt_fail optional failure calback
camlistore.ServerConnection.prototype.getFileTree = function(blobref, success, opt_fail) {
// TODO(mpl): do it relatively to a discovered root?
var path = "./tree/" + blobref;
this.sendXhr_(path, goog.bind(this.genericHandleSearch_, this, success, this.safeFail_(opt_fail)));
};
// @param {string} blobref Permanode blobref.
// @param {number} thumbnailSize
// @param {function(camlistore.ServerType.DescribeResponse)} success.
// @param {Function=} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.describeWithThumbnails = function(blobref, thumbnailSize, success, opt_fail) {
var path = goog.uri.utils.appendPath(
this.config_.searchRoot, 'camli/search/describe?blobref=' + blobref);
// TODO(mpl): should we URI encode the value? doc does not say...
path = goog.uri.utils.appendParam(path, 'thumbnails', thumbnailSize);
this.sendXhr_(path, goog.bind(this.genericHandleSearch_, this, success, this.safeFail_(opt_fail)));
};
// @param {string} signer permanode must belong to signer.
// @param {string} attr searched attribute.
// @param {string} value value of the searched attribute.
// @param {Function} success.
// @param {Function=} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.permanodeOfSignerAttrValue = function(signer, attr, value, success, opt_fail) {
var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/signerattrvalue');
path = goog.uri.utils.appendParams(path,
'signer', signer, 'attr', attr, 'value', value
);
this.sendXhr_(
path,
goog.bind(this.genericHandleSearch_, this,
success, this.safeFail_(opt_fail)
)
);
};
// @param {string|object} query If string, will be sent as 'expression', otherwise will be sent as 'constraint'.
// @param {?object} opt_describe The describe property to send for the query
camlistore.ServerConnection.prototype.buildQuery = function(callerQuery, opt_describe, opt_limit, opt_continuationToken) {
var query = {
sort: "-mod"
};
if (goog.isString(callerQuery)) {
query.expression = callerQuery;
} else {
query.constraint = callerQuery;
}
if (opt_describe) {
query.describe = opt_describe;
}
if (opt_limit) {
query.limit = opt_limit;
}
if (opt_continuationToken) {
query.continue = opt_continuationToken;
}
return query;
}
// @param {string|object} query If string, will be sent as 'expression', otherwise will be sent as 'constraint'.
// @param {?object} opt_describe The describe property to send for the query
camlistore.ServerConnection.prototype.search = function(query, opt_describe, opt_limit, opt_continuationToken, callback) {
var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/query');
this.sendXhr_(path,
goog.bind(this.genericHandleSearch_, this, callback, this.safeFail_()),
"POST", JSON.stringify(this.buildQuery(query, opt_describe, opt_limit, opt_continuationToken)));
};
// Where is the target accessed via? (paths it's at)
// @param {string} signer owner of permanode.
// @param {string} target blobref of permanode we want to find paths to
// @param {Function} success.
// @param {Function=} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.pathsOfSignerTarget = function(signer, target, success, opt_fail) {
var path = goog.uri.utils.appendPath(
this.config_.searchRoot, 'camli/search/signerpaths'
);
path = goog.uri.utils.appendParams(path, 'signer', signer, 'target', target);
this.sendXhr_(path,
goog.bind(this.genericHandleSearch_, this, success, this.safeFail_(opt_fail)));
};
// @param {string} permanode Permanode blobref.
// @param {Function} success.
// @param {Function=} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.permanodeClaims = function(permanode, success, opt_fail) {
var path = goog.uri.utils.appendPath(
this.config_.searchRoot, 'camli/search/claims?permanode=' + permanode
);
this.sendXhr_(
path,
goog.bind(this.genericHandleSearch_, this,
success, this.safeFail_(opt_fail)
)
);
};
// @param {Object} clearObj Unsigned object.
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.sign_ = function(clearObj, success, opt_fail) {
var sigConf = this.config_.signing;
if (!sigConf || !sigConf.publicKeyBlobRef) {
this.safeFail_(opt_fail)("Missing Camli.config.signing.publicKeyBlobRef");
return;
}
clearObj.camliSigner = sigConf.publicKeyBlobRef;
var camVersion = clearObj.camliVersion;
if (camVersion) {
delete clearObj.camliVersion;
}
var clearText = JSON.stringify(clearObj, null, " ");
if (camVersion) {
clearText = "{\"camliVersion\":" + camVersion + ",\n" + clearText.substr("{\n".length);
}
this.sendXhr_(
sigConf.signHandler,
goog.bind(this.handlePost_, this,
success, this.safeFail_(opt_fail)),
"POST",
"json=" + encodeURIComponent(clearText),
{"Content-Type": "application/x-www-form-urlencoded"}
);
};
// @param {Object} signed Signed JSON blob (string) to verify.
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.verify_ = function(signed, success, opt_fail) {
var sigConf = this.config_.signing;
if (!sigConf || !sigConf.publicKeyBlobRef) {
this.safeFail_(opt_fail)("Missing Camli.config.signing.publicKeyBlobRef");
return;
}
this.sendXhr_(
sigConf.verifyHandler,
goog.bind(this.handlePost_, this,
success, this.safeFail_(opt_fail)),
"POST",
"sjson=" + encodeURIComponent(signed),
{"Content-Type": "application/x-www-form-urlencoded"}
);
};
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.handlePost_ = function(success, opt_fail, e) {
this.handleXhrResponseText_(success, opt_fail, e);
};
// @param {string} s String to upload.
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.uploadString_ = function(s, success, opt_fail) {
var blobref = camlistore.blob.refFromString(s);
var parts = [s];
var bb = new Blob(parts);
var fd = new FormData();
fd.append(blobref, bb);
// TODO: hack, hard-coding the upload URL here.
// Change the spec now that App Engine permits 32 MB requests
// and permit a PUT request on the sha1? Or at least let us
// specify the well-known upload URL? In cases like this, uploading
// a new permanode, it's silly to even stat.
this.sendXhr_(
this.config_.blobRoot + "camli/upload",
goog.bind(this.handleUploadString_, this,
blobref,
success,
this.safeFail_(opt_fail)
),
"POST",
fd
);
};
// @param {string} blobref Uploaded blobRef.
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.handleUploadString_ = function(blobref, success, opt_fail, e) {
this.handlePost_(
function(resj) {
if (!resj) {
alert("upload permanode fail; no response");
return;
}
var resObj = JSON.parse(resj);
if (!resObj.received || !resObj.received[0] || !resObj.received[0].blobRef) {
alert("upload permanode fail, expected blobRef not in response");
return;
}
if (success) {
success(blobref);
}
},
this.safeFail_(opt_fail),
e
)
};
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.createPermanode = function(success, opt_fail) {
var json = {
"camliVersion": 1,
"camliType": "permanode",
"random": ""+Math.random()
};
this.sign_(json,
goog.bind(this.handleSignPermanode_, this, success, this.safeFail_(opt_fail)),
function(msg) {
this.safeFail_(opt_fail)("sign permanode fail: " + msg);
}
);
};
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
// @param {string} signed Signed string to upload
camlistore.ServerConnection.prototype.handleSignPermanode_ = function(success, opt_fail, signed) {
this.uploadString_(
signed,
success,
function(msg) {
this.safeFail_(opt_fail)("upload permanode fail: " + msg);
}
)
};
// @param {string} permanode Permanode to change.
// @param {string} claimType What kind of claim: "add-attribute", "set-attribute"...
// @param {string} attribute What attribute the claim applies to.
// @param {string} value Attribute value.
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.changeAttribute_ = function(permanode, claimType, attribute, value, success, opt_fail) {
var json = {
"camliVersion": 1,
"camliType": "claim",
"permaNode": permanode,
"claimType": claimType,
// TODO(mpl): to (im)port.
"claimDate": dateToRfc3339String(new Date()),
"attribute": attribute,
"value": value
};
this.sign_(json,
goog.bind(this.handleSignClaim_, this, success, this.safeFail_(opt_fail)),
function(msg) {
this.safeFail_(opt_fail)("sign " + claimType + " fail: " + msg);
}
);
};
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
// @param {string} signed Signed string to upload
camlistore.ServerConnection.prototype.handleSignClaim_ = function(success, opt_fail, signed) {
this.uploadString_(
signed,
success,
function(msg) {
this.safeFail_(opt_fail)("upload " + claimType + " fail: " + msg);
}
)
};
// @param {string} permanode Permanode blobref.
// @param {string} attribute Name of the attribute to set.
// @param {string} value Value to set the attribute to.
// @param {function(string)} success Success callback, called with blobref of uploaded file.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.newSetAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
this.changeAttribute_(permanode, "set-attribute", attribute, value,
success, this.safeFail_(opt_fail)
);
};
// @param {string} permanode Permanode blobref.
// @param {string} attribute Name of the attribute to add.
// @param {string} value Value of the added attribute.
// @param {function(string)} success Success callback, called with blobref of uploaded file.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.newAddAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
this.changeAttribute_(permanode, "add-attribute", attribute, value,
success, this.safeFail_(opt_fail)
);
};
// @param {string} permanode Permanode blobref.
// @param {string} attribute Name of the attribute to delete.
// @param {string} value Value of the attribute to delete.
// @param {function(string)} success Success callback, called with blobref of uploaded file.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.newDelAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
this.changeAttribute_(permanode, "del-attribute", attribute, value,
success, this.safeFail_(opt_fail)
);
};
// @param {File} file File to be uploaded.
// @param {function(string)} success Success callback, called with blobref of
// uploaded file.
// @param {?Function} opt_fail Optional fail callback.
// @param {?Function} opt_onContentsRef Optional callback to set contents during upload.
camlistore.ServerConnection.prototype.uploadFile = function(file, success, opt_fail, opt_onContentsRef) {
this.getWorker_().sendMessage('ref', file, function(ref) {
if (opt_onContentsRef) {
opt_onContentsRef(ref);
}
this.camliUploadFileHelper_(file, ref, success, this.safeFail_(opt_fail));
}.bind(this));
};
// camliUploadFileHelper uploads the provided file with contents blobref contentsBlobRef
// and returns a blobref of a file blob. It does not create any permanodes.
// Most callers will use camliUploadFile instead of this helper.
//
// camliUploadFileHelper only uploads chunks of the file if they don't already exist
// on the server. It starts by assuming the file might already exist on the server
// and, if so, uses an existing (but re-verified) file schema ref instead.
// @param {File} file File to be uploaded.
// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
// @param {function(string)} success function(fileBlobRef) of the
// server-validated or just-uploaded file schema blob.
// @param {?Function} opt_fail Optional fail callback.
camlistore.ServerConnection.prototype.camliUploadFileHelper_ = function(file, contentsBlobRef, success, opt_fail) {
if (!this.config_.uploadHelper) {
this.safeFail_(opt_fail)("no uploadHelper available");
return;
}
var doUpload = goog.bind(function() {
var fd = new FormData();
fd.append("TODO-some-uploadHelper-form-name", file);
this.sendXhr_(
this.config_.uploadHelper,
goog.bind(this.handleUpload_, this,
file, contentsBlobRef, success, this.safeFail_(opt_fail)
),
"POST",
fd
);
}, this);
this.findExistingFileSchemas_(
contentsBlobRef,
goog.bind(this.dupCheck_, this,
doUpload, contentsBlobRef, success
),
this.safeFail_(opt_fail)
)
}
// @param {File} file File to be uploaded.
// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
// @param {Function} success Success callback.
// @param {?Function} opt_fail Optional fail callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.handleUpload_ = function(file, contentsBlobRef, success, opt_fail, e) {
this.handlePost_(
goog.bind(function(res) {
var resObj = JSON.parse(res);
if (resObj.got && resObj.got.length == 1 && resObj.got[0].fileref) {
var fileblob = resObj.got[0].fileref;
console.log("uploaded " + contentsBlobRef + " => file blob " + fileblob);
success(fileblob);
} else {
this.safeFail_(opt_fail)("failed to upload " + file.name + ": " + contentsBlobRef + ": " + JSON.stringify(res, null, 2))
}
}, this),
this.safeFail_(opt_fail),
e
)
};
// @param {string} wholeDigestRef file digest.
// @param {Function} success callback with data.
// @param {?Function} opt_fail optional failure calback
camlistore.ServerConnection.prototype.findExistingFileSchemas_ = function(wholeDigestRef, success, opt_fail) {
var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/files');
path = goog.uri.utils.appendParam(path, 'wholedigest', wholeDigestRef);
this.sendXhr_(
path,
goog.bind(this.genericHandleSearch_, this,
success, this.safeFail_(opt_fail)
)
);
};
// @param {Function} doUpload fun that takes care of uploading.
// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
// @param {Function} success Success callback.
// @param {Object} res result from the wholedigest search.
camlistore.ServerConnection.prototype.dupCheck_ = function(doUpload, contentsBlobRef, success, res) {
var remain = res.files;
var checkNext = goog.bind(function(files) {
if (files.length == 0) {
doUpload();
return;
}
// TODO: verify filename and other file metadata in the
// file json schema match too, not just the contents
var checkFile = files[0];
console.log("integrity checking the reported dup " + checkFile);
// TODO(mpl): see about passing directly a ref of files maybe instead of a copy?
// just being careful for now.
this.sendXhr_(
this.config_.downloadHelper + checkFile + "/?verifycontents=" + contentsBlobRef,
goog.bind(this.handleVerifycontents_, this,
contentsBlobRef, files.slice(), checkNext, success),
"HEAD"
);
}, this);
checkNext(remain);
}
// @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
// @param {Array.<string>} files files to check.
// @param {Function} checkNext fun, recursive call.
// @param {Function} success Success callback.
// @param {goog.events.Event} e Event that triggered this
camlistore.ServerConnection.prototype.handleVerifycontents_ = function(contentsBlobRef, files, checkNext, success, e) {
var xhr = e.target;
var error = !(xhr.isComplete() && xhr.getStatus() == 200);
var checkFile = files.shift();
if (error) {
console.log("integrity check failed on " + checkFile);
checkNext(files);
return;
}
if (xhr.getResponseHeader("X-Camli-Contents") == contentsBlobRef) {
console.log("integrity check passed on " + checkFile + "; using it.");
success(checkFile);
} else {
checkNext(files);
}
};
// Format |dateVal| as specified by RFC 3339.
function dateToRfc3339String(dateVal) {
// Return a string containing |num| zero-padded to |length| digits.
var pad = function(num, length) {
var numStr = "" + num;
while (numStr.length < length) {
numStr = "0" + numStr;
}
return numStr;
};
return goog.string.subs("%s-%s-%sT%s:%s:%sZ",
dateVal.getUTCFullYear(), pad(dateVal.getUTCMonth() + 1, 2), pad(dateVal.getUTCDate(), 2),
pad(dateVal.getUTCHours(), 2), pad(dateVal.getUTCMinutes(), 2), pad(dateVal.getUTCSeconds(), 2));
};