Skip to content

Commit

Permalink
Closes #479
Browse files Browse the repository at this point in the history
  • Loading branch information
borrrden committed Aug 22, 2015
1 parent 76628a4 commit 5cf4a2a
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 12 deletions.
49 changes: 42 additions & 7 deletions src/Couchbase.Lite.Shared/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -704,9 +704,8 @@ internal void ForceInsert(RevisionInternal inRev, IList<string> revHistory, Uri

if (inRev.GetAttachments() != null) {
var updatedRev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId());
string prevRevID = revHistory.Count >= 2 ? revHistory[1] : null;
Status status = new Status();
if (!ProcessAttachmentsForRevision(updatedRev, prevRevID, status)) {
if (!ProcessAttachmentsForRevision(updatedRev, revHistory.Skip(1).Take(revHistory.Count-1).ToList(), status)) {
throw new CouchbaseLiteException(status.Code);
}

Expand Down Expand Up @@ -1186,7 +1185,7 @@ internal RevisionInternal PutDocument(string docId, IDictionary<string, object>
if (properties != null && properties.Get("_attachments").AsDictionary<string, object>() != null) {
var tmpRev = new RevisionInternal(docId, prevRevId, deleting);
tmpRev.SetProperties(properties);
if (!ProcessAttachmentsForRevision(tmpRev, prevRevId, resultStatus)) {
if (!ProcessAttachmentsForRevision(tmpRev, prevRevId == null ? null : new List<string> { prevRevId }, resultStatus)) {
return null;
}

Expand Down Expand Up @@ -1557,7 +1556,7 @@ internal bool InlineFollowingAttachmentsIn(RevisionInternal rev)
});
}

internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRevId, Status status)
internal bool ProcessAttachmentsForRevision(RevisionInternal rev, IList<string> ancestry, Status status)
{
if (status == null) {
status = new Status();
Expand All @@ -1577,6 +1576,7 @@ internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRev
return true;
}

var prevRevId = ancestry != null && ancestry.Count > 0 ? ancestry[0] : null;
int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1;
IDictionary<string, object> parentAttachments = null;
return rev.MutateAttachments((name, attachInfo) =>
Expand Down Expand Up @@ -1607,12 +1607,22 @@ internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRev
if(parentAttachments == null && prevRevId != null) {
parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId, status);
if(parentAttachments == null) {
if(status.Code == StatusCode.Ok || status.Code == StatusCode.NotFound) {
status.Code = StatusCode.BadAttachment;
if(Attachments.HasBlobForKey(attachment.BlobKey)) {
// Parent revision's body isn't known (we are probably pulling a rev along
// with its entire history) but it's OK, we have the attachment already
status.Code = StatusCode.Ok;
return attachInfo;
}
return null;
var ancestorAttachment = FindAttachment(name, attachment.RevPos, rev.GetDocId(), ancestry);
if(ancestorAttachment != null) {
status.Code = StatusCode.Ok;
return ancestorAttachment;
}
}
status.Code = StatusCode.BadAttachment;
return null;
}
var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>();
Expand All @@ -1638,6 +1648,31 @@ internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRev
});
}

internal IDictionary<string, object> FindAttachment(string name, int revPos, string docId, IList<string> ancestry)
{
if (ancestry == null) {
return null;
}

for (var i = ancestry.Count - 1; i >= 0; i--) {
var revID = ancestry[i];
if (RevisionInternal.GenerationFromRevID(revID) >= revPos) {
var status = new Status();
var attachments = GetAttachmentsFromDoc(docId, revID, status);
if (attachments == null) {
continue;
}

var attachment = attachments.Get(name).AsDictionary<string, object>();
if (attachment != null) {
return attachment;
}
}
}

return null;
}

internal IDictionary<string, object> GetAttachmentsFromDoc(string docId, string revId, Status status)
{
var rev = new RevisionInternal(docId, revId, false);
Expand Down
6 changes: 1 addition & 5 deletions src/Couchbase.Lite.Shared/Store/BlobKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,7 @@ private static byte[] DecodeBase64Digest(string base64Digest)
const string expectedPrefix = "sha1-";
var prefixLength = expectedPrefix.Length;
if (!base64Digest.StartsWith(expectedPrefix, StringComparison.Ordinal)) {
Log.I(TAG, "{0} does not start with sha1-", base64Digest);
prefixLength = base64Digest.IndexOf('-') + 1;
if (prefixLength == -1) {
throw new ArgumentException(String.Format("{0} is not a valid Base64 digest.", base64Digest));
}
return Enumerable.Repeat<byte>(0, 20).ToArray(); // MD5 is no longer valid
}

base64Digest = base64Digest.Remove(0, prefixLength);
Expand Down
5 changes: 5 additions & 0 deletions src/Couchbase.Lite.Shared/Store/BlobStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ public static BlobKey KeyForBlobFromFile(FileInfo file)
return result;
}

public bool HasBlobForKey(BlobKey key)
{
return File.Exists(PathForKey(key));
}

public string PathForKey(BlobKey key)
{
return path + FilePath.separator + key + FileExtension;
Expand Down
80 changes: 80 additions & 0 deletions src/Couchbase.Lite.Tests.Shared/AttachmentsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,86 @@ public class AttachmentsTest : LiteTestCase
{
public const string Tag = "Attachments";

[Test]
public void TestFollowWithRevPos()
{
var attachInfo = new Dictionary<string, object> {
{ "content_type", "text/plain" },
{ "digest", "md5-DaUdFsLh8FKLbcBIDlU57g==" },
{ "follows", true },
{ "length", 51200 },
{ "revpos", 2 }
};

var attachment = default(AttachmentInternal);
Assert.DoesNotThrow(() => attachment = new AttachmentInternal("attachment", attachInfo));
var stub = attachment.AsStubDictionary();

var expected = new Dictionary<string, object> {
{ "content_type", "text/plain" },
{ "digest", "sha1-AAAAAAAAAAAAAAAAAAAAAAAAAAA=" },
{ "stub", true },
{ "length", 51200 },
{ "revpos", 2 }
};

AssertDictionariesAreEqual(expected, stub);
}

[Test]
public void TestIntermediateDeletedRevs()
{
// Put a revision that includes an _attachments dict:
var attach1 = Encoding.UTF8.GetBytes("This is the body of attach1");
var base64 = Convert.ToBase64String(attach1);

var attachDict = new Dictionary<string, object> {
{ "attach", new Dictionary<string, object> {
{ "content_type", "text/plain"},
{ "data", base64 }
}
}
};

IDictionary<string, object> props = new Dictionary<string, object> {
{ "_id", "X" },
{ "_attachments", attachDict }
};

var status = new Status();
var rev1 = default(RevisionInternal);
Assert.DoesNotThrow(() => rev1 = database.PutRevision(new RevisionInternal(props), null, false, status));
Assert.AreEqual(StatusCode.Created, status.Code);
Assert.AreEqual(1L, rev1.GetAttachments().GetCast<IDictionary<string, object>>("attach").GetCast<long>("revpos"));

props = new Dictionary<string, object> {
{ "_id", rev1.GetDocId() },
{ "_deleted", true }
};

var rev2 = default(RevisionInternal);
Assert.DoesNotThrow(() => rev2 = database.PutRevision(new RevisionInternal(props), rev1.GetRevId(), status));
Assert.AreEqual(StatusCode.Ok, status.Code);
Assert.IsTrue(rev2.IsDeleted());

// Insert a revision several generations advanced but which hasn't changed the attachment:
var rev3 = rev1.CopyWithDocID(rev1.GetDocId(), "3-3333");
props = rev3.GetProperties();
props["foo"] = "bar";
rev3.SetProperties(props);
rev3.MutateAttachments((name, att) =>
{
var nuAtt = new Dictionary<string, object>(att);
nuAtt.Remove("data");
nuAtt["stub"] = true;
nuAtt["digest"] = "md5-deadbeef";
return nuAtt;
});

var history = new List<string> { rev3.GetRevId(), rev2.GetRevId(), rev1.GetRevId() };
Assert.DoesNotThrow(() => database.ForceInsert(rev3, history, null));
}

[Test]
public void TestUpgradeMD5()
{
Expand Down

0 comments on commit 5cf4a2a

Please sign in to comment.