mirror of https://github.com/perkeep/perkeep.git
797 lines
23 KiB
JavaScript
797 lines
23 KiB
JavaScript
/**
|
|
* @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.
|
|
*
|
|
*/
|
|
goog.provide('camlistore.ServerConnection');
|
|
|
|
goog.require('camlistore.base64');
|
|
goog.require('camlistore.SHA1');
|
|
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.ServerType');
|
|
|
|
|
|
/**
|
|
* @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) {
|
|
/**
|
|
* @type {camlistore.ServerType.DiscoveryDocument}
|
|
* @private
|
|
*/
|
|
this.config_ = config;
|
|
|
|
/**
|
|
* @type {Function}
|
|
* @private
|
|
*/
|
|
this.sendXhr_ = opt_sendXhr || goog.net.XhrIo.send;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {?Function|undefined} fail Fail func to call if exists.
|
|
* @return {Function}
|
|
*/
|
|
camlistore.ServerConnection.prototype.safeFail_ = function(fail) {
|
|
if (typeof fail === 'undefined') {
|
|
return alert;
|
|
}
|
|
if (fail === null) {
|
|
return alert;
|
|
}
|
|
return fail;
|
|
};
|
|
|
|
/**
|
|
* @param {Function} success Success callback.
|
|
* @param {?Function} fail Optional fail callback.
|
|
* @param {goog.events.Event} e Event that triggered this
|
|
* @private
|
|
*/
|
|
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
|
|
* @private
|
|
*/
|
|
camlistore.ServerConnection.prototype.handleXhrResponseJson_ =
|
|
function(success, fail, e) {
|
|
var xhr = e.target;
|
|
var error = !xhr.isSuccess();
|
|
var result = null;
|
|
if (!error) {
|
|
try {
|
|
result = xhr.getResponseJson();
|
|
} catch(err) {
|
|
console.log("Response was not valid JSON: " + xhr.getResponseText());
|
|
if (fail) {
|
|
fail();
|
|
}
|
|
return;
|
|
}
|
|
error = !result;
|
|
}
|
|
if (error) {
|
|
if (fail) {
|
|
fail()
|
|
} else {
|
|
// TODO(bslatkin): Add a default failure event handler to this class.
|
|
console.log('Failed XHR (GET) in ServerConnection');
|
|
}
|
|
return;
|
|
}
|
|
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
|
|
* @private
|
|
*/
|
|
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 {function(camlistore.ServerType.SearchRecentResponse)} success callback with data.
|
|
* @param {number=} opt_thumbnailSize
|
|
* @param {?Function} opt_fail optional failure calback
|
|
*/
|
|
camlistore.ServerConnection.prototype.getRecentlyUpdatedPermanodes =
|
|
function(success, opt_thumbnailSize, opt_fail) {
|
|
|
|
var path = goog.uri.utils.appendPath(
|
|
this.config_.searchRoot, 'camli/search/recent');
|
|
if (!!opt_thumbnailSize) {
|
|
path = goog.uri.utils.appendParam(path, 'thumbnails', opt_thumbnailSize);
|
|
}
|
|
|
|
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} signer permanode must belong to signer.
|
|
* @param {string} attr searched attribute.
|
|
* @param {string} value value of the searched attribute.
|
|
* @param {boolean} fuzzy fuzzy search.
|
|
* @param {number} max max number of results.
|
|
* @param {number} thumbsize thumbnails size, 0 for no thumbnails.
|
|
* @param {function(camlistore.ServerType.SearchWithAttrResponse)} success.
|
|
* @param {Function=} opt_fail Optional fail callback.
|
|
*/
|
|
camlistore.ServerConnection.prototype.permanodesWithAttr =
|
|
function(signer, attr, value, fuzzy, max, thumbsize, success, opt_fail) {
|
|
var path = goog.uri.utils.appendPath(
|
|
this.config_.searchRoot, 'camli/search/permanodeattr'
|
|
);
|
|
path = goog.uri.utils.appendParams(path,
|
|
'signer', signer, 'attr', attr, 'value', value,
|
|
'fuzzy', fuzzy, 'max', max, 'thumbnails', thumbsize
|
|
);
|
|
|
|
this.sendXhr_(
|
|
path,
|
|
goog.bind(this.genericHandleSearch_, this,
|
|
success, this.safeFail_(opt_fail)
|
|
)
|
|
);
|
|
};
|
|
|
|
// 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.
|
|
* @private
|
|
*/
|
|
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.
|
|
* @private
|
|
*/
|
|
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
|
|
* @private
|
|
*/
|
|
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.
|
|
* @private
|
|
*/
|
|
camlistore.ServerConnection.prototype.uploadString_ =
|
|
function(s, success, opt_fail) {
|
|
var blobref = "sha1-" + Crypto.SHA1(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
|
|
* @private
|
|
*/
|
|
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.
|
|
* @private
|
|
*/
|
|
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
|
|
* @private
|
|
*/
|
|
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.
|
|
* @private
|
|
*/
|
|
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
|
|
* @private
|
|
*/
|
|
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) {
|
|
var fr = new FileReader();
|
|
var onload = function() {
|
|
var dataurl = fr.result;
|
|
var comma = dataurl.indexOf(",");
|
|
if (comma != -1) {
|
|
var b64 = dataurl.substring(comma + 1);
|
|
var arrayBuffer = Base64.decode(b64).buffer;
|
|
var hash = Crypto.SHA1(new Uint8Array(arrayBuffer, 0));
|
|
|
|
var contentsRef = "sha1-" + hash;
|
|
if (opt_onContentsRef) {
|
|
opt_onContentsRef(contentsRef);
|
|
}
|
|
this.camliUploadFileHelper_(file, contentsRef, success, this.safeFail_(opt_fail));
|
|
}
|
|
};
|
|
fr.onload = goog.bind(onload, this);
|
|
fr.onerror = function() {
|
|
console.log("FileReader onerror: " + fr.error + " code=" + fr.error.code);
|
|
};
|
|
fr.readAsDataURL(file);
|
|
};
|
|
|
|
// 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.
|
|
* @private
|
|
*/
|
|
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
|
|
* @private
|
|
*/
|
|
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.
|
|
* @private
|
|
*/
|
|
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
|
|
* @private
|
|
*/
|
|
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);
|
|
}
|
|
};
|
|
|
|
// TODO(mpl): if we don't end up using it anywhere else, just make
|
|
// it a closure within changeAttribute_.
|
|
// 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";
|
|
};
|
|
|