/// <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); } }
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(); }
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); }
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; }
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; }
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); }
/// <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); }
internal IDictionary<string, object> GetAttachmentsFromDoc(string docId, string revId) { var rev = new RevisionInternal(docId, revId, false); LoadRevisionBody(rev); return rev.GetAttachments(); }
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); }