diff --git a/server/camlistored/ui/index.js b/server/camlistored/ui/index.js index 50a24476f..454438698 100644 --- a/server/camlistored/ui/index.js +++ b/server/camlistored/ui/index.js @@ -298,7 +298,7 @@ cam.IndexPage = React.createClass({ totalBytesComplete: completedBytes }); - console.log('Uploaded %d of %d bytes', completedBytes, this.state.totalBytesToUpload); + console.log('Completed %d of %d bytes', completedBytes, this.state.totalBytesToUpload); }, onUploadComplete_: function() { @@ -319,16 +319,18 @@ cam.IndexPage = React.createClass({ var files = e.nativeEvent.dataTransfer.files; var sc = this.props.serverConnection; + var parent = this.getTargetBlobref_(); this.onUploadStart_(files); goog.labs.Promise.all( Array.prototype.map.call(files, function(file) { return uploadFile(file) - .then(fetchExistingPermanode) + .then(fetchPermanodeIfExists) .then(createPermanodeIfNotExists) - .then(nameResults) - .then(createPermanodeAssociations.bind(this)) + .then(updatePermanodeRef) + .then(checkExistingCamliMembership) + .then(createPermanodeAssociations) .thenCatch(function(e) { console.error('File upload fall down go boom. file: %s, error: %s', file.name, e); }) @@ -339,53 +341,92 @@ cam.IndexPage = React.createClass({ }).then(this.onUploadComplete_); function uploadFile(file) { + + // capture status of upload promise chain + var status = { + fileRef: '', + isCamliMemberOfParent: false, + parentRef: parent, + permanodeRef: '', + permanodeCreated: false + }; + var uploadFile = new goog.labs.Promise(sc.uploadFile.bind(sc, file)); - return goog.labs.Promise.all([uploadFile]); + + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status), uploadFile]); } - function fetchExistingPermanode(blobIds) { - var fileRef = blobIds[0]; - var fileUploaded = new goog.labs.Promise.resolve(fileRef); - var getPermanode = new goog.labs.Promise(sc.getPermanodeWithContent.bind(sc, fileRef)); - return goog.labs.Promise.all([fileUploaded, getPermanode]); + function fetchPermanodeIfExists(results) { + var status = results[0]; + status.fileRef = results[1]; + + var getPermanode = new goog.labs.Promise(sc.getPermanodeWithContent.bind(sc, status.fileRef)); + + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status), getPermanode]); } function createPermanodeIfNotExists(results) { - var fileRef = results[0]; - var permanode = results[1]; - if (!permanode) { - var fileUploaded = new goog.labs.Promise.resolve(fileRef); + var status = results[0]; + var permanodeRef = results[1]; + + if (!permanodeRef) { + status.permanodeCreated = true; + var createPermanode = new goog.labs.Promise(sc.createPermanode.bind(sc)); - return goog.labs.Promise.all([fileUploaded, createPermanode]); + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status), createPermanode]); } - // Empty values so the next in chain knows that we're in the "permanode already exists" case. - return goog.labs.Promise.resolve(["", ""]); + + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status), new goog.labs.Promise.resolve(permanodeRef)]); } - // 'readable-ify' the blob references returned from upload/create - function nameResults(blobIds) { - return { - 'fileRef': blobIds[0], - 'permanodeRef': blobIds[1] - }; + function updatePermanodeRef(results) { + var status = results[0]; + status.permanodeRef = results[1]; + + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status)]); } - function createPermanodeAssociations(refs) { - if (refs.permanodeRef == "") { - // Any value would do, but boolean helps make it clear that we end - // here, by resolving the file upload promise chain. - return goog.labs.Promise.resolve(true); + // TODO(mpl): this implementation means that when we're dropping on a set, we send + // one additional query for each permanode that already exists. So in the worst case, + // it amounts to one additional query per dropped item (with a small payload/response). + // Alternatively, we could ask (either by tweaking the search session, or + // "manually") the server for all the set members and cache the response, which means + // only one additional query, and we can then do all the tests locally. However, the + // response size scales with the number of members in the set, so I don't know if it's + // better. A working example is at + // https://camlistore-review.googlesource.com/#/c/5345/2 . We should benchmark and/or + // ask Brad. + + // check, when appropriate, if the permanode is already part of the set we're dropping in. + function checkExistingCamliMembership(results) { + var status = results[0]; + + // Permanode did not exist before, so it couldn't be a member of any set. + if (!status.parentRef || status.permanodeCreated) { + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status), new goog.labs.Promise.resolve(false)]); } + console.log('checking membership'); + var hasMembership = new goog.labs.Promise(sc.isCamliMember.bind(sc, status.permanodeRef, status.parentRef)); + return goog.labs.Promise.all([new goog.labs.Promise.resolve(status), hasMembership]); + } + + function createPermanodeAssociations(results) { + var status = results[0]; + status.isCamliMemberOfParent = results[1]; + + var promises = []; + // associate uploaded file to new permanode - var camliContent = new goog.labs.Promise(sc.newSetAttributeClaim.bind(sc, refs.permanodeRef, 'camliContent', refs.fileRef)); - var promises = [camliContent]; + if (status.permanodeCreated) { + var setCamliContent = new goog.labs.Promise(sc.newSetAttributeClaim.bind(sc, status.permanodeRef, 'camliContent', status.fileRef)); + promises.push(setCamliContent); + } - // if currently viewing a set, make new permanode a member of the set - var parentPermanodeRef = this.getTargetBlobref_(); - if (parentPermanodeRef) { - var camliMember = new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, parentPermanodeRef, 'camliMember', refs.permanodeRef)); - promises.push(camliMember); + // add CamliMember relationship if viewing a set + if (status.parentRef && !status.isCamliMemberOfParent) { + var setCamliMember = new goog.labs.Promise(sc.newAddAttributeClaim.bind(sc, status.parentRef, 'camliMember', status.permanodeRef)); + promises.push(setCamliMember); } return goog.labs.Promise.all(promises); diff --git a/server/camlistored/ui/server_connection.js b/server/camlistored/ui/server_connection.js index e8a28d0e6..e356e2063 100644 --- a/server/camlistored/ui/server_connection.js +++ b/server/camlistored/ui/server_connection.js @@ -60,7 +60,7 @@ cam.ServerConnection.DESCRIBE_REQUEST = { ] }; -cam.ServerConnection.prototype.getPermanodeWithContent = function(contentRef, success, opt_fail) { +cam.ServerConnection.prototype.getPermanodeWithContent = function(contentRef, success) { var query = { permanode: { attr: "camliContent", @@ -77,6 +77,37 @@ cam.ServerConnection.prototype.getPermanodeWithContent = function(contentRef, su this.search(query, null, null, null, callback); }; +// If child is a camliMember of parent success is called with 'true', otherrwise 'false' +// @param {string} blobref of the child +// @param {string} blobref of the parent +// @param {Function} success callback with data. +cam.ServerConnection.prototype.isCamliMember = function(child, parent, success) { + var query = { + logical: { + a: { + permanode: { + attr: "camliMember", + ValueInSet: { + blobRefPrefix: child, + } + } + }, + op: "and", + b: { + blobRefPrefix: parent, + } + }, + }; + var callback = function(result) { + if (!result || !result.blobs || result.blobs.length == 0) { + success(false); + return; + } + success(true); + } + this.search(query, null, null, null, callback); +}; + cam.ServerConnection.prototype.getWorker_ = function() { if (!this.worker_) { var r = new Date().getTime(); // For cachebusting the worker. Sigh. We need content stamping.