internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRevId, Status status) { if (status == null) { status = new Status(); } status.Code = StatusCode.Ok; var revAttachments = rev.GetAttachments(); if (revAttachments == null) { return true; // no-op: no attachments } // Deletions can't have attachments: if (rev.IsDeleted() || revAttachments.Count == 0) { var body = rev.GetProperties(); body.Remove("_attachments"); rev.SetProperties(body); return true; } int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1; IDictionary<string, object> parentAttachments = null; return rev.MutateAttachments((name, attachInfo) => { AttachmentInternal attachment = null; try { attachment = new AttachmentInternal(name, attachInfo); } catch(CouchbaseLiteException e) { return null; } if(attachment.EncodedContent != null) { // If there's inline attachment data, decode and store it: BlobKey blobKey = new BlobKey(); if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) { status.Code = StatusCode.AttachmentError; return null; } attachment.BlobKey = blobKey; } else if(attachInfo.GetCast<bool>("follows")) { // "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); } else if(attachInfo.GetCast<bool>("stub")) { // "stub" on an incoming revision means the attachment is the same as in the parent. 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; } return null; } } var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>(); if(parentAttachment == null) { status.Code = StatusCode.BadAttachment; return null; } return parentAttachment; } // Set or validate the revpos: if(attachment.RevPos == 0) { attachment.RevPos = generation; } else if(attachment.RevPos >= generation) { status.Code = StatusCode.BadAttachment; return null; } Debug.Assert(attachment.IsValid); return attachment.AsStubDictionary(); }); }
internal RevisionInternal TransformRevision(RevisionInternal rev) { if (RevisionBodyTransformationFunction != null) { try { var generation = rev.GetGeneration(); var xformed = RevisionBodyTransformationFunction(rev); if (xformed == null) { return null; } if (xformed != rev) { Debug.Assert((xformed.GetDocId().Equals(rev.GetDocId()))); Debug.Assert((xformed.GetRevId().Equals(rev.GetRevId()))); Debug.Assert((xformed.GetProperties().Get("_revisions").Equals(rev.GetProperties().Get("_revisions")))); if (xformed.GetProperties().ContainsKey("_attachments")) { // Insert 'revpos' properties into any attachments added by the callback: var mx = new RevisionInternal(xformed.GetProperties()); xformed = mx; mx.MutateAttachments((name, info) => { if (info.Get("revpos") != null) { return info; } if (info.Get("data") == null) { throw new InvalidOperationException("Transformer added attachment without adding data"); } var newInfo = new Dictionary<string, object>(info); newInfo["revpos"] = generation; return newInfo; }); } } } catch (Exception e) { Log.W(TAG, String.Format("Exception transforming a revision of doc '{0}'", rev.GetDocId()), e); } } return rev; }
internal static void StubOutAttachmentsInRevBeforeRevPos(RevisionInternal rev, long minRevPos, bool attachmentsFollow) { if (minRevPos <= 1 && !attachmentsFollow) { return; } rev.MutateAttachments((name, attachment) => { var revPos = 0L; if (attachment.ContainsKey("revpos")) { revPos = Convert.ToInt64(attachment["revpos"]); } var includeAttachment = (revPos == 0 || revPos >= minRevPos); var stubItOut = !includeAttachment && (!attachment.ContainsKey("stub") || (bool)attachment["stub"] == false); var addFollows = includeAttachment && attachmentsFollow && (!attachment.ContainsKey("follows") || (bool)attachment["follows"] == false); if (!stubItOut && !addFollows) { return attachment; // no change } // Need to modify attachment entry var editedAttachment = new Dictionary<string, object>(attachment); editedAttachment.Remove("data"); if (stubItOut) { // ...then remove the 'data' and 'follows' key: editedAttachment.Remove("follows"); editedAttachment["stub"] = true; Log.V(TAG, String.Format("Stubbed out attachment {0}: revpos {1} < {2}", rev, revPos, minRevPos)); } else if (addFollows) { editedAttachment.Remove("stub"); editedAttachment["follows"] = true; Log.V(TAG, String.Format("Added 'follows' for attachment {0}: revpos {1} >= {2}", rev, revPos, minRevPos)); } return editedAttachment; }); }
// Replaces the "follows" key with the real attachment data in all attachments to 'doc'. internal bool InlineFollowingAttachmentsIn(RevisionInternal rev) { return rev.MutateAttachments((s, attachment)=> { if (!attachment.ContainsKey("follows")) { return attachment; } var fileURL = FileForAttachmentDict(attachment); byte[] fileData = null; try { var inputStream = fileURL.OpenConnection().GetInputStream(); var os = new ByteArrayOutputStream(); inputStream.CopyTo(os); fileData = os.ToByteArray(); } catch (IOException e) { Log.E(TAG, "could not retrieve attachment data: {0}".Fmt(fileURL.ToString()), e); return null; } var editedAttachment = new Dictionary<string, object>(attachment); editedAttachment.Remove("follows"); editedAttachment.Put("data", Convert.ToBase64String(fileData)); return editedAttachment; }); }
// Replaces attachment data whose revpos is < minRevPos with stubs. // If attachmentsFollow==YES, replaces data with "follows" key. private static void StubOutAttachmentsInRevBeforeRevPos(RevisionInternal rev, int minRevPos, bool attachmentsFollow) { if (minRevPos <= 1 && !attachmentsFollow) { return; } rev.MutateAttachments((s, attachment)=> { var revPos = 0; if (attachment.Get("revpos") != null) { revPos = (int)attachment.Get("revpos"); } var includeAttachment = (revPos == 0 || revPos >= minRevPos); var stubItOut = !includeAttachment && (attachment.Get("stub") == null || (bool)attachment.Get("stub") == false); var addFollows = includeAttachment && attachmentsFollow && (attachment.Get("follows") == null || !(bool)attachment.Get ("follows")); if (!stubItOut && !addFollows) { return attachment; } // no change // Need to modify attachment entry: var editedAttachment = new Dictionary<string, object>(attachment); editedAttachment.Remove("data"); if (stubItOut) { // ...then remove the 'data' and 'follows' key: editedAttachment.Remove("follows"); editedAttachment.Put("stub", true); Log.V(Tag, "Stubbed out attachment {0}: revpos {1} < {2}".Fmt(rev, revPos, minRevPos)); } else { if (addFollows) { editedAttachment.Remove("stub"); editedAttachment.Put("follows", true); Log.V(Tag, "Added 'follows' for attachment {0}: revpos {1} >= {2}".Fmt(rev, revPos, minRevPos)); } } return editedAttachment; }); }
internal bool ExpandAttachments(RevisionInternal rev, int minRevPos, bool allowFollows, bool decodeAttachments, Status outStatus) { outStatus.Code = StatusCode.Ok; rev.MutateAttachments((name, attachment) => { var revPos = attachment.GetCast<long>("revpos"); if(revPos < minRevPos && revPos != 0) { //Stub: return new Dictionary<string, object> { { "stub", true }, { "revpos", revPos } }; } var expanded = new Dictionary<string, object>(attachment); expanded.Remove("stub"); if(decodeAttachments) { expanded.Remove("encoding"); expanded.Remove("encoded_length"); } if(allowFollows && SmallestLength(expanded) >= Database.BIG_ATTACHMENT_LENGTH) { //Data will follow (multipart): expanded["follows"] = true; expanded.Remove("data"); } else { //Put data inline: expanded.Remove("follows"); Status status = new Status(); var attachObj = AttachmentForDict(attachment, name, status); if(attachObj == null) { Log.W(TAG, "Can't get attachment '{0}' of {1} (status {2})", name, rev, status); outStatus.Code = status.Code; return attachment; } var data = decodeAttachments ? attachObj.Content : attachObj.EncodedContent; if(data == null) { Log.W(TAG, "Can't get binary data of attachment '{0}' of {1}", name, rev); outStatus.Code = StatusCode.NotFound; return attachment; } expanded["data"] = Convert.ToBase64String(data.ToArray()); } return expanded; }); return outStatus.Code == StatusCode.Ok; }
internal bool ProcessAttachmentsForRevision(RevisionInternal rev, IList<string> ancestry) { var revAttachments = rev.GetAttachments(); if (revAttachments == null) { return true; // no-op: no attachments } // Deletions can't have attachments: if (rev.IsDeleted() || revAttachments.Count == 0) { var body = rev.GetProperties(); body.Remove("_attachments"); rev.SetProperties(body); 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) => { AttachmentInternal attachment = null; try { attachment = new AttachmentInternal(name, attachInfo); } catch(CouchbaseLiteException) { return null; } if(attachment.EncodedContent != null) { // If there's inline attachment data, decode and store it: BlobKey blobKey = new BlobKey(); if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) { throw new CouchbaseLiteException( String.Format("Failed to write attachment ' {0}'to disk", name), StatusCode.AttachmentError); } attachment.BlobKey = blobKey; } else if(attachInfo.GetCast<bool>("follows")) { // "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); } else if(attachInfo.GetCast<bool>("stub")) { // "stub" on an incoming revision means the attachment is the same as in the parent. if(parentAttachments == null && prevRevId != null) { parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId); if(parentAttachments == null) { 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 return attachInfo; } var ancestorAttachment = FindAttachment(name, attachment.RevPos, rev.GetDocId(), ancestry); if(ancestorAttachment != null) { return ancestorAttachment; } throw new CouchbaseLiteException( String.Format("Unable to find 'stub' attachment {0} in history", name), StatusCode.BadAttachment); } } var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>(); if(parentAttachment == null) { throw new CouchbaseLiteException( String.Format("Unable to find 'stub' attachment {0} in history", name), StatusCode.BadAttachment); } return parentAttachment; } // Set or validate the revpos: if(attachment.RevPos == 0) { attachment.RevPos = generation; } else if(attachment.RevPos > generation) { throw new CouchbaseLiteException( String.Format("Attachment specifies revision generation {0} but document is only at revision generation {1}", attachment.RevPos, generation), StatusCode.BadAttachment); } Debug.Assert(attachment.IsValid); return attachment.AsStubDictionary(); }); }
internal void ExpandAttachments(RevisionInternal rev, int minRevPos, bool allowFollows, bool decodeAttachments) { if (!IsOpen) { throw new CouchbaseLiteException("DB is closed", StatusCode.DbError); } rev.MutateAttachments((name, attachment) => { var revPos = attachment.GetCast<long>("revpos"); if(revPos < minRevPos && revPos != 0) { //Stub: return new Dictionary<string, object> { { "stub", true }, { "revpos", revPos } }; } var expanded = new Dictionary<string, object>(attachment); expanded.Remove("stub"); if(decodeAttachments) { expanded.Remove("encoding"); expanded.Remove("encoded_length"); } if(allowFollows && SmallestLength(expanded) >= Database.BIG_ATTACHMENT_LENGTH) { //Data will follow (multipart): expanded["follows"] = true; expanded.Remove("data"); } else { //Put data inline: expanded.Remove("follows"); var attachObj = AttachmentForDict(attachment, name); var data = decodeAttachments ? attachObj.Content : attachObj.EncodedContent; if(data == null) { Log.W(TAG, "Can't get binary data of attachment '{0}' of {1}", name, rev); return attachment; } expanded["data"] = Convert.ToBase64String(data.ToArray()); } return expanded; }); }
internal RevisionInternal TransformRevision(RevisionInternal rev) { if (RevisionBodyTransformationFunction != null) { try { var generation = rev.Generation; var xformed = RevisionBodyTransformationFunction(rev); if (xformed == null) { return null; } if (xformed != rev) { Debug.Assert((xformed.DocID.Equals(rev.DocID))); Debug.Assert((xformed.RevID.Equals(rev.RevID))); Debug.Assert((xformed.GetProperties().Get("_revisions").Equals(rev.GetProperties().Get("_revisions")))); if (xformed.GetProperties().ContainsKey("_attachments")) { // Insert 'revpos' properties into any attachments added by the callback: var mx = new RevisionInternal(xformed.GetProperties()); xformed = mx; mx.MutateAttachments((name, info) => { if (info.Get("revpos") != null) { return info; } if (info.Get("data") == null) { throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.InternalServerError, Tag, "Transformer added attachment without adding data"); } var newInfo = new Dictionary<string, object>(info); newInfo["revpos"] = generation; return newInfo; }); } } } catch (Exception e) { Log.To.Sync.W(Tag, String.Format("Exception transforming a revision of doc '{0}', aborting...", new SecureLogString(rev.DocID, LogMessageSensitivity.PotentiallyInsecure)), e); } } return rev; }
internal bool ProcessAttachmentsForRevision(RevisionInternal rev, IList<RevisionID> ancestry) { var revAttachments = rev.GetAttachments(); if(revAttachments == null) { return true; // no-op: no attachments } // Deletions can't have attachments: if(rev.Deleted || revAttachments.Count == 0) { var body = rev.GetProperties(); body.Remove("_attachments"); rev.SetProperties(body); return true; } var prevRevId = ancestry != null && ancestry.Count > 0 ? ancestry[0] : null; int generation = rev.Generation; IDictionary<string, object> parentAttachments = null; return rev.MutateAttachments((name, attachInfo) => { AttachmentInternal attachment = null; try { attachment = new AttachmentInternal(name, attachInfo); } catch(CouchbaseLiteException) { Log.To.Database.W(TAG, "Error creating attachment object for '{0}' ('{1}'), " + "returning null", new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure), new SecureLogJsonString(attachInfo, LogMessageSensitivity.PotentiallyInsecure)); return null; } if(attachment.EncodedContent != null) { // If there's inline attachment data, decode and store it: BlobKey blobKey = new BlobKey(); try { Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey); } catch(CouchbaseLiteException) { Log.To.Database.E(TAG, "Failed to write attachment '{0}' to disk, rethrowing...", name); throw; } catch(Exception e) { throw Misc.CreateExceptionAndLog(Log.To.Database, e, TAG, "Exception during attachment writing '{0}'", new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure)); } attachment.BlobKey = blobKey; } else if(attachInfo.GetCast<bool>("follows")) { // "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); } else if(attachInfo.GetCast<bool>("stub")) { // "stub" on an incoming revision means the attachment is the same as in the parent. if(parentAttachments == null && prevRevId != null) { parentAttachments = GetAttachmentsFromDoc(rev.DocID, prevRevId); if(parentAttachments == null) { 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 return attachInfo; } var ancestorAttachment = FindAttachment(name, attachment.RevPos, rev.DocID, ancestry); if(ancestorAttachment != null) { return ancestorAttachment; } throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "Unable to find 'stub' attachment {0} in history (1)", new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure)); } } var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>(); if(parentAttachment == null) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "Unable to find 'stub' attachment {0} in history (2)", new SecureLogString(name, LogMessageSensitivity.PotentiallyInsecure)); } return parentAttachment; } // Set or validate the revpos: if(attachment.RevPos == 0) { attachment.RevPos = generation; } else if(attachment.RevPos > generation) { throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadAttachment, TAG, "Attachment specifies revision generation {0} but document is only at revision generation {1}", attachment.RevPos, generation); } Debug.Assert(attachment.IsValid); return attachment.AsStubDictionary(); }); }
internal bool RegisterAttachmentBodies(IDictionary<string, Stream> attachments, RevisionInternal rev) { var ok = true; rev.MutateAttachments((name, meta) => { var value = attachments.Get(name); if(value != null) { // Register attachment body with database var writer = AttachmentWriter; try { writer.Read(value); writer.Finish(); } catch(Exception e) { Log.To.Database.W(TAG, $"Error reading stream for attachment {name}, skipping...", e); ok = false; return null; } // Make attachment mode "follows", indicating the data is registered var nuMeta = new Dictionary<string, object>(meta); nuMeta.Remove("data"); nuMeta.Remove("stub"); nuMeta["follows"] = true; // Add or verify metadata "digest" property var digest = meta.GetCast<string>("digest"); var sha1Digest = writer.SHA1DigestString(); if(digest != null) { if(digest != sha1Digest && digest != writer.MD5DigestString()) { Log.To.Database.W(TAG, "Attachment '{0}' body digest ({1}) doesn't match " + "'digest' property {2}", name, sha1Digest, digest); ok = false; return null; } } else { nuMeta["digest"] = digest = sha1Digest; } RememberAttachmentWriter(writer, digest); return nuMeta; } return meta; }); return ok; }