github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/server/camlistored/ui/server_connection.js (about)

     1  /*
     2  Copyright 2013 Google Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  goog.provide('cam.ServerConnection');
    18  
    19  goog.require('goog.string');
    20  goog.require('goog.net.XhrIo');
    21  goog.require('goog.Uri'); // because goog.net.XhrIo forgot to include it.
    22  goog.require('goog.debug.ErrorHandler'); // because goog.net.Xhrio forgot to include it.
    23  goog.require('goog.uri.utils');
    24  
    25  goog.require('cam.blob');
    26  goog.require('cam.ServerType');
    27  goog.require('cam.WorkerMessageRouter');
    28  
    29  // @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.
    30  // @param {cam.ServerType.DiscoveryDocument} config Discovery document for the current server.
    31  // @param {Function=} opt_sendXhr Function for sending XHRs for testing.
    32  // @constructor
    33  cam.ServerConnection = function(config, opt_sendXhr) {
    34  	this.config_ = config;
    35  	this.sendXhr_ = opt_sendXhr || goog.net.XhrIo.send;
    36  	this.worker_ = null;
    37  };
    38  
    39  cam.ServerConnection.prototype.getWorker_ = function() {
    40  	if (!this.worker_) {
    41  		var r = new Date().getTime(); // For cachebusting the worker. Sigh. We need content stamping.
    42  		this.worker_ = new cam.WorkerMessageRouter(new Worker('hash_worker.js?r=' + r));
    43  	}
    44  	return this.worker_;
    45  };
    46  
    47  cam.ServerConnection.prototype.getConfig = function() {
    48  	return this.config_;
    49  };
    50  
    51  // @param {?Function|undefined} fail Fail func to call if exists.
    52  // @return {Function}
    53  cam.ServerConnection.prototype.safeFail_ = function(fail) {
    54  	return fail || function(msg) {
    55  		throw new Error(msg);
    56  	};
    57  };
    58  
    59  // @param {Function} success Success callback.
    60  // @param {?Function} fail Optional fail callback.
    61  // @param {goog.events.Event} e Event that triggered this
    62  cam.ServerConnection.prototype.handleXhrResponseText_ = function(success, fail, e) {
    63  	var xhr = e.target;
    64  	var error = !xhr.isSuccess();
    65  	var result = null;
    66  	if (!error) {
    67  		result = xhr.getResponseText();
    68  		error = !result;
    69  	}
    70  	if (error) {
    71  		if (fail) {
    72  			fail(xhr.getLastError())
    73  		} else {
    74  			// TODO(bslatkin): Add a default failure event handler to this class.
    75  			console.log('Failed XHR (text) in ServerConnection');
    76  		}
    77  		return;
    78  	}
    79  	success(result);
    80  };
    81  
    82  // @param {string} blobref blobref whose contents we want.
    83  // @param {Function} success callback with data.
    84  // @param {?Function} opt_fail optional failure calback
    85  cam.ServerConnection.prototype.getBlobContents = function(blobref, success, opt_fail) {
    86  	var path = goog.uri.utils.appendPath(
    87  		this.config_.blobRoot, 'camli/' + blobref
    88  	);
    89  
    90  	this.sendXhr_(path,
    91  		goog.bind(this.handleXhrResponseText_, this,
    92  			success, this.safeFail_(opt_fail)
    93  		)
    94  	);
    95  };
    96  
    97  // TODO(mpl): set a global timeout ?
    98  // Brett, would it be worth to use the XhrIo send instance method, with listeners,
    99  // instead of the send() utility function ?
   100  // @param {Function} success Success callback.
   101  // @param {?Function} fail Optional fail callback.
   102  // @param {goog.events.Event} e Event that triggered this
   103  cam.ServerConnection.prototype.handleXhrResponseJson_ = function(success, fail, e) {
   104  	var xhr = e.target;
   105  	var error = !xhr.isSuccess();
   106  	var result = null;
   107  
   108  	try {
   109  		result = xhr.getResponseJson();
   110  	} catch(err) {
   111  		result = "Response was not valid JSON: " + xhr.getResponseText();
   112  	}
   113  
   114  	if (error) {
   115  		fail(result.error || result);
   116  	} else {
   117  		success(result);
   118  	}
   119  };
   120  
   121  // @param {Function} success callback with data.
   122  // @param {?Function} opt_fail optional failure calback
   123  cam.ServerConnection.prototype.discoSignRoot = function(success, opt_fail) {
   124  	var path = goog.uri.utils.appendPath(this.config_.jsonSignRoot, '/camli/sig/discovery');
   125  	this.sendXhr_(path, goog.bind(this.handleXhrResponseJson_, this, success, this.safeFail_(opt_fail)));
   126  };
   127  
   128  // @param {function(cam.ServerType.StatusResponse)} success.
   129  // @param {?Function} opt_fail optional failure calback
   130  cam.ServerConnection.prototype.serverStatus = function(success, opt_fail) {
   131  	var path = goog.uri.utils.appendPath(this.config_.statusRoot, 'status.json');
   132  
   133  	this.sendXhr_(path,
   134  		goog.bind(this.handleXhrResponseJson_, this, success, function(msg) {
   135  			console.log("serverStatus error: " + msg);
   136  		}));
   137  };
   138  
   139  // @param {Function} success Success callback.
   140  // @param {?Function} opt_fail Optional fail callback.
   141  // @param {goog.events.Event} e Event that triggered this
   142  cam.ServerConnection.prototype.genericHandleSearch_ = function(success, opt_fail, e) {
   143  	this.handleXhrResponseJson_(success, this.safeFail_(opt_fail), e);
   144  };
   145  
   146  // @param {string} blobref root of the tree
   147  // @param {Function} success callback with data.
   148  // @param {?Function} opt_fail optional failure calback
   149  cam.ServerConnection.prototype.getFileTree = function(blobref, success, opt_fail) {
   150  
   151  	// TODO(mpl): do it relatively to a discovered root?
   152  	var path = "./tree/" + blobref;
   153  	this.sendXhr_(path, goog.bind(this.genericHandleSearch_, this, success, this.safeFail_(opt_fail)));
   154  };
   155  
   156  
   157  // @param {string} blobref Permanode blobref.
   158  // @param {number} thumbnailSize
   159  // @param {function(cam.ServerType.DescribeResponse)} success.
   160  // @param {Function=} opt_fail Optional fail callback.
   161  cam.ServerConnection.prototype.describeWithThumbnails = function(blobref, thumbnailSize, success, opt_fail) {
   162  	var path = goog.uri.utils.appendPath(
   163  		this.config_.searchRoot, 'camli/search/describe?blobref=' + blobref);
   164  
   165  	// TODO(mpl): should we URI encode the value? doc does not say...
   166  	path = goog.uri.utils.appendParam(path, 'thumbnails', thumbnailSize);
   167  	this.sendXhr_(path, goog.bind(this.genericHandleSearch_, this, success, this.safeFail_(opt_fail)));
   168  };
   169  
   170  // @param {string} signer permanode must belong to signer.
   171  // @param {string} attr searched attribute.
   172  // @param {string} value value of the searched attribute.
   173  // @param {Function} success.
   174  // @param {Function=} opt_fail Optional fail callback.
   175  cam.ServerConnection.prototype.permanodeOfSignerAttrValue = function(signer, attr, value, success, opt_fail) {
   176  	var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/signerattrvalue');
   177  	path = goog.uri.utils.appendParams(path,
   178  		'signer', signer, 'attr', attr, 'value', value
   179  	);
   180  
   181  	this.sendXhr_(
   182  		path,
   183  		goog.bind(this.genericHandleSearch_, this,
   184  			success, this.safeFail_(opt_fail)
   185  		)
   186  	);
   187  };
   188  
   189  // @param {string|object} query If string, will be sent as 'expression', otherwise will be sent as 'constraint'.
   190  // @param {?object} opt_describe The describe property to send for the query
   191  cam.ServerConnection.prototype.buildQuery = function(callerQuery, opt_describe, opt_limit, opt_continuationToken) {
   192  	var query = {
   193  		sort: "-created"
   194  	};
   195  
   196  	if (goog.isString(callerQuery)) {
   197  		query.expression = callerQuery;
   198  	} else {
   199  		query.constraint = callerQuery;
   200  	}
   201  
   202  	if (opt_describe) {
   203  		query.describe = opt_describe;
   204  	}
   205  	if (opt_limit) {
   206  		query.limit = opt_limit;
   207  	}
   208  	if (opt_continuationToken) {
   209  		query.continue = opt_continuationToken;
   210  	}
   211  
   212  	return query;
   213  }
   214  
   215  // @param {string|object} query If string, will be sent as 'expression', otherwise will be sent as 'constraint'.
   216  // @param {?object} opt_describe The describe property to send for the query
   217  cam.ServerConnection.prototype.search = function(query, opt_describe, opt_limit, opt_continuationToken, callback) {
   218  	var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/query');
   219  	this.sendXhr_(path,
   220  		goog.bind(this.genericHandleSearch_, this, callback, this.safeFail_()),
   221  		"POST", JSON.stringify(this.buildQuery(query, opt_describe, opt_limit, opt_continuationToken)));
   222  };
   223  
   224  // Where is the target accessed via? (paths it's at)
   225  // @param {string} signer owner of permanode.
   226  // @param {string} target blobref of permanode we want to find paths to
   227  // @param {Function} success.
   228  // @param {Function=} opt_fail Optional fail callback.
   229  cam.ServerConnection.prototype.pathsOfSignerTarget = function(signer, target, success, opt_fail) {
   230  	var path = goog.uri.utils.appendPath(
   231  		this.config_.searchRoot, 'camli/search/signerpaths'
   232  	);
   233  	path = goog.uri.utils.appendParams(path, 'signer', signer, 'target', target);
   234  	this.sendXhr_(path,
   235  		goog.bind(this.genericHandleSearch_, this, success, this.safeFail_(opt_fail)));
   236  };
   237  
   238  // @param {string} permanode Permanode blobref.
   239  // @param {Function} success.
   240  // @param {Function=} opt_fail Optional fail callback.
   241  cam.ServerConnection.prototype.permanodeClaims = function(permanode, success, opt_fail) {
   242  	var path = goog.uri.utils.appendPath(
   243  		this.config_.searchRoot, 'camli/search/claims?permanode=' + permanode
   244  	);
   245  
   246  	this.sendXhr_(
   247  		path,
   248  		goog.bind(this.genericHandleSearch_, this,
   249  			success, this.safeFail_(opt_fail)
   250  		)
   251  	);
   252  };
   253  
   254  // @param {Object} clearObj Unsigned object.
   255  // @param {Function} success Success callback.
   256  // @param {?Function} opt_fail Optional fail callback.
   257  cam.ServerConnection.prototype.sign_ = function(clearObj, success, opt_fail) {
   258  	var sigConf = this.config_.signing;
   259  	if (!sigConf || !sigConf.publicKeyBlobRef) {
   260  		this.safeFail_(opt_fail)("Missing Camli.config.signing.publicKeyBlobRef");
   261  		return;
   262  	}
   263  
   264  		clearObj.camliSigner = sigConf.publicKeyBlobRef;
   265  		var camVersion = clearObj.camliVersion;
   266  		if (camVersion) {
   267  			 delete clearObj.camliVersion;
   268  		}
   269  		var clearText = JSON.stringify(clearObj, null, "	");
   270  		if (camVersion) {
   271  			 clearText = "{\"camliVersion\":" + camVersion + ",\n" + clearText.substr("{\n".length);
   272  		}
   273  
   274  	this.sendXhr_(
   275  		sigConf.signHandler,
   276  		goog.bind(this.handlePost_, this,
   277  			success, this.safeFail_(opt_fail)),
   278  		"POST",
   279  		"json=" + encodeURIComponent(clearText),
   280  		{"Content-Type": "application/x-www-form-urlencoded"}
   281  	);
   282  };
   283  
   284  // @param {Object} signed Signed JSON blob (string) to verify.
   285  // @param {Function} success Success callback.
   286  // @param {?Function} opt_fail Optional fail callback.
   287  cam.ServerConnection.prototype.verify_ = function(signed, success, opt_fail) {
   288  	var sigConf = this.config_.signing;
   289  	if (!sigConf || !sigConf.publicKeyBlobRef) {
   290  		this.safeFail_(opt_fail)("Missing Camli.config.signing.publicKeyBlobRef");
   291  		return;
   292  	}
   293  	this.sendXhr_(
   294  		sigConf.verifyHandler,
   295  		goog.bind(this.handlePost_, this,
   296  			success, this.safeFail_(opt_fail)),
   297  		"POST",
   298  		"sjson=" + encodeURIComponent(signed),
   299  		{"Content-Type": "application/x-www-form-urlencoded"}
   300  	);
   301  };
   302  
   303  // @param {Function} success Success callback.
   304  // @param {?Function} opt_fail Optional fail callback.
   305  // @param {goog.events.Event} e Event that triggered this
   306  cam.ServerConnection.prototype.handlePost_ = function(success, opt_fail, e) {
   307  	this.handleXhrResponseText_(success, opt_fail, e);
   308  };
   309  
   310  
   311  // @param {string} s String to upload.
   312  // @param {Function} success Success callback.
   313  // @param {?Function} opt_fail Optional fail callback.
   314  cam.ServerConnection.prototype.uploadString_ = function(s, success, opt_fail) {
   315  	var blobref = cam.blob.refFromString(s);
   316  	var parts = [s];
   317  	var bb = new Blob(parts);
   318  	var fd = new FormData();
   319  	fd.append(blobref, bb);
   320  
   321  	// TODO: hack, hard-coding the upload URL here.
   322  	// Change the spec now that App Engine permits 32 MB requests
   323  	// and permit a PUT request on the sha1?	Or at least let us
   324  	// specify the well-known upload URL?	In cases like this, uploading
   325  	// a new permanode, it's silly to even stat.
   326  	this.sendXhr_(
   327  		this.config_.blobRoot + "camli/upload",
   328  		goog.bind(this.handleUploadString_, this,
   329  			blobref,
   330  			success,
   331  			this.safeFail_(opt_fail)
   332  		),
   333  		"POST",
   334  		fd
   335  	);
   336  };
   337  
   338  // @param {string} blobref Uploaded blobRef.
   339  // @param {Function} success Success callback.
   340  // @param {?Function} opt_fail Optional fail callback.
   341  // @param {goog.events.Event} e Event that triggered this
   342  cam.ServerConnection.prototype.handleUploadString_ = function(blobref, success, opt_fail, e) {
   343  	this.handlePost_(
   344  		function(resj) {
   345  			if (!resj) {
   346  				alert("upload permanode fail; no response");
   347  				return;
   348  			}
   349  			var resObj = JSON.parse(resj);
   350  			if (!resObj.received || !resObj.received[0] || !resObj.received[0].blobRef) {
   351  				alert("upload permanode fail, expected blobRef not in response");
   352  				return;
   353  			}
   354  			if (success) {
   355  				success(blobref);
   356  			}
   357  		},
   358  		this.safeFail_(opt_fail),
   359  		e
   360  	)
   361  };
   362  
   363  // @param {Function} success Success callback.
   364  // @param {?Function} opt_fail Optional fail callback.
   365  cam.ServerConnection.prototype.createPermanode = function(success, opt_fail) {
   366  	var json = {
   367  		"camliVersion": 1,
   368  		"camliType": "permanode",
   369  		"random": ""+Math.random()
   370  	};
   371  	this.sign_(json,
   372  		goog.bind(this.handleSignPermanode_, this, success, this.safeFail_(opt_fail)),
   373  		function(msg) {
   374  			this.safeFail_(opt_fail)("sign permanode fail: " + msg);
   375  		}
   376  	);
   377  };
   378  
   379  // @param {Function} success Success callback.
   380  // @param {?Function} opt_fail Optional fail callback.
   381  // @param {string} signed Signed string to upload
   382  cam.ServerConnection.prototype.handleSignPermanode_ = function(success, opt_fail, signed) {
   383  	this.uploadString_(
   384  		signed,
   385  		success,
   386  		function(msg) {
   387  			this.safeFail_(opt_fail)("upload permanode fail: " + msg);
   388  		}
   389  	)
   390  };
   391  
   392  
   393  // @param {string} permanode Permanode to change.
   394  // @param {string} claimType What kind of claim: "add-attribute", "set-attribute"...
   395  // @param {string} attribute What attribute the claim applies to.
   396  // @param {string} value Attribute value.
   397  // @param {Function} success Success callback.
   398  // @param {?Function} opt_fail Optional fail callback.
   399  cam.ServerConnection.prototype.changeAttribute_ = function(permanode, claimType, attribute, value, success, opt_fail) {
   400  	var json = {
   401  		"camliVersion": 1,
   402  		"camliType": "claim",
   403  		"permaNode": permanode,
   404  		"claimType": claimType,
   405  		// TODO(mpl): to (im)port.
   406  		"claimDate": dateToRfc3339String(new Date()),
   407  		"attribute": attribute,
   408  		"value": value
   409  	};
   410  	this.sign_(json,
   411  		goog.bind(this.handleSignClaim_, this, success, this.safeFail_(opt_fail)),
   412  		function(msg) {
   413  			this.safeFail_(opt_fail)("sign " + claimType + " fail: " + msg);
   414  		}
   415  	);
   416  };
   417  
   418  // @param {string} permanode Permanode to delete.
   419  // @param {Function} success Success callback.
   420  // @param {?Function} opt_fail Optional fail callback.
   421  cam.ServerConnection.prototype.newDeleteClaim = function(permanode, success, opt_fail) {
   422  	var json = {
   423  		"camliVersion": 1,
   424  		"camliType": "claim",
   425  		"target": permanode,
   426  		"claimType": "delete",
   427  		"claimDate": dateToRfc3339String(new Date())
   428  	};
   429  	this.sign_(json,
   430  		goog.bind(this.handleSignClaim_, this, success, this.safeFail_(opt_fail)),
   431  		function(msg) {
   432  			this.safeFail_(opt_fail)("sign delete fail: " + msg);
   433  		}
   434  	);
   435  };
   436  
   437  // @param {Function} success Success callback.
   438  // @param {?Function} opt_fail Optional fail callback.
   439  // @param {string} signed Signed string to upload
   440  cam.ServerConnection.prototype.handleSignClaim_ = function(success, opt_fail, signed) {
   441  	this.uploadString_(
   442  		signed,
   443  		success,
   444  		function(msg) {
   445  			this.safeFail_(opt_fail)("upload " + claimType + " fail: " + msg);
   446  		}
   447  	)
   448  };
   449  
   450  // @param {string} permanode Permanode blobref.
   451  // @param {string} attribute Name of the attribute to set.
   452  // @param {string} value Value to set the attribute to.
   453  // @param {function(string)} success Success callback, called with blobref of uploaded file.
   454  // @param {?Function} opt_fail Optional fail callback.
   455  cam.ServerConnection.prototype.newSetAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
   456  	this.changeAttribute_(permanode, "set-attribute", attribute, value,
   457  		success, this.safeFail_(opt_fail)
   458  	);
   459  };
   460  
   461  
   462  // @param {string} permanode Permanode blobref.
   463  // @param {string} attribute Name of the attribute to add.
   464  // @param {string} value Value of the added attribute.
   465  // @param {function(string)} success Success callback, called with blobref of uploaded file.
   466  // @param {?Function} opt_fail Optional fail callback.
   467  cam.ServerConnection.prototype.newAddAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
   468  	this.changeAttribute_(permanode, "add-attribute", attribute, value,
   469  		success, this.safeFail_(opt_fail)
   470  	);
   471  };
   472  
   473  // @param {string} permanode Permanode blobref.
   474  // @param {string} attribute Name of the attribute to delete.
   475  // @param {string} value Value of the attribute to delete.
   476  // @param {function(string)} success Success callback, called with blobref of uploaded file.
   477  // @param {?Function} opt_fail Optional fail callback.
   478  cam.ServerConnection.prototype.newDelAttributeClaim = function(permanode, attribute, value, success, opt_fail) {
   479  	this.changeAttribute_(permanode, "del-attribute", attribute, value,
   480  		success, this.safeFail_(opt_fail)
   481  	);
   482  };
   483  
   484  
   485  // @param {File} file File to be uploaded.
   486  // @param {function(string)} success Success callback, called with blobref of
   487  // uploaded file.
   488  // @param {?Function} opt_fail Optional fail callback.
   489  // @param {?Function} opt_onContentsRef Optional callback to set contents during upload.
   490  cam.ServerConnection.prototype.uploadFile = function(file, success, opt_fail, opt_onContentsRef) {
   491  	this.getWorker_().sendMessage('ref', file, function(ref) {
   492  		if (opt_onContentsRef) {
   493  			opt_onContentsRef(ref);
   494  		}
   495  		this.camliUploadFileHelper_(file, ref, success, this.safeFail_(opt_fail));
   496  	}.bind(this));
   497  };
   498  
   499  // camliUploadFileHelper uploads the provided file with contents blobref contentsBlobRef
   500  // and returns a blobref of a file blob.	It does not create any permanodes.
   501  // Most callers will use camliUploadFile instead of this helper.
   502  //
   503  // camliUploadFileHelper only uploads chunks of the file if they don't already exist
   504  // on the server. It starts by assuming the file might already exist on the server
   505  // and, if so, uses an existing (but re-verified) file schema ref instead.
   506  // @param {File} file File to be uploaded.
   507  // @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
   508  // @param {function(string)} success function(fileBlobRef) of the
   509  // server-validated or just-uploaded file schema blob.
   510  // @param {?Function} opt_fail Optional fail callback.
   511  cam.ServerConnection.prototype.camliUploadFileHelper_ = function(file, contentsBlobRef, success, opt_fail) {
   512  	if (!this.config_.uploadHelper) {
   513  		this.safeFail_(opt_fail)("no uploadHelper available");
   514  		return;
   515  	}
   516  
   517  	var doUpload = goog.bind(function() {
   518  		var fd = new FormData();
   519  		fd.append("TODO-some-uploadHelper-form-name", file);
   520  		this.sendXhr_(
   521  			this.config_.uploadHelper,
   522  			goog.bind(this.handleUpload_, this,
   523  				file, contentsBlobRef, success, this.safeFail_(opt_fail)
   524  			),
   525  			"POST",
   526  			fd
   527  		);
   528  	}, this);
   529  
   530  	this.findExistingFileSchemas_(
   531  		contentsBlobRef,
   532  		goog.bind(this.dupCheck_, this,
   533  			doUpload, contentsBlobRef, success
   534  		),
   535  		this.safeFail_(opt_fail)
   536  	)
   537  }
   538  
   539  // @param {File} file File to be uploaded.
   540  // @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
   541  // @param {Function} success Success callback.
   542  // @param {?Function} opt_fail Optional fail callback.
   543  // @param {goog.events.Event} e Event that triggered this
   544  cam.ServerConnection.prototype.handleUpload_ = function(file, contentsBlobRef, success, opt_fail, e) {
   545  	this.handlePost_(
   546  		goog.bind(function(res) {
   547  			var resObj = JSON.parse(res);
   548  			if (resObj.got && resObj.got.length == 1 && resObj.got[0].fileref) {
   549  				var fileblob = resObj.got[0].fileref;
   550  				console.log("uploaded " + contentsBlobRef + " => file blob " + fileblob);
   551  				success(fileblob);
   552  			} else {
   553  				this.safeFail_(opt_fail)("failed to upload " + file.name + ": " + contentsBlobRef + ": " + JSON.stringify(res, null, 2))
   554  			}
   555  		}, this),
   556  		this.safeFail_(opt_fail),
   557  		e
   558  	)
   559  };
   560  
   561  
   562  // @param {string} wholeDigestRef file digest.
   563  // @param {Function} success callback with data.
   564  // @param {?Function} opt_fail optional failure calback
   565  cam.ServerConnection.prototype.findExistingFileSchemas_ = function(wholeDigestRef, success, opt_fail) {
   566  	var path = goog.uri.utils.appendPath(this.config_.searchRoot, 'camli/search/files');
   567  	path = goog.uri.utils.appendParam(path, 'wholedigest', wholeDigestRef);
   568  
   569  	this.sendXhr_(
   570  		path,
   571  		goog.bind(this.genericHandleSearch_, this,
   572  			success, this.safeFail_(opt_fail)
   573  		)
   574  	);
   575  };
   576  
   577  
   578  // @param {Function} doUpload fun that takes care of uploading.
   579  // @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
   580  // @param {Function} success Success callback.
   581  // @param {Object} res result from the wholedigest search.
   582  cam.ServerConnection.prototype.dupCheck_ = function(doUpload, contentsBlobRef, success, res) {
   583  	var remain = res.files;
   584  	var checkNext = goog.bind(function(files) {
   585  		if (files.length == 0) {
   586  			doUpload();
   587  			return;
   588  		}
   589  		// TODO: verify filename and other file metadata in the
   590  		// file json schema match too, not just the contents
   591  		var checkFile = files[0];
   592  		console.log("integrity checking the reported dup " + checkFile);
   593  
   594  		// TODO(mpl): see about passing directly a ref of files maybe instead of a copy?
   595  		// just being careful for now.
   596  		this.sendXhr_(
   597  			this.config_.downloadHelper + checkFile + "/?verifycontents=" + contentsBlobRef,
   598  			goog.bind(this.handleVerifycontents_, this,
   599  				contentsBlobRef, files.slice(), checkNext, success),
   600  			"HEAD"
   601  		);
   602  	}, this);
   603  	checkNext(remain);
   604  }
   605  
   606  // @param {string} contentsBlobRef Blob ref of file as sha1'd locally.
   607  // @param {Array.<string>} files files to check.
   608  // @param {Function} checkNext fun, recursive call.
   609  // @param {Function} success Success callback.
   610  // @param {goog.events.Event} e Event that triggered this
   611  cam.ServerConnection.prototype.handleVerifycontents_ = function(contentsBlobRef, files, checkNext, success, e) {
   612  	var xhr = e.target;
   613  	var error = !(xhr.isComplete() && xhr.getStatus() == 200);
   614  	var checkFile = files.shift();
   615  
   616  	if (error) {
   617  		console.log("integrity check failed on " + checkFile);
   618  		checkNext(files);
   619  		return;
   620  	}
   621  	if (xhr.getResponseHeader("X-Camli-Contents") == contentsBlobRef) {
   622  		console.log("integrity check passed on " + checkFile + "; using it.");
   623  		success(checkFile);
   624  	} else {
   625  		checkNext(files);
   626  	}
   627  };
   628  
   629  // Format |dateVal| as specified by RFC 3339.
   630  function dateToRfc3339String(dateVal) {
   631  	// Return a string containing |num| zero-padded to |length| digits.
   632  	var pad = function(num, length) {
   633  		var numStr = "" + num;
   634  		while (numStr.length < length) {
   635  			numStr = "0" + numStr;
   636  		}
   637  		return numStr;
   638  	};
   639  
   640  	return goog.string.subs("%s-%s-%sT%s:%s:%sZ",
   641  		dateVal.getUTCFullYear(), pad(dateVal.getUTCMonth() + 1, 2), pad(dateVal.getUTCDate(), 2),
   642  		pad(dateVal.getUTCHours(), 2), pad(dateVal.getUTCMinutes(), 2), pad(dateVal.getUTCSeconds(), 2));
   643  };