diff --git a/src/Couchbase.Lite.Shared/Database.cs b/src/Couchbase.Lite.Shared/Database.cs index 3fdbf0414..283fc57e8 100644 --- a/src/Couchbase.Lite.Shared/Database.cs +++ b/src/Couchbase.Lite.Shared/Database.cs @@ -736,6 +736,11 @@ internal void ForgetReplication(Replication replication) internal void AddActiveReplication(Replication replication) { + if (ActiveReplicators == null) { + Log.W(TAG, "ActiveReplicators is null, so replication will not be added"); + return; + } + ActiveReplicators.Add(replication); replication.Changed += (sender, e) => { @@ -1327,9 +1332,9 @@ internal void ProcessAttachmentsForRevision(IDictionary - internal void InstallAttachment(AttachmentInternal attachment, IDictionary attachInfo) + internal void InstallAttachment(AttachmentInternal attachment) { - var digest = (string)attachInfo.Get("digest"); + var digest = attachment.Digest; if (digest == null) { throw new CouchbaseLiteException(StatusCode.BadAttachment); } @@ -1379,6 +1384,12 @@ internal Uri FileForAttachmentDict(IDictionary attachmentDict) return retval; } + internal IList GetRevisionHistory(RevisionInternal rev, IList ancestorRevIds) + { + HashSet ancestors = ancestorRevIds != null ? new HashSet(ancestorRevIds) : null; + return Storage.GetRevisionHistory(rev, ancestors); + } + internal bool ExpandAttachments(RevisionInternal rev, int minRevPos, bool allowFollows, bool decodeAttachments, Status outStatus) { @@ -1559,7 +1570,7 @@ internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRev AttachmentInternal attachment = null; try { attachment = new AttachmentInternal(name, attachInfo); - } catch(CouchbaseLiteException e) { + } catch(CouchbaseLiteException) { return null; } @@ -1576,7 +1587,7 @@ internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRev // "follows" means the uploader provided the attachment in a separate MIME part. // This means it's already been registered in _pendingAttachmentsByDigest; // I just need to look it up by its "digest" property and install it into the store: - InstallAttachment(attachment, attachInfo); + InstallAttachment(attachment); } else if(attachInfo.GetCast("stub")) { // "stub" on an incoming revision means the attachment is the same as in the parent. if(parentAttachments == null && prevRevId != null) { @@ -1603,7 +1614,7 @@ internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRev // Set or validate the revpos: if(attachment.RevPos == 0) { attachment.RevPos = generation; - } else if(attachment.RevPos >= generation) { + } else if(attachment.RevPos > generation) { status.Code = StatusCode.BadAttachment; return null; } @@ -1678,7 +1689,7 @@ internal IDictionary GetAttachmentsFromRevision(Revi // "follows" means the uploader provided the attachment in a separate MIME part. // This means it's already been registered in _pendingAttachmentsByDigest; // I just need to look it up by its "digest" property and install it into the store: - InstallAttachment(attachment, attachInfo); + InstallAttachment(attachment); } else { diff --git a/src/Couchbase.Lite.Shared/Database/ICouchStore.cs b/src/Couchbase.Lite.Shared/Database/ICouchStore.cs index 9f4658880..17085ac35 100644 --- a/src/Couchbase.Lite.Shared/Database/ICouchStore.cs +++ b/src/Couchbase.Lite.Shared/Database/ICouchStore.cs @@ -149,14 +149,7 @@ internal interface ICouchStore /// Returns the given revision's list of direct ancestors (as Revision objects) in _reverse_ /// chronological order, starting with the revision itself. /// - IList GetRevisionHistory(RevisionInternal rev); - - /// - /// Returns the revision history as a _revisions dictionary, as returned by the REST API's ?revs=true option. - /// If 'ancestorRevIDs' is present, the revision history will only go back as far as any of the revision ID - /// strings in that array. - /// - IDictionary GetRevisionHistory(RevisionInternal rev, IList ancestorRevIds); + IList GetRevisionHistory(RevisionInternal rev, ICollection ancestorRevIds); /// /// Returns all the known revisions (or all current/conflicting revisions) of a document. diff --git a/src/Couchbase.Lite.Shared/Documents/AttachmentInternal.cs b/src/Couchbase.Lite.Shared/Documents/AttachmentInternal.cs index 82d5b42bb..f4857ab3d 100644 --- a/src/Couchbase.Lite.Shared/Documents/AttachmentInternal.cs +++ b/src/Couchbase.Lite.Shared/Documents/AttachmentInternal.cs @@ -234,6 +234,15 @@ public AttachmentInternal(string name, IDictionary info) if (Digest == null) { throw new CouchbaseLiteException(StatusCode.BadAttachment); } + + if(info.ContainsKey("revpos")) { + var revPos = info.GetCast("revpos"); + if (revPos <= 0) { + throw new CouchbaseLiteException(StatusCode.BadAttachment); + } + + RevPos = revPos; + } } else { throw new CouchbaseLiteException(StatusCode.BadAttachment); } diff --git a/src/Couchbase.Lite.Shared/Replication/Pusher.cs b/src/Couchbase.Lite.Shared/Replication/Pusher.cs index 8580b867a..07dcaf56e 100644 --- a/src/Couchbase.Lite.Shared/Replication/Pusher.cs +++ b/src/Couchbase.Lite.Shared/Replication/Pusher.cs @@ -209,6 +209,134 @@ private void RemovePending(RevisionInternal revisionInternal) } } + /*internal override void ProcessInbox(RevisionList inbox) + { + if (!online) { + Log.V(Tag, "Offline, so skipping inbox process"); + return; + } + + // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants: + // + var diffs = new Dictionary>(); + foreach (var rev in inbox) { + var docID = rev.GetDocId(); + var revs = diffs.Get(docID); + if (revs == null) { + revs = new List(); + diffs[docID] = revs; + } + revs.AddItem(rev.GetRevId()); + AddPending(rev); + } + + // Call _revs_diff on the target db: + Log.D(Tag, "processInbox() calling asyncTaskStarted()"); + Log.D(Tag, "posting to /_revs_diff: {0}", String.Join(Environment.NewLine, new[] { Manager.GetObjectMapper().WriteValueAsString(diffs) })); + + AsyncTaskStarted(); + SendAsyncRequest(HttpMethod.Post, "/_revs_diff", diffs, (response, e) => + { + try { + var results = response.AsDictionary(); + + Log.D(Tag, "/_revs_diff response: {0}\r\n{1}", response, results); + + if (e != null) { + LastError = e; + RevisionFailed(); + } else { + if (results.Count != 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: + var docsToSend = new List (); + var revsToSend = new RevisionList(); + foreach (var rev in inbox) { + // Is this revision in the server's 'missing' list? + IDictionary properties = null; + var revResults = results.Get(rev.GetDocId()).AsDictionary(); + if (revResults == null) { + continue; + } + + var revs = revResults.Get("missing").AsList(); + if (revs == null || !revs.Any( id => id.Equals(rev.GetRevId(), StringComparison.OrdinalIgnoreCase))) { + RemovePending(rev); + continue; + } + + RevisionInternal loadedRev; + try { + loadedRev = LocalDatabase.LoadRevisionBody (rev); + properties = new Dictionary(rev.GetProperties()); + } catch (CouchbaseLiteException e1) { + Log.W(Tag, string.Format("{0} Couldn't get local contents of {1}", rev, this), e1); + RevisionFailed(); + continue; + } + + if(loadedRev.GetProperties().GetCast("_removed")) { + // Filter out _removed revision + RemovePending(rev); + continue; + } + + var populatedRev = TransformRevision(loadedRev); + IList possibleAncestors = null; + if (revResults.ContainsKey("possible_ancestors")) { + possibleAncestors = revResults["possible_ancestors"].AsList(); + } + + properties = new Dictionary(populatedRev.GetProperties()); + var history = LocalDatabase.GetRevisionHistory(populatedRev, possibleAncestors); + properties["_revisions"] = Database.MakeRevisionHistoryDict(history); + 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: + var minRevPos = FindCommonAncestor(populatedRev, possibleAncestors); + Status status = new Status(); + if(!LocalDatabase.ExpandAttachments(populatedRev, minRevPos + 1, !dontSendMultipart, false, status)) { + Log.W(Tag, "Error expanding attachments!"); + RevisionFailed(); + continue; + } + + properties = populatedRev.GetProperties(); + if (!dontSendMultipart && UploadMultipartRevision(populatedRev)) { + SafeIncrementCompletedChangesCount(); + continue; + } + } + + if (properties == null || !properties.ContainsKey("_id")) { + throw new InvalidOperationException("properties must contain a document _id"); + } + + // Add the _revisions list: + revsToSend.Add(rev); + + //now add it to the docs to send + docsToSend.AddItem (properties); + } + + UploadBulkDocs(docsToSend, revsToSend); + } else { + foreach (var revisionInternal in inbox) { + RemovePending(revisionInternal); + } + } + } + } catch (Exception ex) { + Log.E(Tag, "Unhandled exception in Pusher.ProcessInbox", ex); + } finally { + Log.D(Tag, "processInbox() calling AsyncTaskFinished()"); + AsyncTaskFinished(1); + } + }); + }*/ + private void UploadBulkDocs(IList docsToSend, RevisionList revChanges) { // Post the revisions to the destination. "new_edits":false means that the server should @@ -617,8 +745,8 @@ internal override void ProcessInbox(RevisionList inbox) } properties = new Dictionary(populatedRev.GetProperties()); - var revisions = LocalDatabase.Storage.GetRevisionHistory(populatedRev, possibleAncestors); - properties["_revisions"] = revisions; + var history = LocalDatabase.GetRevisionHistory(populatedRev, possibleAncestors); + properties["_revisions"] = Database.MakeRevisionHistoryDict(history); populatedRev.SetProperties(properties); // Strip any attachments already known to the target db: diff --git a/src/Couchbase.Lite.Shared/Revisions/SavedRevision.cs b/src/Couchbase.Lite.Shared/Revisions/SavedRevision.cs index 596d5e6ed..de9c3d435 100644 --- a/src/Couchbase.Lite.Shared/Revisions/SavedRevision.cs +++ b/src/Couchbase.Lite.Shared/Revisions/SavedRevision.cs @@ -156,7 +156,7 @@ public override String ParentId { public override IEnumerable RevisionHistory { get { var revisions = new List(); - var internalRevisions = Database.Storage.GetRevisionHistory(RevisionInternal); + var internalRevisions = Database.Storage.GetRevisionHistory(RevisionInternal, null); foreach (var internalRevision in internalRevisions) { diff --git a/src/Couchbase.Lite.Shared/Store/SqliteCouchStore.cs b/src/Couchbase.Lite.Shared/Store/SqliteCouchStore.cs index 7a1b749a2..ec67b2694 100644 --- a/src/Couchbase.Lite.Shared/Store/SqliteCouchStore.cs +++ b/src/Couchbase.Lite.Shared/Store/SqliteCouchStore.cs @@ -358,7 +358,7 @@ internal static string ParseRevIDSuffix(string rev) internal IDictionary GetRevisionHistoryDictStartingFromAnyAncestor(RevisionInternal rev, IListancestorRevIDs) { - var history = GetRevisionHistory(rev); // This is in reverse order, newest ... oldest + var history = GetRevisionHistory(rev, null); // This is in reverse order, newest ... oldest if (ancestorRevIDs != null && ancestorRevIDs.Any()) { for (var i = 0; i < history.Count; i++) @@ -1234,7 +1234,7 @@ public RevisionInternal GetParentRevision(RevisionInternal rev) return result; } - public IList GetRevisionHistory(RevisionInternal rev) + public IList GetRevisionHistory(RevisionInternal rev, ICollection ancestorRevIds) { string docId = rev.GetDocId(); string revId = rev.GetRevId(); @@ -1272,6 +1272,10 @@ public IList GetRevisionHistory(RevisionInternal rev) if(lastSequence == 0) { return false; } + + if(ancestorRevIds != null && ancestorRevIds.Contains(revId)) { + return false; + } } return true; @@ -1285,7 +1289,7 @@ public IList GetRevisionHistory(RevisionInternal rev) return history; } - public IDictionary GetRevisionHistory(RevisionInternal rev, IList ancestorRevIds) + public IDictionary GetRevisionHistoryDict(RevisionInternal rev, IList ancestorRevIds) { string docId = rev.GetDocId(); string revId = rev.GetRevId(); diff --git a/src/Couchbase.Lite.Tests.Shared/CRUDOperationsTest.cs b/src/Couchbase.Lite.Tests.Shared/CRUDOperationsTest.cs index 68864d1e0..f050ff969 100644 --- a/src/Couchbase.Lite.Tests.Shared/CRUDOperationsTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/CRUDOperationsTest.cs @@ -169,7 +169,7 @@ public void TestCRUDOperations() Assert.IsTrue(changeRevisions.Count == 1); // Get Revision History: - IList history = database.Storage.GetRevisionHistory(revD); + IList history = database.Storage.GetRevisionHistory(revD, null); Assert.AreEqual(revD, history[0]); Assert.AreEqual(rev2, history[1]); Assert.AreEqual(rev1, history[2]); diff --git a/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs b/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs index 757fc3dec..71fe7f69e 100644 --- a/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs @@ -406,6 +406,92 @@ public void TestPusherChangedEvent() Assert.AreEqual(ReplicationStatus.Stopped, statusHistory[1]); } + [Test] // Issue #449 + public void TestPushAttachmentToCouchDB() + { + const string dbName = "db"; + var dbUri = new Uri("http://localhost:5984/" + dbName); + + try { + HttpWebRequest.Create(dbUri).GetResponse(); + } catch(Exception) { + Assert.Inconclusive("Apache CouchDB not running"); + } + + + var deleteRequest = HttpWebRequest.Create(dbUri); + deleteRequest.Method = "DELETE"; + try { + var response = (HttpWebResponse)deleteRequest.GetResponse(); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } catch(WebException ex) { + if (ex.Status == WebExceptionStatus.ProtocolError) { + var response = ex.Response as HttpWebResponse; + if (response != null) { + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + } else { + Assert.Fail("Error from CouchDB: {0}", response.StatusCode); + } + } else { + Assert.Fail("Error from CouchDB: {0}", ex); + } + } + + var putRequest = HttpWebRequest.Create(dbUri); + putRequest.Method = "PUT"; + var putResponse = (HttpWebResponse)putRequest.GetResponse(); + Assert.AreEqual(HttpStatusCode.Created, putResponse.StatusCode); + + var push = database.CreatePushReplication(dbUri); + CreateDocuments(database, 2); + var attachDoc = database.CreateDocument(); + var newRev = attachDoc.CreateRevision(); + var newProps = newRev.UserProperties; + newProps["foo"] = "bar"; + newRev.SetUserProperties(newProps); + var attachmentStream = GetAsset("attachment.png"); + newRev.SetAttachment("attachment.png", "image/png", attachmentStream); + newRev.Save(); + + RunReplication(push); + Assert.AreEqual(3, push.ChangesCount); + Assert.AreEqual(3, push.CompletedChangesCount); + attachDoc = database.GetExistingDocument(attachDoc.Id); + attachDoc.Update(rev => + { + var props = rev.UserProperties; + props["extraminutes"] = "5"; + rev.SetUserProperties(props); + return true; + }); + + push = database.CreatePushReplication(push.RemoteUrl); + RunReplication(push); + + database.Close(); + database = EnsureEmptyDatabase(database.Name); + var pull = database.CreatePullReplication(push.RemoteUrl); + RunReplication(pull); + Assert.AreEqual(3, database.DocumentCount); + attachDoc = database.GetExistingDocument(attachDoc.Id); + Assert.IsNotNull(attachDoc, "Failed to retrieve doc with attachment"); + Assert.IsNotNull(attachDoc.CurrentRevision.Attachments, "Failed to retrieve attachments on attachment doc"); + attachDoc.Update(rev => + { + var props = rev.UserProperties; + props["extraminutes"] = "10"; + rev.SetUserProperties(props); + return true; + }); + + push = database.CreatePushReplication(pull.RemoteUrl); + RunReplication(push); + Assert.IsNull(push.LastError); + Assert.AreEqual(1, push.ChangesCount); + Assert.AreEqual(1, push.CompletedChangesCount); + Assert.AreEqual(3, database.DocumentCount); + } + // Reproduces issue #167 // https://github.com/couchbase/couchbase-lite-android/issues/167 /// diff --git a/src/Couchbase.Lite.Tests.Shared/RevTreeTest.cs b/src/Couchbase.Lite.Tests.Shared/RevTreeTest.cs index 782506786..e948c32b3 100644 --- a/src/Couchbase.Lite.Tests.Shared/RevTreeTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/RevTreeTest.cs @@ -271,7 +271,7 @@ private void VerifyHistory(Database db, RevisionInternal rev, IList hist Assert.AreEqual(rev, gotRev); AssertPropertiesAreEqual(rev.GetProperties(), gotRev.GetProperties()); - var revHistory = db.Storage.GetRevisionHistory(gotRev); + var revHistory = db.Storage.GetRevisionHistory(gotRev, null); Assert.AreEqual(history.Count, revHistory.Count); for (int i = 0; i < history.Count; i++) diff --git a/src/ListenerComponent/Couchbase.Lite.Listener.Shared/PeerToPeer/DocumentMethods.cs b/src/ListenerComponent/Couchbase.Lite.Listener.Shared/PeerToPeer/DocumentMethods.cs index 7130303c4..9a673c0b6 100644 --- a/src/ListenerComponent/Couchbase.Lite.Listener.Shared/PeerToPeer/DocumentMethods.cs +++ b/src/ListenerComponent/Couchbase.Lite.Listener.Shared/PeerToPeer/DocumentMethods.cs @@ -501,7 +501,7 @@ internal static RevisionInternal ApplyOptions(DocumentContentOptions options, Re } if (options.HasFlag(DocumentContentOptions.IncludeRevsInfo)) { - dst["_revs_info"] = db.Storage.GetRevisionHistory(rev).Select(x => + dst["_revs_info"] = db.Storage.GetRevisionHistory(rev, null).Select(x => { string status = "available"; if(x.IsDeleted()) {