/// <summary> /// Uploads the supplied content as an attachment to the specified document. /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> /// <remarks> /// http://docs.couchdb.org/en/latest/api/document/attachments.html#put--db-docid-attname /// </remarks> public static ICouchbaseResponseState UpdateAttachment(ICouchbaseListenerContext context) { var state = new AsyncOpCouchbaseResponseState(); DatabaseMethods.PerformLogicWithDatabase(context, true, db => { var blob = db.AttachmentWriter; var httpBody = new byte[context.ContentLength]; context.BodyStream.ReadAsync(httpBody, 0, httpBody.Length).ContinueWith(t => { if (t.Result == 0) { state.Response = context.CreateResponse(StatusCode.BadAttachment); state.SignalFinished(); return; } blob.AppendData(httpBody); blob.Finish(); state.Response = UpdateAttachment(context, db, context.AttachmentName, context.DocumentName, blob); state.SignalFinished(); }); return(null); }); return(state); }
/// <summary> /// Returns the file attachment associated with the document. The raw data of the associated attachment is returned /// (just as if you were accessing a static file. The returned Content-Type will be the same as the content type /// set when the document attachment was submitted into the database. /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> /// <remarks> /// http://docs.couchdb.org/en/latest/api/document/attachments.html#get--db-docid-attname /// </remarks> public static ICouchbaseResponseState GetAttachment(ICouchbaseListenerContext context) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { Status status = new Status(); var revID = context.GetQueryParam("rev"); var rev = db.GetDocument(context.DocumentName, revID == null ? null : revID.AsRevID(), false, status); if (rev == null) { return context.CreateResponse(status.Code); } if (context.CacheWithEtag(rev.RevID.ToString())) { return context.CreateResponse(StatusCode.NotModified); } string acceptEncoding = context.RequestHeaders["Accept-Encoding"]; bool acceptEncoded = acceptEncoding != null && acceptEncoding.Contains("gzip") && context.RequestHeaders["Range"] == null; var attachment = db.GetAttachmentForRevision(rev, context.AttachmentName); if (attachment == null) { return context.CreateResponse(StatusCode.AttachmentNotFound); } var response = context.CreateResponse(); if (context.Method.Equals(HttpMethod.Head)) { var length = attachment.Length; if (acceptEncoded && attachment.Encoding == AttachmentEncoding.GZIP && attachment.EncodedLength > 0) { length = attachment.EncodedLength; } response["Content-Length"] = length.ToString(); } else { var contents = acceptEncoded ? attachment.EncodedContent : attachment.Content; if (contents == null) { response.InternalStatus = StatusCode.NotFound; return response; } response.BinaryBody = contents; } response["Content-Type"] = attachment.ContentType; if (acceptEncoded && attachment.Encoding == AttachmentEncoding.GZIP) { response["Content-Encoding"] = "gzip"; } return response; }).AsDefaultState()); }
/// <summary> /// Marks the specified document as deleted by adding a field _deleted with the value true. /// Documents with this field will not be returned within requests anymore, but stay in the database. /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> /// <remarks> /// http://docs.couchdb.org/en/latest/api/document/common.html#delete--db-docid /// </remarks> public static ICouchbaseResponseState DeleteDocument(ICouchbaseListenerContext context) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { string docId = context.DocumentName; return UpdateDb(context, db, docId, null, true); }).AsDefaultState()); }
// Factors out the logic of opening the database and reading the document body from the HTTP request // and performs the specified logic on the body received in the request, barring any problems private static CouchbaseLiteResponse PerformLogicWithDocumentBody(ICouchbaseListenerContext context, Func <Database, Body, CouchbaseLiteResponse> callback) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { MultipartDocumentReader reader = new MultipartDocumentReader(db); reader.SetContentType(context.RequestHeaders["Content-Type"]); reader.AppendData(context.BodyStream.ReadAllBytes()); try { reader.Finish(); } catch (InvalidOperationException) { return context.CreateResponse(StatusCode.BadRequest); } return callback(db, new Body(reader.GetDocumentProperties())); })); }
// Performs the actual query logic on a design document private static CouchbaseLiteResponse QueryDesignDocument(ICouchbaseListenerContext context, IList <object> keys) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { var view = db.GetView(String.Format("{0}/{1}", context.DesignDocName, context.ViewName)); var status = view.CompileFromDesignDoc(); if (status.IsError) { return context.CreateResponse(status.Code); } var options = context.QueryOptions; if (options == null) { return context.CreateResponse(StatusCode.BadRequest); } if (keys != null) { options.Keys = keys; } if (options.Stale == IndexUpdateMode.Before || view.LastSequenceIndexed <= 0) { view.UpdateIndex(); } else if (options.Stale == IndexUpdateMode.After && view.LastSequenceIndexed < db.LastSequenceNumber) { db.RunAsync(_ => view.UpdateIndex()); } // Check for conditional GET and set response Etag header: if (keys == null) { long eTag = options.IncludeDocs ? db.LastSequenceNumber : view.LastSequenceIndexed; if (context.CacheWithEtag(eTag.ToString())) { return context.CreateResponse(StatusCode.NotModified); } } return DatabaseMethods.QueryView(context, db, view, options); })); }
// Factors out the logic of opening the database and reading the document body from the HTTP request // and performs the specified logic on the body received in the request, barring any problems private static CouchbaseLiteResponse PerformLogicWithDocumentBody(ICouchbaseListenerContext context, Func <Database, Body, CouchbaseLiteResponse> callback) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { MultipartDocumentReader reader = new MultipartDocumentReader(db); reader.SetContentType(context.RequestHeaders["Content-Type"]); try { reader.AppendData(context.BodyStream.ReadAllBytes()); reader.Finish(); } catch (InvalidOperationException e) { Log.To.Router.E(TAG, "Exception trying to read data from multipart upload", e); return context.CreateResponse(StatusCode.BadRequest); } catch (IOException e) { Log.To.Router.E(TAG, "IOException while reading context body", e); return context.CreateResponse(StatusCode.RequestTimeout); } return callback(db, new Body(reader.GetDocumentProperties())); })); }
/// <summary> /// Returns document by the specified docid from the specified db. Unless you request a /// specific revision, the latest revision of the document will always be returned. /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> /// <remarks> /// http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid /// </remarks> public static ICouchbaseResponseState GetDocument(ICouchbaseListenerContext context) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => { var response = context.CreateResponse(); string docId = context.DocumentName; bool isLocalDoc = docId.StartsWith("_local", StringComparison.InvariantCulture); DocumentContentOptions options = context.ContentOptions; string openRevsParam = context.GetQueryParam("open_revs"); bool mustSendJson = context.ExplicitlyAcceptsType("application/json"); Status status = new Status(); if (openRevsParam == null || isLocalDoc) { //Regular GET: var revId = context.GetQueryParam("rev").AsRevID(); //often null RevisionInternal rev; bool includeAttachments = false, sendMultipart = false; if (isLocalDoc) { rev = db.Storage.GetLocalDocument(docId, revId); } else { includeAttachments = options.HasFlag(DocumentContentOptions.IncludeAttachments); if (includeAttachments) { sendMultipart = !mustSendJson; options &= ~DocumentContentOptions.IncludeAttachments; } rev = db.GetDocument(docId, revId, true, status); if (rev != null) { rev = ApplyOptions(options, rev, context, db, status); } if (rev == null) { response.StatusReason = status.Code == StatusCode.Deleted ? "deleted" : "missing"; response.InternalStatus = StatusCode.NotFound; return response; } } if (rev == null) { response.InternalStatus = StatusCode.NotFound; return response; } if (context.CacheWithEtag(rev.RevID?.ToString())) { response.InternalStatus = StatusCode.NotModified; return response; } if (!isLocalDoc && includeAttachments) { int minRevPos = 1; var attsSince = context.GetJsonQueryParam("atts_since")?.AsList <string>()?.AsRevIDs(); var ancestorId = db.Storage.FindCommonAncestor(rev, attsSince); if (ancestorId != null) { minRevPos = ancestorId.Generation + 1; } bool attEncodingInfo = context.GetQueryParam <bool>("att_encoding_info", bool.TryParse, false); db.ExpandAttachments(rev, minRevPos, sendMultipart, attEncodingInfo); } if (sendMultipart) { response.MultipartWriter = MultipartWriterForRev(db, rev, "multipart/related"); } else { response.JsonBody = rev.GetBody(); } } else { // open_revs query: IList <IDictionary <string, object> > result; if (openRevsParam.Equals("all")) { // ?open_revs=all returns all current/leaf revisions: bool includeDeleted = context.GetQueryParam <bool>("include_deleted", bool.TryParse, false); RevisionList allRevs = db.Storage.GetAllDocumentRevisions(docId, true, includeDeleted); result = new List <IDictionary <string, object> >(); foreach (var rev in allRevs) { if (!includeDeleted && rev.Deleted) { continue; } var loadedRev = db.RevisionByLoadingBody(rev, status); if (loadedRev != null) { ApplyOptions(options, loadedRev, context, db, status); } if (loadedRev != null) { result.Add(new Dictionary <string, object> { { "ok", loadedRev.GetProperties() } }); } else if (status.Code <= StatusCode.InternalServerError) { result.Add(new Dictionary <string, object> { { "missing", rev.RevID } }); } else { response.InternalStatus = status.Code; return response; } } } else { // ?open_revs=[...] returns an array of specific revisions of the document: var openRevs = context.GetJsonQueryParam("open_revs").AsList <object>(); if (openRevs == null) { response.InternalStatus = StatusCode.BadParam; return response; } result = new List <IDictionary <string, object> >(); foreach (var revIDObj in openRevs) { var revID = revIDObj as string; if (revID == null) { response.InternalStatus = StatusCode.BadId; return response; } var rev = db.GetDocument(docId, revID.AsRevID(), true); if (rev != null) { rev = ApplyOptions(options, rev, context, db, status); } if (rev != null) { result.Add(new Dictionary <string, object> { { "ok", rev.GetProperties() } }); } else { result.Add(new Dictionary <string, object> { { "missing", revID } }); } } } if (mustSendJson) { response["Content-Type"] = "application/json"; response.JsonBody = new Body(result.Cast <object>().ToList()); } else { response.SetMultipartBody(result.Cast <object>().ToList(), "multipart/mixed"); } } return response; }).AsDefaultState()); }
/// <summary> /// Deletes the attachment of the specified doc. /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> /// <remarks> /// http://docs.couchdb.org/en/latest/api/document/attachments.html#delete--db-docid-attname /// </remarks> public static ICouchbaseResponseState DeleteAttachment(ICouchbaseListenerContext context) { return(DatabaseMethods.PerformLogicWithDatabase(context, true, db => UpdateAttachment(context, db, context.AttachmentName, context.DocumentName, null)).AsDefaultState()); }