Example #1
0
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException">When attempting to add an invalid revision</exception>
        internal void ForceInsert(RevisionInternal inRev, IList<string> revHistory, Uri source)
        {
            if (revHistory == null) {
                revHistory = new List<string>(0);
            }

            var rev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId());
            rev.SetSequence(0);
            string revID = rev.GetRevId();
            if (!IsValidDocumentId(rev.GetDocId()) || revID == null) {
                throw new CouchbaseLiteException(StatusCode.BadId);
            }

            if (revHistory.Count == 0) {
                revHistory.Add(revID);
            } else if (revID != revHistory[0]) {
                throw new CouchbaseLiteException(StatusCode.BadId);
            }

            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)) {
                    throw new CouchbaseLiteException(status.Code);
                }

                inRev = updatedRev;
            }

            StoreValidation validationBlock = null;
            if (Shared != null && Shared.HasValues("validation", Name)) {
                validationBlock = ValidateRevision;
            }

            var insertStatus = Storage.ForceInsert(inRev, revHistory, validationBlock, source);
            if(insertStatus.IsError) {
                throw new CouchbaseLiteException(insertStatus.Code);
            }
        }
Example #2
0
        internal IDictionary<string, object> GetAttachmentsFromDoc(string docId, string revId, Status status)
        {
            var rev = new RevisionInternal(docId, revId, false);
            try {
                LoadRevisionBody(rev);
            } catch(CouchbaseLiteException e) {
                status.Code = e.CBLStatus.Code;
                return null;
            }

            return rev.GetAttachments();
        }
Example #3
0
        internal AttachmentInternal GetAttachmentForRevision(RevisionInternal rev, string name, Status status = null)
        {
            Debug.Assert(name != null);
            var attachments = rev.GetAttachments();
            if (attachments == null) {
                try {
                    rev = LoadRevisionBody(rev);
                } catch(CouchbaseLiteException e) {
                    if (status != null) {
                        status.Code = e.CBLStatus.Code;
                    }

                    return null;
                }

                attachments = rev.GetAttachments();
                if (attachments == null) {
                    status.Code = StatusCode.NotFound;
                    return null;
                }
            }

            return AttachmentForDict(attachments.Get(name).AsDictionary<string, object>(), name, status);
        }
Example #4
0
        internal MultipartWriter MultipartWriterForRev(RevisionInternal rev, string contentType)
        {
            var writer = new MultipartWriter(contentType, null);
            writer.SetNextPartHeaders(new Dictionary<string, string> { { "Content-Type", "application/json" } });
            writer.AddData(rev.GetBody().AsJson());
            var attachments = rev.GetAttachments();
            if (attachments == null) {
                return writer;
            }

            foreach (var entry in attachments) {
                var attachment = entry.Value.AsDictionary<string, object>();
                if (attachment != null && attachment.GetCast<bool>("follows", false)) {
                    var disposition = String.Format("attachment; filename={0}", Quote(entry.Key));
                    writer.SetNextPartHeaders(new Dictionary<string, string> { { "Content-Disposition", disposition } });

                    Status status = new Status();
                    var attachObj = AttachmentForDict(attachment, entry.Key, status);
                    if (attachObj == null) {
                        return null;
                    }

                    var fileURL = attachObj.ContentUrl;
                    if (fileURL != null) {
                        writer.AddFileUrl(fileURL);
                    } else {
                        writer.AddStream(attachObj.ContentStream);
                    }
                }
            }

            return writer;
        }
Example #5
0
        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();
            });
        }
        public void ForceInsert(RevisionInternal inRev, IList<string> revHistory, StoreValidation validationBlock, Uri source)
        {
            if (_config.HasFlag(C4DatabaseFlags.ReadOnly)) {
                throw new CouchbaseLiteException("Attempting to write to a readonly database", StatusCode.Forbidden);
            }

            var json = Manager.GetObjectMapper().WriteValueAsString(inRev.GetProperties(), true);
            var change = default(DocumentChange);
            RunInTransaction(() =>
            {
                // First get the CBForest doc:
                WithC4Document(inRev.GetDocId(), null, false, true, doc =>
                {
                    ForestDBBridge.Check(err => Native.c4doc_insertRevisionWithHistory(doc, json, inRev.IsDeleted(), 
                        inRev.GetAttachments() != null, revHistory.ToArray(), err));

                    // Save updated doc back to the database:
                    var isWinner = SaveDocument(doc, revHistory[0], inRev.GetProperties());
                    inRev.SetSequence((long)doc->sequence);
                    change = ChangeWithNewRevision(inRev, isWinner, doc, source);
                });

                return true;
            });

            if (change != null && Delegate != null) {
                Delegate.DatabaseStorageChanged(change);
            }
        }
        public RevisionInternal PutRevision(string inDocId, string inPrevRevId, IDictionary<string, object> properties,
            bool deleting, bool allowConflict, StoreValidation validationBlock)
        {
            if(_config.HasFlag(C4DatabaseFlags.ReadOnly)) {
                throw new CouchbaseLiteException("Attempting to write to a readonly database", StatusCode.Forbidden);
            }

            var json = default(string);
            if (properties != null) {
                json = Manager.GetObjectMapper().WriteValueAsString(Database.StripDocumentJSON(properties), true);
            } else {
                json = "{}";
            }

            if (inDocId == null) {
                inDocId = Misc.CreateGUID();
            }

            var putRev = default(RevisionInternal);
            var change = default(DocumentChange);
            var success = RunInTransaction(() =>
            {
                var docId = inDocId;
                var prevRevId = inPrevRevId;
                var transactionSuccess = false;
                WithC4Document(docId, null, false, true, doc =>
                {
                    if(prevRevId != null) {
                        // Updating an existing revision; make sure it exists and is a leaf:
                        ForestDBBridge.Check(err => Native.c4doc_selectRevision(doc, prevRevId, false, err));
                        if(!allowConflict && !doc->selectedRev.IsLeaf) {
                            throw new CouchbaseLiteException(StatusCode.Conflict);
                        }
                    } else {
                        // No parent revision given:
                        if(deleting) {
                            // Didn't specify a revision to delete: NotFound or a Conflict, depending
                            throw new CouchbaseLiteException(doc->Exists ? StatusCode.Conflict : StatusCode.NotFound);
                        }

                        // If doc exists, current rev must be in a deleted state or there will be a conflict:
                        if(Native.c4doc_selectCurrentRevision(doc)) {
                            if(doc->selectedRev.IsDeleted) {
                                // New rev will be child of the tombstone:
                                prevRevId = (string)doc->revID;
                            } else {
                                throw new CouchbaseLiteException(StatusCode.Conflict);
                            }
                        }
                    }

                    // Compute the new revID. (Can't be done earlier because prevRevID may have changed.)
                    var newRevID = Delegate != null ? Delegate.GenerateRevID(Encoding.UTF8.GetBytes(json), deleting, prevRevId) : null;
                    if(newRevID == null) {
                        throw new CouchbaseLiteException(StatusCode.BadId);
                    }

                    putRev = new RevisionInternal(docId, newRevID, deleting);
                    if(properties != null) {
                        properties["_id"] = docId;
                        properties["_rev"] = newRevID;
                        putRev.SetProperties(properties);
                    }

                    // Run any validation blocks:
                    if(validationBlock != null) {
                        var prevRev = default(RevisionInternal);
                        if(prevRevId != null) {
                            prevRev = new RevisionInternal(docId, prevRevId, doc->selectedRev.IsDeleted);
                        }
                            
                        var status = validationBlock(putRev, prevRev, prevRevId);
                        if(status.IsError) {
                            throw new CouchbaseLiteException(String.Format("{0} failed validation", putRev), 
                                status.Code);
                        }
                    }

                    // Add the revision to the database:
                    ForestDBBridge.Check(err => Native.c4doc_insertRevision(doc, newRevID, json, deleting,
                        putRev.GetAttachments() != null, allowConflict, err));
                    var isWinner = SaveDocument(doc, newRevID, properties);
                    putRev.SetSequence((long)doc->sequence);
                    change = ChangeWithNewRevision(putRev, isWinner, doc, null);
                    transactionSuccess = true;
                });

                return transactionSuccess;
            });

            if (!success) {
                return null;
            }

            if (Delegate != null && change != null) {
                Delegate.DatabaseStorageChanged(change);
            }

            return putRev;
        }
Example #8
0
        internal AttachmentInternal GetAttachmentForRevision(RevisionInternal rev, string name)
        {
            Debug.Assert(name != null);
            var attachments = rev.GetAttachments();
            if (attachments == null) {
                rev = LoadRevisionBody(rev);
                attachments = rev.GetAttachments();
                if (attachments == null) {
                    return null;
                }
            }

            return AttachmentForDict(attachments.Get(name).AsDictionary<string, object>(), name);
        }
Example #9
0
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException">When attempting to add an invalid revision</exception>
        internal void ForceInsert(RevisionInternal inRev, IList<string> revHistory, Uri source)
        {
            if (!IsOpen) {
                throw new CouchbaseLiteException("DB is closed", StatusCode.DbError);
            }

            if (revHistory == null) {
                revHistory = new List<string>(0);
            }

            var rev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId());
            rev.SetSequence(0);
            string revID = rev.GetRevId();
            if (!Document.IsValidDocumentId(rev.GetDocId()) || revID == null) {
                throw new CouchbaseLiteException(StatusCode.BadId);
            }

            if (revHistory.Count == 0) {
                revHistory.Add(revID);
            } else if (revID != revHistory[0]) {
                throw new CouchbaseLiteException(StatusCode.BadId);
            }

            if (inRev.GetAttachments() != null) {
                var updatedRev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId());
                ProcessAttachmentsForRevision(updatedRev, revHistory.Skip(1).Take(revHistory.Count-1).ToList());
                inRev = updatedRev;
            }

            StoreValidation validationBlock = null;
            if (Shared != null && Shared.HasValues("validation", Name)) {
                validationBlock = ValidateRevision;
            }

            Storage.ForceInsert(inRev, revHistory, validationBlock, source);
        }
Example #10
0
 internal IDictionary<string, object> GetAttachmentsFromDoc(string docId, string revId)
 {
     var rev = new RevisionInternal(docId, revId, false);
     LoadRevisionBody(rev);
     return rev.GetAttachments();
 }
Example #11
0
        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();
            });
        }
        private MultipartWriter GetMultipartWriter(RevisionInternal rev, string boundary)
        {
            // Find all the attachments with "follows" instead of a body, and put 'em in a multipart stream.
            // It's important to scan the _attachments entries in the same order in which they will appear
            // in the JSON, because CouchDB expects the MIME bodies to appear in that same order
            var bodyStream = default(MultipartWriter);
            var attachments = rev.GetAttachments();
            foreach (var a in attachments) {
                var attachment = a.Value.AsDictionary<string, object>();
                if (attachment != null && attachment.GetCast<bool>("follows")) {
                    if (bodyStream == null) {
                        // Create the HTTP multipart stream:
                        bodyStream = new MultipartWriter("multipart/related", boundary);
                        bodyStream.SetNextPartHeaders(new Dictionary<string, string> { 
                            { "Content-Type", "application/json" } 
                        });

                        // Use canonical JSON encoder so that _attachments keys will be written in the
                        // same order that this for loop is processing the attachments.
                        var json = Manager.GetObjectMapper().WriteValueAsBytes(rev.GetProperties(), true);
                        if (CanSendCompressedRequests) {
                            bodyStream.AddGZippedData(json);
                        } else {
                            bodyStream.AddData(json);
                        }
                    }

                    // Add attachment as another MIME part:
                    var disposition = String.Format("attachment; filename={0}", Misc.QuoteString(a.Key));
                    var contentType = attachment.GetCast<string>("type");
                    var contentEncoding = attachment.GetCast<string>("encoding");
                    bodyStream.SetNextPartHeaders(new NonNullDictionary<string, string> {
                        { "Content-Disposition", disposition },
                        { "Content-Type", contentType },
                        { "Content-Encoding", contentEncoding }
                    });

                    var attachmentObj = default(AttachmentInternal);
                    try {
                        attachmentObj = LocalDatabase.AttachmentForDict(attachment, a.Key);
                    } catch(CouchbaseLiteException) {
                        return null;
                    }

                    bodyStream.AddStream(attachmentObj.ContentStream, attachmentObj.Length);
                }
            }

            return bodyStream;
        }
        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();
            });
        }
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException">When attempting to add an invalid revision</exception>
        internal void ForceInsert(RevisionInternal inRev, IList<RevisionID> revHistory, Uri source)
        {
            if(!IsOpen) {
                throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.DbError, TAG,
                    "Cannot perform ForceInsert on a closed database");
            }

            if(revHistory == null) {
                revHistory = new List<RevisionID>(0);
            } else {
                var tmp = revHistory.ToList();
                revHistory = tmp;
            }

            var rev = new RevisionInternal(inRev);
            rev.Sequence = 0;
            RevisionID revID = rev.RevID;
            if(!Document.IsValidDocumentId(rev.DocID) || revID == null) {
                if(rev == null) {
                    throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG,
                        "Cannot force insert a revision with a null revision ID");
                }

                throw Misc.CreateExceptionAndLog(Log.To.Database, StatusCode.BadId, TAG,
                    "{0} is not a valid document ID",
                    new SecureLogString(rev.DocID, LogMessageSensitivity.PotentiallyInsecure));
            }

            if(revHistory.Count == 0 || revID != revHistory[0]) {
                revHistory.Insert(0, revID);
            }

            if(inRev.GetAttachments() != null) {
                var updatedRev = new RevisionInternal(inRev);
                ProcessAttachmentsForRevision(updatedRev, revHistory.Skip(1).Take(revHistory.Count - 1).ToList());
                inRev = updatedRev;
            }

            StoreValidation validationBlock = null;
            if(Shared != null && Shared.HasValues("validation", Name)) {
                validationBlock = ValidateRevision;
            }

            Storage?.ForceInsert(inRev, revHistory, validationBlock, source);
        }