/** * Fallback to upload a revision if uploadMultipartRevision failed due to the server's rejecting * multipart format. - (void) uploadJSONRevision: (CBL_Revision*)originalRev in CBLRestPusher.m */ private void uploadJsonRevision(final RevisionInternal rev) { // Get the revision's properties: if (!db.inlineFollowingAttachmentsIn(rev)) { setError(new CouchbaseLiteException(Status.BAD_ATTACHMENT)); return; } final String path = String.format("/%s?new_edits=false", encodeDocumentId(rev.getDocID())); CustomFuture future = sendAsyncRequest( "PUT", path, rev.getProperties(), new RemoteRequestCompletionBlock() { public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) { if (e != null) { setError(e); } else { Log.v(Log.TAG_SYNC, "%s: Sent %s (JSON), response=%s", this, rev, result); removePending(rev); } } }); future.setQueue(pendingFutures); pendingFutures.add(future); }
/** in CBL_Pusher.m - (CBLMultipartWriter*)multipartWriterForRevision: (CBL_Revision*)rev */ @InterfaceAudience.Private private boolean uploadMultipartRevision(final RevisionInternal revision) { // holds inputStream for blob to close after using final List<InputStream> streamList = new ArrayList<InputStream>(); MultipartEntity multiPart = null; Map<String, Object> revProps = revision.getProperties(); Map<String, Object> attachments = (Map<String, Object>) revProps.get("_attachments"); for (String attachmentKey : attachments.keySet()) { Map<String, Object> attachment = (Map<String, Object>) attachments.get(attachmentKey); if (attachment.containsKey("follows")) { if (multiPart == null) { multiPart = new MultipartEntity(); try { String json = Manager.getObjectMapper().writeValueAsString(revProps); Charset utf8charset = Charset.forName("UTF-8"); byte[] uncompressed = json.getBytes(utf8charset); byte[] compressed = null; byte[] data = uncompressed; String contentEncoding = null; if (uncompressed.length > RemoteRequest.MIN_JSON_LENGTH_TO_COMPRESS && canSendCompressedRequests()) { compressed = Utils.compressByGzip(uncompressed); if (compressed.length < uncompressed.length) { data = compressed; contentEncoding = "gzip"; } } // NOTE: StringBody.contentEncoding default value is null. Setting null value to // contentEncoding does not cause any impact. multiPart.addPart( "param1", new StringBody(data, "application/json", utf8charset, contentEncoding)); } catch (IOException e) { throw new IllegalArgumentException(e); } } BlobStore blobStore = this.db.getAttachmentStore(); String base64Digest = (String) attachment.get("digest"); BlobKey blobKey = new BlobKey(base64Digest); InputStream blobStream = blobStore.blobStreamForKey(blobKey); if (blobStream == null) { Log.w( Log.TAG_SYNC, "Unable to load the blob stream for blobKey: %s - Skipping upload of multipart revision.", blobKey); return false; } else { streamList.add(blobStream); String contentType = null; if (attachment.containsKey("content_type")) { contentType = (String) attachment.get("content_type"); } else if (attachment.containsKey("type")) { contentType = (String) attachment.get("type"); } else if (attachment.containsKey("content-type")) { Log.w( Log.TAG_SYNC, "Found attachment that uses content-type" + " field name instead of content_type (see couchbase-lite-android" + " issue #80): %s", attachment); } // contentType = null causes Exception from FileBody of apache. if (contentType == null) contentType = "application/octet-stream"; // default // NOTE: Content-Encoding might not be necessary to set. Apache FileBody does not set // Content-Encoding. // FileBody always return null for getContentEncoding(), and Content-Encoding header // is not set in multipart // CBL iOS: // https://github.com/couchbase/couchbase-lite-ios/blob/feb7ff5eda1e80bd00e5eb19f1d46c793f7a1951/Source/CBL_Pusher.m#L449-L452 String contentEncoding = null; if (attachment.containsKey("encoding")) { contentEncoding = (String) attachment.get("encoding"); } InputStreamBody inputStreamBody = new CustomStreamBody(blobStream, contentType, attachmentKey, contentEncoding); multiPart.addPart(attachmentKey, inputStreamBody); } } } if (multiPart == null) { return false; } final String path = String.format("/%s?new_edits=false", encodeDocumentId(revision.getDocID())); Log.d(Log.TAG_SYNC, "Uploading multipart request. Revision: %s", revision); addToChangesCount(1); CustomFuture future = sendAsyncMultipartRequest( "PUT", path, multiPart, new RemoteRequestCompletionBlock() { @Override public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) { try { if (e != null) { if (e instanceof HttpResponseException) { // Server doesn't like multipart, eh? Fall back to JSON. if (((HttpResponseException) e).getStatusCode() == 415) { // status 415 = "bad_content_type" dontSendMultipart = true; uploadJsonRevision(revision); } } else { Log.e(Log.TAG_SYNC, "Exception uploading multipart request", e); setError(e); } } else { Log.v(Log.TAG_SYNC, "Uploaded multipart request. Revision: %s", revision); removePending(revision); } } finally { // close all inputStreams for Blob for (InputStream stream : streamList) { try { stream.close(); } catch (IOException ioe) { } } addToCompletedChangesCount(1); } } }); future.setQueue(pendingFutures); pendingFutures.add(future); return true; }
/** - (void) processInbox: (CBL_RevisionList*)changes in CBLRestPusher.m */ @Override @InterfaceAudience.Private protected void processInbox(final RevisionList changes) { Log.v(Log.TAG_SYNC, "processInbox() changes=" + changes.size()); // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants: // <http://wiki.apache.org/couchdb/HttpPostRevsDiff> Map<String, List<String>> diffs = new HashMap<String, List<String>>(); for (RevisionInternal rev : changes) { String docID = rev.getDocID(); List<String> revs = diffs.get(docID); if (revs == null) { revs = new ArrayList<String>(); diffs.put(docID, revs); } revs.add(rev.getRevID()); addPending(rev); } // Call _revs_diff on the target db: Log.v(Log.TAG_SYNC, "%s: posting to /_revs_diff", this); CustomFuture future = sendAsyncRequest( "POST", "/_revs_diff", diffs, new RemoteRequestCompletionBlock() { @Override public void onCompletion(HttpResponse httpResponse, Object response, Throwable e) { Log.v(Log.TAG_SYNC, "%s: got /_revs_diff response", this); Map<String, Object> results = (Map<String, Object>) response; if (e != null) { setError(e); } else { if (results.size() != 0) { // Go through the list of local changes again, selecting the ones the // destination server // said were missing and mapping them to a JSON dictionary in the form // _bulk_docs wants: List<Object> docsToSend = new ArrayList<Object>(); RevisionList revsToSend = new RevisionList(); long bufferedSize = 0; for (RevisionInternal rev : changes) { // Is this revision in the server's 'missing' list? Map<String, Object> properties = null; Map<String, Object> revResults = (Map<String, Object>) results.get(rev.getDocID()); if (revResults == null) { removePending(rev); continue; } List<String> revs = (List<String>) revResults.get("missing"); if (revs == null || !revs.contains(rev.getRevID())) { removePending(rev); continue; } // NOTE: force to load body by Database.loadRevisionBody() // In SQLiteStore.loadRevisionBody() does not load data from database // if sequence != 0 && body != null rev.setSequence(0); rev.setBody(null); RevisionInternal loadedRev; try { loadedRev = db.loadRevisionBody(rev); } catch (CouchbaseLiteException e1) { Log.w( Log.TAG_SYNC, "%s Couldn't get local contents of %s", rev, PusherInternal.this); continue; } RevisionInternal populatedRev = transformRevision(loadedRev); loadedRev = null; List<String> possibleAncestors = (List<String>) revResults.get("possible_ancestors"); properties = new HashMap<String, Object>(populatedRev.getProperties()); Map<String, Object> revisions = db.getRevisionHistoryDictStartingFromAnyAncestor( populatedRev, possibleAncestors); properties.put("_revisions", revisions); populatedRev.setProperties(properties); // Strip any attachments already known to the target db: if (properties.containsKey("_attachments")) { // Look for the latest common ancestor and stub out older attachments: int minRevPos = findCommonAncestor(populatedRev, possibleAncestors); Status status = new Status(Status.OK); if (!db.expandAttachments( populatedRev, minRevPos + 1, !dontSendMultipart, false, status)) { Log.w( Log.TAG_SYNC, "%s: Couldn't expand attachments of %s", this, populatedRev); continue; } properties = populatedRev.getProperties(); if (!dontSendMultipart && uploadMultipartRevision(populatedRev)) { continue; } } if (properties == null || !properties.containsKey("_id")) { throw new IllegalStateException("properties must contain a document _id"); } revsToSend.add(rev); docsToSend.add(properties); bufferedSize += JSONUtils.estimate(properties); if (bufferedSize > kMaxBulkDocsObjectSize) { uploadBulkDocs(docsToSend, revsToSend); docsToSend = new ArrayList<Object>(); revsToSend = new RevisionList(); bufferedSize = 0; } } // Post the revisions to the destination: uploadBulkDocs(docsToSend, revsToSend); } else { // None of the revisions are new to the remote for (RevisionInternal revisionInternal : changes) { removePending(revisionInternal); } } } } }); future.setQueue(pendingFutures); pendingFutures.add(future); pauseOrResume(); }
/** * Post the revisions to the destination. "new_edits":false means that the server should use the * given _rev IDs instead of making up new ones. * * <p>- (void) uploadBulkDocs: (NSArray*)docsToSend changes: (CBL_RevisionList*)changes in * CBLRestPusher.m */ @InterfaceAudience.Private protected void uploadBulkDocs(List<Object> docsToSend, final RevisionList changes) { final int numDocsToSend = docsToSend.size(); if (numDocsToSend == 0) { return; } Log.v( Log.TAG_SYNC, "%s: POSTing " + numDocsToSend + " revisions to _bulk_docs: %s", PusherInternal.this, docsToSend); addToChangesCount(numDocsToSend); Map<String, Object> bulkDocsBody = new HashMap<String, Object>(); bulkDocsBody.put("docs", docsToSend); bulkDocsBody.put("new_edits", false); CustomFuture future = sendAsyncRequest( "POST", "/_bulk_docs", bulkDocsBody, new RemoteRequestCompletionBlock() { @Override public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) { if (e == null) { Set<String> failedIDs = new HashSet<String>(); // _bulk_docs response is really an array, not a dictionary! List<Map<String, Object>> items = (List) result; for (Map<String, Object> item : items) { Status status = statusFromBulkDocsResponseItem(item); if (status.isError()) { // One of the docs failed to save. Log.w(Log.TAG_SYNC, "%s: _bulk_docs got an error: %s", item, this); // 403/Forbidden means validation failed; don't treat it as an error // because I did my job in sending the revision. Other statuses are // actual replication errors. if (status.getCode() != Status.FORBIDDEN) { String docID = (String) item.get("id"); failedIDs.add(docID); // TODO - port from iOS // NSURL* url = docID ? [_remote URLByAppendingPathComponent: docID] : nil; // error = CBLStatusToNSError(status, url); } } } // Remove from the pending list all the revs that didn't fail: for (RevisionInternal revisionInternal : changes) { if (!failedIDs.contains(revisionInternal.getDocID())) { removePending(revisionInternal); } } } if (e != null) { setError(e); } else { Log.v(Log.TAG_SYNC, "%s: POSTed to _bulk_docs", PusherInternal.this); } addToCompletedChangesCount(numDocsToSend); } }); future.setQueue(pendingFutures); pendingFutures.add(future); }
/** * Fetches the contents of a revision from the remote db, including its parent revision ID. The * contents are stored into rev.properties. */ @InterfaceAudience.Private public void pullRemoteRevision(final RevisionInternal rev) { Log.d(Log.TAG_SYNC, "%s: pullRemoteRevision with rev: %s", this, rev); ++httpConnectionCount; // Construct a query. We want the revision history, and the bodies of attachments that have // been added since the latest revisions we have locally. // See: http://wiki.apache.org/couchdb/HTTP_Document_API#Getting_Attachments_With_a_Document StringBuilder path = new StringBuilder("/"); path.append(encodeDocumentId(rev.getDocID())); path.append("?rev=").append(URIUtils.encode(rev.getRevID())); path.append("&revs=true&attachments=true"); // If the document has attachments, add an 'atts_since' param with a list of // already-known revisions, so the server can skip sending the bodies of any // attachments we already have locally: AtomicBoolean hasAttachment = new AtomicBoolean(false); List<String> knownRevs = db.getPossibleAncestorRevisionIDs( rev, PullerInternal.MAX_NUMBER_OF_ATTS_SINCE, hasAttachment); if (hasAttachment.get() && knownRevs != null && knownRevs.size() > 0) { path.append("&atts_since="); path.append(joinQuotedEscaped(knownRevs)); } // create a final version of this variable for the log statement inside // FIXME find a way to avoid this final String pathInside = path.toString(); CustomFuture future = sendAsyncMultipartDownloaderRequest( "GET", pathInside, null, db, new RemoteRequestCompletionBlock() { @Override public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) { if (e != null) { Log.e(Log.TAG_SYNC, "Error pulling remote revision", e); revisionFailed(rev, e); } else { Map<String, Object> properties = (Map<String, Object>) result; PulledRevision gotRev = new PulledRevision(properties); gotRev.setSequence(rev.getSequence()); Log.d( Log.TAG_SYNC, "%s: pullRemoteRevision add rev: %s to batcher: %s", PullerInternal.this, gotRev, downloadsToInsert); if (gotRev.getBody() != null) gotRev.getBody().compact(); // Add to batcher ... eventually it will be fed to -insertRevisions:. downloadsToInsert.queueObject(gotRev); } // Note that we've finished this task: --httpConnectionCount; // Start another task if there are still revisions waiting to be pulled: pullRemoteRevisions(); } }); future.setQueue(pendingFutures); pendingFutures.add(future); }