mirror of https://github.com/perkeep/perkeep.git
462 lines
15 KiB
JavaScript
462 lines
15 KiB
JavaScript
/*
|
|
Copyright 2011 Google Inc.
|
|
|
|
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.
|
|
*/
|
|
|
|
// Camli namespace.
|
|
var Camli = {};
|
|
|
|
var disco = null; // TODO: kill this in favor of Camli.config.
|
|
|
|
// Method 1 to get discovery information (JSONP style):
|
|
function onConfiguration(config) {
|
|
Camli.config = disco = config;
|
|
console.log("Got config: " + JSON.stringify(config));
|
|
}
|
|
|
|
function saneOpts(opts) {
|
|
if (!opts) {
|
|
opts = {}
|
|
}
|
|
if (!opts.success) {
|
|
opts.success = function() {};
|
|
}
|
|
if (!opts.fail) {
|
|
opts.fail = function() {};
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
// 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 dateVal.getUTCFullYear() + "-" + pad(dateVal.getUTCMonth() + 1, 2) + "-" + pad(dateVal.getUTCDate(), 2) + "T" +
|
|
pad(dateVal.getUTCHours(), 2) + ":" + pad(dateVal.getUTCMinutes(), 2) + ":" + pad(dateVal.getUTCSeconds(), 2) + "Z";
|
|
}
|
|
|
|
var cachedCamliSigDiscovery;
|
|
|
|
// opts.success called with discovery object
|
|
// opts.fail called with error text
|
|
function camliSigDiscovery(opts) {
|
|
opts = saneOpts(opts);
|
|
if (cachedCamliSigDiscovery) {
|
|
opts.success(cachedCamliSigDiscovery);
|
|
return;
|
|
}
|
|
var cb = {};
|
|
cb.success = function(sd) {
|
|
cachedCamliSigDiscovery = sd;
|
|
opts.success(sd);
|
|
};
|
|
cb.fail = opts.fail;
|
|
var xhr = camliJsonXhr("camliSigDiscovery", cb);
|
|
xhr.open("GET", Camli.config.jsonSignRoot + "/camli/sig/discovery", true);
|
|
xhr.send();
|
|
}
|
|
|
|
function camliDescribeBlob(blobref, opts) {
|
|
var xhr = camliJsonXhr("camliDescribeBlob", opts);
|
|
var path = Camli.config.searchRoot + "camli/search/describe?blobref=" +
|
|
blobref;
|
|
xhr.open("GET", path, true);
|
|
xhr.send();
|
|
}
|
|
|
|
function makeURL(base, map) {
|
|
for (var key in map) {
|
|
if (base.indexOf("?") == -1) {
|
|
base += "?";
|
|
} else {
|
|
base += "&";
|
|
}
|
|
base += key + "=" + encodeURIComponent(map[key]);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function camliPermanodeOfSignerAttrValue(signer, attr, value, opts) {
|
|
var xhr = camliJsonXhr("camliPermanodeOfSignerAttrValue", opts);
|
|
var path = makeURL(Camli.config.searchRoot + "camli/search/signerattrvalue",
|
|
{ signer: signer, attr: attr, value: value });
|
|
xhr.open("GET", path, true);
|
|
xhr.send();
|
|
}
|
|
|
|
// Where is the target accessed via? (paths it's at)
|
|
function camliPathsOfSignerTarget(signer, target, opts) {
|
|
var xhr = camliJsonXhr("camliPathsOfSignerTarget", opts);
|
|
var path = makeURL(Camli.config.searchRoot + "camli/search/signerpaths",
|
|
{ signer: signer, target: target });
|
|
xhr.open("GET", path, true);
|
|
xhr.send();
|
|
}
|
|
|
|
function camliGetPermanodeClaims(permanode, opts) {
|
|
var xhr = camliJsonXhr("camliGetPermanodeClaims", opts);
|
|
var path = Camli.config.searchRoot + "camli/search/claims?permanode=" +
|
|
permanode;
|
|
xhr.open("GET", path, true);
|
|
xhr.send();
|
|
}
|
|
|
|
function camliGetBlobContents(blobref, opts) {
|
|
opts = saneOpts(opts);
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState != 4) { return; }
|
|
if (xhr.status != 200) {
|
|
opts.fail("camliGetBlobContents HTTP status " + xhr.status);
|
|
return;
|
|
}
|
|
opts.success(xhr.responseText);
|
|
};
|
|
xhr.open("GET", camliBlobURL(blobref), true);
|
|
xhr.send();
|
|
}
|
|
|
|
function camliBlobURL(blobref) {
|
|
return Camli.config.blobRoot + "camli/" + blobref;
|
|
}
|
|
|
|
function camliDescribeBlogURL(blobref) {
|
|
return Camli.config.searchRoot + 'camli/search/describe?blobref=' + blobref;
|
|
}
|
|
|
|
function camliSign(clearObj, opts) {
|
|
opts = saneOpts(opts);
|
|
|
|
camliSigDiscovery(
|
|
{
|
|
success: function(sigConf) {
|
|
if (!sigConf.publicKeyBlobRef) {
|
|
opts.fail("Missing sigConf.publicKeyBlobRef");
|
|
return;
|
|
}
|
|
clearObj.camliSigner = sigConf.publicKeyBlobRef;
|
|
clearText = JSON.stringify(clearObj, null, 2);
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState != 4) { return; }
|
|
if (xhr.status != 200) {
|
|
opts.fail("got status " + xhr.status);
|
|
return;
|
|
}
|
|
opts.success(xhr.responseText);
|
|
};
|
|
xhr.open("POST", sigConf.signHandler, true);
|
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
|
xhr.send("json=" + encodeURIComponent(clearText));
|
|
},
|
|
fail: function(errMsg) {
|
|
opts.fail(errMsg);
|
|
}
|
|
});
|
|
}
|
|
|
|
// file: File object
|
|
// contentsBlobRef: blob ref of file as sha1'd locally
|
|
// opts: fail(strMsg) success(strFileBlobRef) of the validated (or uploaded + created) file schema blob.
|
|
// associating with a permanode is caller's job.
|
|
function camliUploadFileHelper(file, contentsBlobRef, opts) {
|
|
opts = saneOpts(opts);
|
|
if (!Camli.config.uploadHelper) {
|
|
opts.fail("no uploadHelper available");
|
|
return
|
|
}
|
|
|
|
var doUpload = function() {
|
|
var fd = new FormData();
|
|
fd.append(fd, file);
|
|
var uploadCb = { fail: opts.fail };
|
|
uploadCb.success = function(res) {
|
|
if (res.got && res.got.length == 1 && res.got[0].fileref) {
|
|
var fileblob = res.got[0].fileref;
|
|
console.log("uploaded " + contentsBlobRef + " => file blob " + fileblob);
|
|
opts.success(fileblob);
|
|
} else {
|
|
opts.fail("failed to upload " + file.name + ": " + contentsBlobRef + ": " + JSON.stringify(res, null, 2))
|
|
}
|
|
};
|
|
var xhr = camliJsonXhr("camliUploadFileHelper", uploadCb);
|
|
xhr.open("POST", Camli.config.uploadHelper);
|
|
xhr.send(fd);
|
|
};
|
|
|
|
var dupcheckCb = { fail: opts.fail };
|
|
dupcheckCb.success = function(res) {
|
|
var remain = res.files;
|
|
var checkNext;
|
|
checkNext = function() {
|
|
if (remain.length == 0) {
|
|
doUpload();
|
|
return;
|
|
}
|
|
// TODO: verify filename and other file metadata in the
|
|
// file json schema match too, not just the contents
|
|
var checkFile = remain.shift();
|
|
console.log("integrity checking the reported dup " + checkFile);
|
|
|
|
var vcb = {};
|
|
vcb.fail = function(xhr) {
|
|
console.log("integrity checked failed on " + checkFile);
|
|
checkNext();
|
|
};
|
|
vcb.success = function(xhr) {
|
|
if (xhr.getResponseHeader("X-Camli-Contents") == contentsBlobRef) {
|
|
console.log("integrity checked passed on " + checkFile + "; using it.");
|
|
opts.success(checkFile);
|
|
} else {
|
|
checkNext();
|
|
}
|
|
};
|
|
var xhr = camliXhr("headVerifyFile", vcb);
|
|
xhr.open("HEAD", Camli.config.downloadHelper + checkFile + "/?verifycontents=" + contentsBlobRef, true);
|
|
xhr.send();
|
|
};
|
|
checkNext();
|
|
};
|
|
camliFindExistingFileSchemas(contentsBlobRef, dupcheckCb);
|
|
}
|
|
|
|
function camliUploadString(s, opts) {
|
|
opts = saneOpts(opts);
|
|
var blobref = "sha1-" + Crypto.SHA1(s);
|
|
|
|
bb = new WebKitBlobBuilder();
|
|
bb.append(s);
|
|
|
|
var fd = new FormData();
|
|
fd.append(blobref, bb.getBlob());
|
|
|
|
var uploadCb = {};
|
|
uploadCb.success = function(resj) {
|
|
// TODO: check resj.received[] array.
|
|
opts.success(blobref);
|
|
};
|
|
uploadCb.fail = opts.fail;
|
|
var xhr = camliJsonXhr("camliUploadString", uploadCb);
|
|
// 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.
|
|
xhr.open("POST", Camli.config.blobRoot + "camli/upload")
|
|
xhr.send(fd);
|
|
}
|
|
|
|
function camliCreateNewPermanode(opts) {
|
|
opts = saneOpts(opts);
|
|
var json = {
|
|
"camliVersion": 1,
|
|
"camliType": "permanode",
|
|
"random": ""+Math.random()
|
|
};
|
|
camliSign(json, {
|
|
success: function(got) {
|
|
camliUploadString(
|
|
got,
|
|
{
|
|
success: opts.success,
|
|
fail: function(msg) {
|
|
opts.fail("upload permanode fail: " + msg);
|
|
}
|
|
});
|
|
},
|
|
fail: function(msg) {
|
|
opts.fail("sign permanode fail: " + msg);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Returns the first value from the query string corresponding to |key|.
|
|
// Returns null if the key isn't present.
|
|
function getQueryParam(key) {
|
|
var params = document.location.search.substring(1).split('&');
|
|
for (var i = 0; i < params.length; ++i) {
|
|
var parts = params[i].split('=');
|
|
if (parts.length == 2 && decodeURIComponent(parts[0]) == key)
|
|
return decodeURIComponent(parts[1]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function camliGetRecentlyUpdatedPermanodes(opts) {
|
|
var xhr = camliJsonXhr("camliGetRecentlyUpdatedPermanodes", opts);
|
|
xhr.open("GET", Camli.config.searchRoot + "camli/search/recent", true);
|
|
xhr.send();
|
|
}
|
|
|
|
function camliGetPermanodesWithAttr(signer, attr, value, fuzzy, opts) {
|
|
var xhr = camliJsonXhr("camliGetPermanodesWithAttr", opts);
|
|
var path = makeURL(Camli.config.searchRoot + "camli/search/permanodeattr",
|
|
{ signer: signer, attr: attr, value: value, fuzzy: fuzzy });
|
|
xhr.open("GET", path, true);
|
|
xhr.send();
|
|
}
|
|
|
|
function camliXhr(name, opts) {
|
|
opts = saneOpts(opts);
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState != 4) { return; }
|
|
if (xhr.status == 200) {
|
|
opts.success(xhr);
|
|
} else {
|
|
opts.fail(name + ": expected status 200; got " + xhr.status + ": " + xhr.responseText);
|
|
}
|
|
};
|
|
return xhr;
|
|
}
|
|
|
|
function camliJsonXhr(name, opts) {
|
|
opts = saneOpts(opts);
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState != 4) { return; }
|
|
if (xhr.status != 200) {
|
|
try {
|
|
var resj = JSON.parse(xhr.responseText);
|
|
opts.fail(name + ": expected status 200; got " + xhr.status + ": " + resj.error);
|
|
} catch(x) {
|
|
opts.fail(name + ": expected status 200; got " + xhr.status + ": " + xhr.responseText);
|
|
}
|
|
return;
|
|
}
|
|
var resj;
|
|
try {
|
|
resj = JSON.parse(xhr.responseText);
|
|
} catch(x) {
|
|
opts.fail(name + ": error parsing JSON in response: " + xhr.responseText);
|
|
return
|
|
}
|
|
if (resj.error) {
|
|
opts.fail(resj.error);
|
|
} else {
|
|
opts.success(resj);
|
|
}
|
|
};
|
|
return xhr;
|
|
}
|
|
|
|
function camliFindExistingFileSchemas(wholeDigestRef, opts) {
|
|
var xhr = camliJsonXhr("camliFindExistingFileSchemas", opts);
|
|
var path = Camli.config.searchRoot + "camli/search/files?wholedigest=" +
|
|
wholeDigestRef;
|
|
xhr.open("GET", path, true);
|
|
xhr.send();
|
|
}
|
|
|
|
// Returns true if the passed-in string might be a blobref.
|
|
function isPlausibleBlobRef(blobRef) {
|
|
return /^\w+-[a-f0-9]+$/.test(blobRef);
|
|
}
|
|
|
|
function linkifyBlobRefs(schemaBlob) {
|
|
var re = /(\w{3,6}-[a-f0-9]{30,})/g;
|
|
return schemaBlob.replace(re, "<a href='./?b=$1'>$1</a>");
|
|
}
|
|
|
|
// Helper function for camliNewSetAttributeClaim() (and eventually, for
|
|
// similar functions to add or delete attributes).
|
|
function changeAttribute(permanode, claimType, attribute, value, opts) {
|
|
opts = saneOpts(opts);
|
|
var json = {
|
|
"camliVersion": 1,
|
|
"camliType": "claim",
|
|
"permaNode": permanode,
|
|
"claimType": claimType,
|
|
"claimDate": dateToRfc3339String(new Date()),
|
|
"attribute": attribute,
|
|
"value": value
|
|
};
|
|
camliSign(json, {
|
|
success: function(signedBlob) {
|
|
camliUploadString(signedBlob, {
|
|
success: opts.success,
|
|
fail: function(msg) {
|
|
opts.fail("upload " + claimType + " fail: " + msg);
|
|
}
|
|
});
|
|
},
|
|
fail: function(msg) {
|
|
opts.fail("sign " + claimType + " fail: " + msg);
|
|
}
|
|
});
|
|
}
|
|
|
|
function camliBlobTitle(pn, des) {
|
|
return _camliBlobTitleOrThumb(pn, des, 0, 0);
|
|
}
|
|
|
|
function camliBlobThumbnail(pn, des, width, height) {
|
|
return _camliBlobTitleOrThumb(pn, des, width, height);
|
|
}
|
|
|
|
// pn: permanode to find a good title of
|
|
// jdes: describe response of root permanode
|
|
// w, h: if both of them are non-zero, returns html of an wxh size <img> thumbnail, not a title.
|
|
function _camliBlobTitleOrThumb(pn, des, w, h) {
|
|
var d = des[pn];
|
|
if (!d) {
|
|
return pn;
|
|
}
|
|
if (d.camliType == "file" && d.file && d.file.fileName) {
|
|
var fileName = d.file.fileName
|
|
if (w != 0 && h != 0 && d.file.mimeType && d.file.mimeType.indexOf("image/") == 0) {
|
|
var img = "<img src='./thumbnail/" + pn + "/" +
|
|
fileName.replace(/['"<>\?&]/g, "") + "?mw=" + w + "&mh=" + h + "'>";
|
|
return img;
|
|
}
|
|
return fileName;
|
|
}
|
|
if (d.permanode) {
|
|
var attr = d.permanode.attr;
|
|
if (!attr) {
|
|
return pn;
|
|
}
|
|
if (attr.title) {
|
|
return attr.title[0];
|
|
}
|
|
if (attr.camliContent) {
|
|
return _camliBlobTitleOrThumb(attr.camliContent[0], des, w, h);
|
|
}
|
|
}
|
|
return pn;
|
|
}
|
|
|
|
// Create and upload a new set-attribute claim.
|
|
function camliNewSetAttributeClaim(permanode, attribute, value, opts) {
|
|
changeAttribute(permanode, "set-attribute", attribute, value, opts);
|
|
}
|
|
|
|
// Create and upload a new add-attribute claim.
|
|
function camliNewAddAttributeClaim(permanode, attribute, value, opts) {
|
|
changeAttribute(permanode, "add-attribute", attribute, value, opts);
|
|
}
|
|
|
|
// Create and upload a new del-attribute claim.
|
|
function camliNewDelAttributeClaim(permanode, attribute, value, opts) {
|
|
changeAttribute(permanode, "del-attribute", attribute, value, opts);
|
|
}
|
|
|