/// <summary> /// Performs the given logic with the specified database /// </summary> /// <returns>The result (in terms of response to the client) of the database operation</returns> /// <param name="context">The Couchbase Lite HTTP context</param> /// <param name="open">Whether or not to open the database, or just find it</param> /// <param name="action">The logic to perform on the database</param> public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open, Func <Database, CouchbaseLiteResponse> action) { string dbName = context.DatabaseName; Database db = context.DbManager.GetDatabase(dbName, true); if (db == null) { return(context.CreateResponse(StatusCode.NotFound)); } if (open) { try { db.Open(); } catch (CouchbaseLiteException e) { Log.To.Listener.W(TAG, "Exception in PerformLogicWithDatabase, returning 500", e); return(context.CreateResponse(StatusCode.DbError)); } catch (Exception e) { Log.To.Listener.E(TAG, "Exception in PerformLogicWithDatabase, returning 500", e); return(context.CreateResponse(StatusCode.DbError)); } } return(action(db)); }
/// <summary> /// The POST to _all_docs allows to specify multiple keys to be selected from the database. /// This enables you to request multiple documents in a single request, in place of multiple GET /{db}/{docid} requests. /// </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/database/bulk-api.html#post--db-_all_docs /// <remarks> public static ICouchbaseResponseState GetAllSpecifiedDocuments(ICouchbaseListenerContext context) { return(PerformLogicWithDatabase(context, true, db => { var options = context.QueryOptions; if (options == null) { return context.CreateResponse(StatusCode.BadParam); } var body = context.BodyAs <Dictionary <string, object> >(); if (body == null) { return context.CreateResponse(StatusCode.BadJson); } if (!body.ContainsKey("keys")) { return context.CreateResponse(StatusCode.BadParam); } var keys = body["keys"].AsList <object>(); options.Keys = keys; return DoAllDocs(context, db, options); }).AsDefaultState()); }
/// <summary> /// Gets information about the specified 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/database/common.html#get--db /// </remarks> public static ICouchbaseResponseState GetConfiguration(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, true, db => { int numDocs = db.GetDocumentCount(); long updateSequence = db.GetLastSequenceNumber(); if (numDocs < 0 || updateSequence < 0) { return context.CreateResponse(StatusCode.DbError); } var response = context.CreateResponse(); response.JsonBody = new Body(new Dictionary<string, object> { { "db_name", db.Name }, { "db_uuid", db.PublicUUID() }, { "doc_count", numDocs }, { "update_seq", updateSequence }, { "committed_update_seq", updateSequence }, { "purge_seq", 0 }, //TODO: Implement { "disk_size", db.GetTotalDataSize() }, { "start_time", db.StartTime * 1000 }, { "revs_limit", db.GetMaxRevTreeDepth() } }); return response; }).AsDefaultState(); }
/// <summary> /// A database purge permanently removes the references to deleted documents from 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/database/misc.html#post--db-_purge /// <remarks> public static ICouchbaseResponseState Purge(ICouchbaseListenerContext context) { return(PerformLogicWithDatabase(context, true, db => { var body = context.BodyAs <Dictionary <string, IList <string> > >(); if (body == null) { return context.CreateResponse(StatusCode.BadJson); } var purgedRevisions = db.Storage.PurgeRevisions(body); if (purgedRevisions == null) { return context.CreateResponse(StatusCode.DbError); } var responseBody = new Body(new Dictionary <string, object> { { "purged", purgedRevisions } }); var retVal = context.CreateResponse(); retVal.JsonBody = responseBody; return retVal; }).AsDefaultState()); }
private const int MIN_HEARTBEAT = 5000; //NOTE: iOS uses seconds but .NET uses milliseconds #endregion #region Public Methods /// <summary> /// Gets information about the specified 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/database/common.html#get--db /// <remarks> public static ICouchbaseResponseState GetConfiguration(ICouchbaseListenerContext context) { return(PerformLogicWithDatabase(context, true, db => { int numDocs = db.DocumentCount; long updateSequence = db.LastSequenceNumber; if (numDocs < 0 || updateSequence < 0) { return context.CreateResponse(StatusCode.DbError); } var response = context.CreateResponse(); response.JsonBody = new Body(new Dictionary <string, object> { { "db_name", db.Name }, { "doc_count", numDocs }, { "update_seq", updateSequence }, { "committed_update_seq", updateSequence }, { "purge_seq", 0 }, //TODO: Implement { "disk_size", db.TotalDataSize }, { "start_time", db.StartTime * 1000 } }); return response; }).AsDefaultState()); }
/// <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> /// Request compaction of the specified database. Compaction compresses the disk database file. /// </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/database/compact.html#post--db-_compact /// <remarks> public static ICouchbaseResponseState Compact(ICouchbaseListenerContext context) { return(PerformLogicWithDatabase(context, true, db => { try { db.Compact(); return context.CreateResponse(StatusCode.Accepted); } catch (CouchbaseLiteException) { return context.CreateResponse(StatusCode.DbError); } }).AsDefaultState()); }
/// <summary> /// Request, configure, or stop, a replication operation. /// </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/server/common.html#post--_replicate /// <remarks> public static ICouchbaseResponseState ManageReplicationSession(ICouchbaseListenerContext context) { var body = default(IDictionary <string, object>); try { byte[] buffer = new byte[context.ContentLength]; context.BodyStream.Read(buffer, 0, buffer.Length); body = new Body(buffer).GetProperties() ?? new Dictionary <string, object>(); } catch (IOException e) { Log.To.Router.E("_replicate", "IOException while reading POST body", e); return(context.CreateResponse(StatusCode.RequestTimeout).AsDefaultState()); } Replication rep = context.DbManager.ReplicationWithProperties(body); var response = context.CreateResponse(); bool cancel = body.Get("cancel") is bool && (bool)body.Get("cancel"); if (cancel) { if (!rep.IsRunning) { response.InternalStatus = StatusCode.NotFound; } else { rep.Stop(); } } else { rep.Start(); if (rep.Continuous || body.GetCast <bool>("async", false)) { response.JsonBody = new Body(new Dictionary <string, object> { { "session_id", rep.sessionID } }); } else { return(new OneShotCouchbaseResponseState(rep) { Response = response }); } } return(response.AsDefaultState()); }
// Attempt to write the response over the wire to the client private static void ProcessResponse(ICouchbaseListenerContext context, ICouchbaseResponseState responseState) { CouchbaseLiteResponse responseObject = CheckForAltMethod(context, responseState.Response); if (!responseState.IsAsync) { try { Log.To.Router.I(TAG, "Processing response for {0}...", context.RequestUrl.PathAndQuery); Log.To.Router.V(TAG, "Processing request range..."); responseObject.ProcessRequestRanges(); Log.To.Router.V(TAG, "Writing headers..."); responseObject.WriteHeaders(); Log.To.Router.V(TAG, "Writing body..."); responseObject.WriteToContext(); Log.To.Router.I(TAG, "{0} {1} => {2} ({3})", context.Method, context.RequestUrl.PathAndQuery, responseObject.Status, responseObject.StatusMessage); } catch (Exception e) { Log.To.Router.E(TAG, "Exception writing response", e); responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState(); } } else { _UnfinishedResponses.Add(responseState); } }
/// <summary> /// Verifies and registers a facebook token for use in replication authentication /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> public static ICouchbaseResponseState RegisterFacebookToken(ICouchbaseListenerContext context) { var response = context.CreateResponse(); var body = context.BodyAs<Dictionary<string, object>>(); string email = body.GetCast<string>("email"); string remoteUrl = body.GetCast<string>("remote_url"); string accessToken = body.GetCast<string>("access_token"); if (email != null && remoteUrl != null && accessToken != null) { Uri siteUrl; if (!Uri.TryCreate(remoteUrl, UriKind.Absolute, out siteUrl)) { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary<string, object> { { "error", "invalid remote_url" } }); } else if (!FacebookAuthorizer.RegisterAccessToken(accessToken, email, siteUrl)) { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary<string, object> { { "error", "invalid access_token" } }); } else { response.JsonBody = new Body(new Dictionary<string, object> { { "ok", "registered" }, { "email", email } }); } } else { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary<string, object> { { "error", "required fields: access_token, email, remote_url" } }); } return response.AsDefaultState(); }
/// <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); }
//Do an all document request on the database (i.e. fetch all docs given some options) private static CouchbaseLiteResponse DoAllDocs(ICouchbaseListenerContext context, Database db, QueryOptions options) { var result = db.GetAllDocs(options); if (!result.ContainsKey("rows")) { return(context.CreateResponse(StatusCode.BadJson)); } var documentProps = from row in (List <QueryRow>)result["rows"] select row.AsJSONDictionary(); result["rows"] = documentProps; var response = context.CreateResponse(); response.JsonBody = new Body(result); return(response); }
/// <summary> /// Returns a JSON structure of all of the documents in a given 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/database/bulk-api.html#get--db-_all_docs /// <remarks> public static ICouchbaseResponseState GetAllDocuments(ICouchbaseListenerContext context) { return(PerformLogicWithDatabase(context, true, db => { if (context.CacheWithEtag(db.LastSequenceNumber.ToString())) { return context.CreateResponse(StatusCode.NotModified); } var options = context.QueryOptions; if (options == null) { return context.CreateResponse(StatusCode.BadParam); } return DoAllDocs(context, db, options); }).AsDefaultState()); }
/// <summary> /// Creates a new 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/database/common.html#put--db /// <remarks> public static ICouchbaseResponseState UpdateConfiguration(ICouchbaseListenerContext context) { string dbName = context.DatabaseName; Database db = context.DbManager.GetDatabase(dbName, false); if (db != null && db.Exists()) { return(context.CreateResponse(StatusCode.PreconditionFailed).AsDefaultState()); } try { db.Open(); } catch (CouchbaseLiteException) { return(context.CreateResponse(StatusCode.Exception).AsDefaultState()); } return(context.CreateResponse(StatusCode.Created).AsDefaultState()); }
// Check for an incorrect request method on a request private static CouchbaseLiteResponse CheckForAltMethod(ICouchbaseListenerContext context, CouchbaseLiteResponse response) { if (response.Status != RouteCollection.EndpointNotFoundStatus) { return(response); } var request = context.RequestUrl; bool hasAltMethod = _Delete.HasLogicForRequest(request) || _Get.HasLogicForRequest(request) || _Post.HasLogicForRequest(request) || _Put.HasLogicForRequest(request); if (hasAltMethod) { return(context.CreateResponse(StatusCode.MethodNotAllowed)); } return(context.CreateResponse(StatusCode.NotFound)); }
// 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); })); }
/// <summary> /// List of running tasks, including the task type, name, status and process ID /// </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/server/common.html#get--_active_tasks /// </remarks> public static ICouchbaseResponseState GetActiveTasks(ICouchbaseListenerContext context) { // Get the current task info of all replicators: var activity = new List <object>(); var replicators = new List <Replication>(); foreach (var db in context.DbManager.AllOpenDatabases()) { var activeReplicators = default(IList <Replication>); if (db.ActiveReplicators.AcquireTemp(out activeReplicators)) { foreach (var repl in activeReplicators) { replicators.Add(repl); activity.Add(repl.ActiveTaskInfo); } } } if (context.ChangesFeedMode >= ChangesFeedMode.Continuous) { // Continuous activity feed (this is a CBL-specific API): var response = context.CreateResponse(); response.WriteHeaders(); response.Chunked = true; foreach (var item in activity) { response.SendContinuousLine((IDictionary <string, object>)item, context.ChangesFeedMode); } return(new ReplicationCouchbaseResponseState(replicators) { Response = response, ChangesFeedMode = context.ChangesFeedMode }); } else { var response = context.CreateResponse(); response.JsonBody = new Body(activity); return(response.AsDefaultState()); } }
/// <summary> /// Creates a new named document, or creates a new revision of the existing 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/common.html#put--db-docid /// </remarks> public static ICouchbaseResponseState UpdateDocument(ICouchbaseListenerContext context) { return(PerformLogicWithDocumentBody(context, (db, body) => { var response = context.CreateResponse(); string docId = context.DocumentName; db.ForgetDesignDocument(context.DesignDocName); if (context.GetQueryParam <bool>("new_edits", bool.TryParse, true)) { // Regular PUT: return UpdateDb(context, db, docId, body, false); } else { // PUT with new_edits=false -- forcible insertion of existing revision: RevisionInternal rev = new RevisionInternal(body); if (rev == null) { response.InternalStatus = StatusCode.BadJson; return response; } if (!docId.Equals(rev.DocID) || rev.RevID == null) { response.InternalStatus = StatusCode.BadId; return response; } var history = Database.ParseCouchDBRevisionHistory(body.GetProperties()); Status status = new Status(StatusCode.Ok); var castContext = context as ICouchbaseListenerContext2; var source = (castContext != null && !castContext.IsLoopbackRequest) ? castContext.Sender : null; try { Log.To.Router.I(TAG, "Force inserting {0}", rev); Log.To.Router.V(TAG, "With history {0}", new LogJsonString(history)); db.ForceInsert(rev, history, source); } catch (CouchbaseLiteException e) { status = e.CBLStatus; } if (!status.IsError) { response.JsonBody = new Body(new Dictionary <string, object> { { "ok", true }, { "id", rev.DocID }, { "rev", rev.RevID } }); } response.InternalStatus = status.Code; return response; } }).AsDefaultState()); }
/// <summary> /// Returns a list of all the databases in the Couchbase Lite instance. /// </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/server/common.html#get--_all_dbs /// <remarks> public static ICouchbaseResponseState GetAllDbs(ICouchbaseListenerContext context) { var names = context.DbManager.AllDatabaseNames.Cast <object>().ToList(); var body = new Body(names); var couchResponse = context.CreateResponse(); couchResponse.JsonBody = body; return(couchResponse.AsDefaultState()); }
/// <summary> /// Deletes the specified database, and all the documents and attachments contained within it. /// </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/database/common.html#delete--db /// <remarks> public static ICouchbaseResponseState DeleteConfiguration(ICouchbaseListenerContext context) { return(PerformLogicWithDatabase(context, false, db => { if (context.GetQueryParam("rev") != null) { // CouchDB checks for this; probably meant to be a document deletion return context.CreateResponse(StatusCode.BadId); } try { db.Delete(); } catch (CouchbaseLiteException) { return context.CreateResponse(StatusCode.InternalServerError); } return context.CreateResponse(); }).AsDefaultState()); }
//Do an all document request on the database (i.e. fetch all docs given some options) private static CouchbaseLiteResponse DoAllDocs(ICouchbaseListenerContext context, Database db, QueryOptions options) { var iterator = db.GetAllDocs(options); if (iterator == null) { return(context.CreateResponse(StatusCode.BadJson)); } var response = context.CreateResponse(); var result = (from row in iterator select row.AsJSONDictionary()).ToList(); response.JsonBody = new Body(new NonNullDictionary <string, object> { { "rows", result }, { "total_rows", result.Count }, { "offset", options.Skip }, { "update_seq", options.UpdateSeq ? (object)db.LastSequenceNumber : null } }); return(response); }
/// <summary> /// Requests one or more Universally Unique Identifiers (UUIDs) from the Couchbase Lite instance. /// </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/server/common.html#get--_uuids /// <remarks> public static ICouchbaseResponseState GetUUIDs(ICouchbaseListenerContext context) { int count = context.GetQueryParam <int>("count", int.TryParse, 1); if (count > 1000) { return(context.CreateResponse(StatusCode.Forbidden).AsDefaultState()); } var uuidList = new List <object>(); for (int i = 0; i < count; i++) { uuidList.Add(Guid.NewGuid()); } var couchResponse = context.CreateResponse(); couchResponse.JsonBody = new Body(uuidList); return(couchResponse.AsDefaultState()); }
/// <summary> /// Performs the given logic with the specified database /// </summary> /// <returns>The result (in terms of response to the client) of the database operation</returns> /// <param name="context">The Couchbase Lite HTTP context</param> /// <param name="open">Whether or not to open the database, or just find it</param> /// <param name="action">The logic to perform on the database</param> public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open, Func <Database, CouchbaseLiteResponse> action) { string dbName = context.DatabaseName; Database db = context.DbManager.GetDatabase(dbName, false); if (db == null || !db.Exists()) { return(context.CreateResponse(StatusCode.NotFound)); } if (open) { try { db.Open(); } catch (Exception) { return(context.CreateResponse(StatusCode.DbError)); } } return(action(db)); }
// Check for an incorrect request method on a request private static CouchbaseLiteResponse CheckForAltMethod(ICouchbaseListenerContext context, CouchbaseLiteResponse response) { if (response.Status != RouteCollection.EndpointNotFoundStatus) { return(response); } Log.To.Router.I(TAG, "{0} method not found for endpoint {1}, searching for alternate...", context.Method, context.RequestUrl.PathAndQuery); var request = context.RequestUrl; bool hasAltMethod = _Delete.HasLogicForRequest(request) || _Get.HasLogicForRequest(request) || _Post.HasLogicForRequest(request) || _Put.HasLogicForRequest(request); if (hasAltMethod) { Log.To.Router.I(TAG, "Suitable method found; returning 406"); return(context.CreateResponse(StatusCode.MethodNotAllowed)); } Log.To.Router.I(TAG, "No suitable method found; returning 404", context.RequestUrl.PathAndQuery); return(context.CreateResponse(StatusCode.NotFound)); }
// 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> /// Creates a new named document, or creates a new revision of the existing 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/common.html#put--db-docid /// <remarks> public static ICouchbaseResponseState UpdateDocument(ICouchbaseListenerContext context) { return(PerformLogicWithDocumentBody(context, (db, body) => { var response = context.CreateResponse(); string docId = context.DocumentName; if (context.GetQueryParam <bool>("new_edits", bool.TryParse, true)) { // Regular PUT: return UpdateDb(context, db, docId, body, false); } else { // PUT with new_edits=false -- forcible insertion of existing revision: RevisionInternal rev = new RevisionInternal(body); if (rev == null) { response.InternalStatus = StatusCode.BadJson; return response; } if (!docId.Equals(rev.GetDocId()) || rev.GetRevId() == null) { response.InternalStatus = StatusCode.BadId; return response; } var history = Database.ParseCouchDBRevisionHistory(body.GetProperties()); Status status = new Status(); try { db.ForceInsert(rev, history, null, status); } catch (CouchbaseLiteException e) { status = e.GetCBLStatus(); } if (!status.IsError) { response.JsonBody = new Body(new Dictionary <string, object> { { "ok", true }, { "id", rev.GetDocId() }, { "rev", rev.GetRevId() } }); } response.InternalStatus = status.GetCode(); return response; } }).AsDefaultState()); }
// 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); }); }
/// <summary> /// Returns complete information about authenticated user (stubbed, not actual functionality) /// </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/server/authn.html#get--_session /// <remarks> public static ICouchbaseResponseState GetSession(ICouchbaseListenerContext context) { // Even though CouchbaseLite doesn't support user logins, it implements a generic response to the // CouchDB _session API, so that apps that call it (such as Futon!) won't barf. var couchResponse = context.CreateResponse(); couchResponse.JsonBody = new Body(new Dictionary <string, object> { { "ok", true }, { "userCtx", new Dictionary <string, object> { { "name", null }, { "roles", new[] { "_admin" } } } } }); return(couchResponse.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())); })); }
/// <summary> /// Returns a JSON structure containing information about the server, including a welcome message and the version of the server. /// </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/server/common.html#get-- /// <remarks> public static ICouchbaseResponseState Greeting(ICouchbaseListenerContext context) { var info = new Dictionary<string, object> { { "couchdb", "Welcome" }, //for compatibility { "CouchbaseLite", "Welcome" }, { "version", Manager.VersionString }, { "vendor", new Dictionary<string, object> { { "name", "Couchbase Lite (C#)" }, { "version", Manager.VersionString } } } }; var body = new Body(info); var couchResponse = context.CreateResponse(); couchResponse.JsonBody = body; return couchResponse.AsDefaultState(); }
/// <summary> /// Returns a JSON structure containing information about the server, including a welcome message and the version of the server. /// </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/server/common.html#get-- /// <remarks> public static ICouchbaseResponseState Greeting(ICouchbaseListenerContext context) { var info = new Dictionary <string, object> { { "couchdb", "Welcome" }, //for compatibility { "CouchbaseLite", "Welcome" }, { "version", Manager.VersionString }, { "vendor", new Dictionary <string, object> { { "name", "Couchbase Lite (C#)" }, { "version", Manager.VersionString } } } }; var body = new Body(info); var couchResponse = context.CreateResponse(); couchResponse.JsonBody = body; return(couchResponse.AsDefaultState()); }
/// <summary> /// Verifies and registers a facebook token for use in replication authentication /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> public static ICouchbaseResponseState RegisterFacebookToken(ICouchbaseListenerContext context) { var response = context.CreateResponse(); var body = context.BodyAs <Dictionary <string, object> >(); string email = body.GetCast <string>("email"); string remoteUrl = body.GetCast <string>("remote_url"); string accessToken = body.GetCast <string>("access_token"); if (email != null && remoteUrl != null && accessToken != null) { Uri siteUrl; if (!Uri.TryCreate(remoteUrl, UriKind.Absolute, out siteUrl)) { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary <string, object> { { "error", "invalid remote_url" } }); } else if (!FacebookAuthorizer.RegisterAccessToken(accessToken, email, siteUrl)) { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary <string, object> { { "error", "invalid access_token" } }); } else { response.JsonBody = new Body(new Dictionary <string, object> { { "ok", "registered" }, { "email", email } }); } } else { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary <string, object> { { "error", "required fields: access_token, email, remote_url" } }); } return(response.AsDefaultState()); }
/// <summary> /// Queries the specified view using the specified options /// </summary> /// <returns>The HTTP response containing the results of the query</returns> /// <param name="context">The request context</param> /// <param name="view">The view to query</param> /// <param name="options">The options to apply to the query</param> public static CouchbaseLiteResponse QueryView(ICouchbaseListenerContext context, Database db, View view, QueryOptions options) { var result = view.QueryWithOptions(options); object updateSeq = options.UpdateSeq ? (object)view.LastSequenceIndexed : null; var mappedResult = new List <object>(); foreach (var row in result) { row.Database = db; var dict = row.AsJSONDictionary(); if (context.ContentOptions != DocumentContentOptions.None) { var doc = dict.Get("doc").AsDictionary <string, object>(); if (doc != null) { // Add content options: RevisionInternal rev = new RevisionInternal(doc); var status = new Status(); rev = DocumentMethods.ApplyOptions(context.ContentOptions, rev, context, db, status); if (rev != null) { dict["doc"] = rev.GetProperties(); } } } mappedResult.Add(dict); } var body = new Body(new NonNullDictionary <string, object> { { "rows", mappedResult }, { "total_rows", view.TotalRows }, { "offset", options.Skip }, { "update_seq", updateSeq } }); var retVal = context.CreateResponse(); retVal.JsonBody = body; return(retVal); }
/// <summary> /// Verifies and registers a persona token for use in replication authentication /// </summary> /// <returns>The response state for further HTTP processing</returns> /// <param name="context">The context of the Couchbase Lite HTTP request</param> public static ICouchbaseResponseState RegisterPersonaToken(ICouchbaseListenerContext context) { var response = context.CreateResponse(); var body = context.BodyAs<Dictionary<string, object>>(); string email = PersonaAuthorizer.RegisterAssertion(body.GetCast<string>("assertion")); if (email != null) { response.JsonBody = new Body(new Dictionary<string, object> { { "ok", "registered" }, { "email", email } }); } else { response.InternalStatus = StatusCode.BadParam; response.JsonBody = new Body(new Dictionary<string, object> { { "error", "invalid assertion" } }); } return response.AsDefaultState(); }
// Attempt to write the response over the wire to the client private static void ProcessResponse(ICouchbaseListenerContext context, ICouchbaseResponseState responseState) { CouchbaseLiteResponse responseObject = CheckForAltMethod(context, responseState.Response); if (!responseState.IsAsync) { try { responseObject.ProcessRequestRanges(); responseObject.WriteHeaders(); responseObject.WriteToContext(); } catch (Exception e) { Log.E(TAG, "Exception writing response", e); responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState(); } } else { _UnfinishedResponses.Add(responseState); } }
/// <summary> /// Request compaction of the specified database. Compaction compresses the disk database file. /// </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/database/compact.html#post--db-_compact /// </remarks> public static ICouchbaseResponseState Compact(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, true, db => { try { db.Compact(); return context.CreateResponse(StatusCode.Accepted); } catch (CouchbaseLiteException) { return context.CreateResponse(StatusCode.DbError); } }).AsDefaultState(); }
public static ICouchbaseResponseState GetChangesPost(ICouchbaseListenerContext context) { DBMonitorCouchbaseResponseState responseState = new DBMonitorCouchbaseResponseState(); var responseObject = PerformLogicWithDatabase(context, true, db => { var response = context.CreateResponse(); responseState.Response = response; var body = context.BodyAs<Dictionary<string, object>>(); ProcessBody(body); if (body.GetCast<ChangesFeedMode>("feed") < ChangesFeedMode.Continuous) { if(context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) { response.InternalStatus = StatusCode.NotModified; return response; } } var options = ChangesOptions.Default; responseState.Db = db; responseState.ContentOptions = body.GetCast<DocumentContentOptions>("content_options"); responseState.ChangesFeedMode = body.GetCast<ChangesFeedMode>("feed"); responseState.ChangesIncludeDocs = body.GetCast<bool>("include_docs"); options.IncludeDocs = responseState.ChangesIncludeDocs; responseState.ChangesIncludeConflicts = body.GetCast<string>("style") == "all_docs"; options.IncludeConflicts = responseState.ChangesIncludeConflicts; options.ContentOptions = responseState.ContentOptions; options.SortBySequence = !options.IncludeConflicts; options.Limit = body.GetCast<int>("limit", options.Limit); int since = body.GetCast<int>("since"); string filterName = body.GetCast<string>("filter"); if(filterName != null) { Status status = new Status(); responseState.ChangesFilter = db.GetFilter(filterName, status); if(responseState.ChangesFilter == null) { return context.CreateResponse(status.Code); } responseState.FilterParams = context.GetQueryParams(); } RevisionList changes = db.ChangesSince(since, options, responseState.ChangesFilter, responseState.FilterParams); if((responseState.ChangesFeedMode >= ChangesFeedMode.Continuous) || (responseState.ChangesFeedMode == ChangesFeedMode.LongPoll && changes.Count == 0)) { // Response is going to stay open (continuous, or hanging GET): response.Chunked = true; if(responseState.ChangesFeedMode == ChangesFeedMode.EventSource) { response["Content-Type"] = "text/event-stream; charset=utf-8"; } if(responseState.ChangesFeedMode >= ChangesFeedMode.Continuous) { response.WriteHeaders(); foreach(var rev in changes) { response.SendContinuousLine(ChangesDictForRev(rev, responseState), context.ChangesFeedMode); } } responseState.SubscribeToDatabase(db); int heartbeat = body.GetCast<int>("heartbeat", Int32.MinValue); if(heartbeat != Int32.MinValue) { if(heartbeat <= 0) { responseState.IsAsync = false; return context.CreateResponse(StatusCode.BadParam); } heartbeat = Math.Max(heartbeat, (int)MinHeartbeat.TotalMilliseconds); string heartbeatResponse = context.ChangesFeedMode == ChangesFeedMode.EventSource ? "\n\n" : "\r\n"; responseState.StartHeartbeat(heartbeatResponse, TimeSpan.FromMilliseconds(heartbeat)); } return context.CreateResponse(); } else { if(responseState.ChangesIncludeConflicts) { response.JsonBody = new Body(ResponseBodyForChanges(changes, since, options.Limit, responseState)); } else { response.JsonBody = new Body(ResponseBodyForChanges(changes, since, responseState)); } return response; } }); responseState.Response = responseObject; return responseState; }
/// <summary> /// Returns a sorted list of changes made to documents in the database, in time order of application. /// </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/database/changes.html#get--db-_changes /// </remarks> public static ICouchbaseResponseState GetChanges(ICouchbaseListenerContext context) { DBMonitorCouchbaseResponseState responseState = new DBMonitorCouchbaseResponseState(); var responseObject = PerformLogicWithDatabase(context, true, db => { var response = context.CreateResponse(); responseState.Response = response; if (context.ChangesFeedMode < ChangesFeedMode.Continuous) { if(context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) { response.InternalStatus = StatusCode.NotModified; return response; } } var options = ChangesOptions.Default; responseState.Db = db; responseState.ContentOptions = context.ContentOptions; responseState.ChangesFeedMode = context.ChangesFeedMode; responseState.ChangesIncludeDocs = context.GetQueryParam<bool>("include_docs", bool.TryParse, false); options.IncludeDocs = responseState.ChangesIncludeDocs; responseState.ChangesIncludeConflicts = context.GetQueryParam("style") == "all_docs"; options.IncludeConflicts = responseState.ChangesIncludeConflicts; options.ContentOptions = context.ContentOptions; options.SortBySequence = !options.IncludeConflicts; options.Limit = context.GetQueryParam<int>("limit", int.TryParse, options.Limit); int since = context.GetQueryParam<int>("since", int.TryParse, 0); string filterName = context.GetQueryParam("filter"); if(filterName != null) { Status status = new Status(); responseState.ChangesFilter = db.GetFilter(filterName, status); if(responseState.ChangesFilter == null) { return context.CreateResponse(status.Code); } responseState.FilterParams = context.GetQueryParams(); } RevisionList changes = db.ChangesSince(since, options, responseState.ChangesFilter, responseState.FilterParams); if((context.ChangesFeedMode >= ChangesFeedMode.Continuous) || (context.ChangesFeedMode == ChangesFeedMode.LongPoll && changes.Count == 0)) { // Response is going to stay open (continuous, or hanging GET): response.Chunked = true; if(context.ChangesFeedMode == ChangesFeedMode.EventSource) { response["Content-Type"] = "text/event-stream; charset=utf-8"; } if(context.ChangesFeedMode >= ChangesFeedMode.Continuous) { response.WriteHeaders(); foreach(var rev in changes) { var success = response.SendContinuousLine(ChangesDictForRev(rev, responseState), context.ChangesFeedMode); if(!success) { return context.CreateResponse(StatusCode.BadRequest); } } } responseState.SubscribeToDatabase(db); string heartbeatParam = context.GetQueryParam("heartbeat"); if(heartbeatParam != null) { int heartbeat; if(!int.TryParse(heartbeatParam, out heartbeat) || heartbeat <= 0) { responseState.IsAsync = false; return context.CreateResponse(StatusCode.BadParam); } var heartbeatSpan = TimeSpan.FromMilliseconds(heartbeat); if(heartbeatSpan < MinHeartbeat) { heartbeatSpan = MinHeartbeat; } string heartbeatResponse = context.ChangesFeedMode == ChangesFeedMode.EventSource ? "\n\n" : "\r\n"; responseState.StartHeartbeat(heartbeatResponse, heartbeatSpan); } return response; } else { if(responseState.ChangesIncludeConflicts) { response.JsonBody = new Body(ResponseBodyForChanges(changes, since, options.Limit, responseState)); } else { response.JsonBody = new Body(ResponseBodyForChanges(changes, since, responseState)); } return response; } }); responseState.Response = responseObject; return responseState; }
/// <summary> /// Create and update multiple documents at the same time within a single request. /// </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/database/bulk-api.html#post--db-_bulk_docs /// </remarks> public static ICouchbaseResponseState ProcessDocumentChangeOperations(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, true, db => { var postBody = context.BodyAs<Dictionary<string, object>>(); if(postBody == null) { return context.CreateResponse(StatusCode.BadJson); } if(!postBody.ContainsKey("docs")) { return context.CreateResponse(StatusCode.BadParam); } var docs = postBody["docs"].AsList<IDictionary<string, object>>(); bool allOrNothing; postBody.TryGetValue<bool>("all_or_nothing", out allOrNothing); bool newEdits; postBody.TryGetValue<bool>("new_edits", out newEdits); var response = context.CreateResponse(); StatusCode status = StatusCode.Ok; bool success = db.RunInTransaction(() => { List<IDictionary<string, object>> results = new List<IDictionary<string, object>>(docs.Count); var castContext = context as ICouchbaseListenerContext2; var source = castContext != null && !castContext.IsLoopbackRequest ? castContext.Sender : null; foreach(var doc in docs) { string docId = doc.CblID(); RevisionInternal rev = null; Body body = new Body(doc); if(!newEdits) { if(!RevisionInternal.IsValid(body)) { status = StatusCode.BadParam; } else { rev = new RevisionInternal(body); var history = Database.ParseCouchDBRevisionHistory(doc); try { db.ForceInsert(rev, history, source); } catch(CouchbaseLiteException e) { status = e.Code; } } } else { status = DocumentMethods.UpdateDocument(context, db, docId, body, false, allOrNothing, out rev); } IDictionary<string, object> result = null; if((int)status < 300) { Debug.Assert(rev != null && rev.RevID != null); if(newEdits) { result = new Dictionary<string, object> { { "id", rev.DocID }, { "rev", rev.RevID }, { "status", (int)status } }; } } else if((int)status >= 500) { return false; // abort the whole thing if something goes badly wrong } else if(allOrNothing) { return false; // all_or_nothing backs out if there's any error } else { var info = Status.ToHttpStatus(status); result = new Dictionary<string, object> { { "id", docId }, { "error", info.Item2 }, { "status", info.Item1 } }; } if(result != null) { results.Add(result); } } response.JsonBody = new Body(results.Cast<object>().ToList()); return true; }); if(!success) { response.InternalStatus = status; } return response; }).AsDefaultState(); }
/// <summary> /// The POST to _all_docs allows to specify multiple keys to be selected from the database. /// This enables you to request multiple documents in a single request, in place of multiple GET /{db}/{docid} requests. /// </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/database/bulk-api.html#post--db-_all_docs /// </remarks> public static ICouchbaseResponseState GetAllSpecifiedDocuments(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, true, db => { var options = context.QueryOptions; if(options == null) { return context.CreateResponse(StatusCode.BadParam); } var body = context.BodyAs<Dictionary<string, object>>(); if(body == null) { return context.CreateResponse(StatusCode.BadJson); } if(!body.ContainsKey("keys")) { return context.CreateResponse(StatusCode.BadParam); } var keys = body["keys"].AsList<object>(); options.Keys = keys; return DoAllDocs(context, db, options); }).AsDefaultState(); }
// Perform a document operation on the specified database private static CouchbaseLiteResponse UpdateDb(ICouchbaseListenerContext context, Database db, string docId, Body body, bool deleting) { var response = context.CreateResponse(); if (docId != null) { // On PUT/DELETE, get revision ID from either ?rev= query, If-Match: header, or doc body: string revParam = context.GetQueryParam("rev"); string ifMatch = context.RequestHeaders["If-Match"]; if (ifMatch != null) { if (revParam == null) { revParam = ifMatch; } else if (!revParam.Equals(ifMatch)) { return context.CreateResponse(StatusCode.BadRequest); } } if (revParam != null && body != null) { var revProp = body.GetPropertyForKey("_rev"); if (revProp == null) { // No _rev property in body, so use ?rev= query param instead: var props = body.GetProperties(); props["_rev"] = revParam; body = new Body(props); } else if (!revProp.Equals(revParam)) { return context.CreateResponse(StatusCode.BadRequest); // mismatch between _rev and rev } } } RevisionInternal rev; StatusCode status = UpdateDocument(context, db, docId, body, deleting, false, out rev); if ((int)status < 300) { context.CacheWithEtag(rev.GetRevId()); // set ETag if (!deleting) { var url = context.RequestUrl; if (docId != null) { response["Location"] = url.AbsoluteUri; } } response.JsonBody = new Body(new Dictionary<string, object> { { "ok", true }, { "id", rev.GetDocId() }, { "rev", rev.GetRevId() } }); } response.InternalStatus = status; return response; }
//Do an all document request on the database (i.e. fetch all docs given some options) private static CouchbaseLiteResponse DoAllDocs(ICouchbaseListenerContext context, Database db, QueryOptions options) { var iterator = db.GetAllDocs(options); if (iterator == null) { return context.CreateResponse(StatusCode.BadJson); } var response = context.CreateResponse(); var result = (from row in iterator select row.AsJSONDictionary()).ToList(); response.JsonBody = new Body(new NonNullDictionary<string, object> { { "rows", result }, { "total_rows", result.Count }, { "offset", options.Skip }, { "update_seq", options.UpdateSeq ? (object)db.GetLastSequenceNumber() : null } }); return response; }
/// <summary> /// Queries the specified view using the specified options /// </summary> /// <returns>The HTTP response containing the results of the query</returns> /// <param name="context">The request context</param> /// <param name="db">The database to run the query in</param> /// <param name="view">The view to query</param> /// <param name="options">The options to apply to the query</param> public static CouchbaseLiteResponse QueryView(ICouchbaseListenerContext context, Database db, View view, QueryOptions options) { var result = view.QueryWithOptions(options); object updateSeq = options.UpdateSeq ? (object)view.LastSequenceIndexed : null; var mappedResult = new List<object>(); foreach (var row in result) { row.Database = db; var dict = row.AsJSONDictionary(); if (context.ContentOptions != DocumentContentOptions.None) { var doc = dict.Get("doc").AsDictionary<string, object>(); if (doc != null) { // Add content options: RevisionInternal rev = new RevisionInternal(doc); var status = new Status(); rev = DocumentMethods.ApplyOptions(context.ContentOptions, rev, context, db, status); if (rev != null) { dict["doc"] = rev.GetProperties(); } } } mappedResult.Add(dict); } var body = new Body(new NonNullDictionary<string, object> { { "rows", mappedResult }, { "total_rows", view.TotalRows }, { "offset", options.Skip }, { "update_seq", updateSeq } }); var retVal = context.CreateResponse(); retVal.JsonBody = body; return retVal; }
// Attempt to write the response over the wire to the client private static void ProcessResponse(ICouchbaseListenerContext context, ICouchbaseResponseState responseState) { CouchbaseLiteResponse responseObject = CheckForAltMethod(context, responseState.Response); if (!responseState.IsAsync) { try { responseObject.ProcessRequestRanges(); responseObject.WriteHeaders(); responseObject.WriteToContext(); } catch(Exception e) { Log.E(TAG, "Exception writing response", e); responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState(); } } else { _UnfinishedResponses.Add(responseState); } }
/// <summary> /// A database purge permanently removes the references to deleted documents from 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/database/misc.html#post--db-_purge /// </remarks> public static ICouchbaseResponseState Purge(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, true, db => { var body = context.BodyAs<Dictionary<string, IList<string>>>(); if(body == null) { return context.CreateResponse(StatusCode.BadJson); } var purgedRevisions = db.Storage.PurgeRevisions(body); if(purgedRevisions == null) { return context.CreateResponse(StatusCode.DbError); } var responseBody = new Body(new Dictionary<string, object> { { "purged", purgedRevisions } }); var retVal = context.CreateResponse(); retVal.JsonBody = responseBody; return retVal; }).AsDefaultState(); }
/// <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"); DocumentContentOptions options = context.ContentOptions; string openRevsParam = context.GetQueryParam("open_revs"); bool mustSendJson = context.ExplicitlyAcceptsType("application/json"); if (openRevsParam == null || isLocalDoc) { //Regular GET: string revId = context.GetQueryParam("rev"); //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; } Status status = new Status(); rev = db.GetDocument(docId, revId, true, status); if(rev != null) { rev = ApplyOptions(options, rev, context, db, status); } if(rev == null) { if(status.Code == StatusCode.Deleted) { response.StatusReason = "deleted"; } else { response.StatusReason = "missing"; } response.InternalStatus = status.Code; return response; } } if(rev == null) { response.InternalStatus = StatusCode.NotFound; return response; } if(context.CacheWithEtag(rev.GetRevId())) { response.InternalStatus = StatusCode.NotModified; return response; } if(!isLocalDoc && includeAttachments) { int minRevPos = 1; IList<string> attsSince = context.GetJsonQueryParam("atts_since").AsList<string>(); string ancestorId = db.Storage.FindCommonAncestor(rev, attsSince); if(ancestorId != null) { minRevPos = RevisionInternal.GenerationFromRevID(ancestorId) + 1; } Status status = new Status(); bool attEncodingInfo = context.GetQueryParam<bool>("att_encoding_info", bool.TryParse, false); if(!db.ExpandAttachments(rev, minRevPos, sendMultipart, attEncodingInfo, status)) { response.InternalStatus = status.Code; return response; } } if(sendMultipart) { response.MultipartWriter = db.MultipartWriterForRev(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); result = new List<IDictionary<string, object>>(); foreach(var rev in allRevs) { if(!includeDeleted && rev.IsDeleted()) { continue; } Status status = new Status(); RevisionInternal 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.GetRevId() } }); } 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; } Status status = new Status(); var rev = db.GetDocument(docId, revID, true, status); 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(); }
// Check for an incorrect request method on a request private static CouchbaseLiteResponse CheckForAltMethod(ICouchbaseListenerContext context, CouchbaseLiteResponse response) { if (response.Status != RouteCollection.EndpointNotFoundStatus) { return response; } var request = context.RequestUrl; bool hasAltMethod = _Delete.HasLogicForRequest(request) || _Get.HasLogicForRequest(request) || _Post.HasLogicForRequest(request) || _Put.HasLogicForRequest(request); if (hasAltMethod) { return context.CreateResponse(StatusCode.MethodNotAllowed); } return context.CreateResponse(StatusCode.NotFound); }
/// <summary> /// Creates a new named document, or creates a new revision of the existing 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/common.html#put--db-docid /// <remarks> public static ICouchbaseResponseState UpdateDocument(ICouchbaseListenerContext context) { return PerformLogicWithDocumentBody(context, (db, body) => { var response = context.CreateResponse(); string docId = context.DocumentName; if(context.GetQueryParam<bool>("new_edits", bool.TryParse, true)) { // Regular PUT: return UpdateDb(context, db, docId, body, false); } else { // PUT with new_edits=false -- forcible insertion of existing revision: RevisionInternal rev = new RevisionInternal(body); if(rev == null) { response.InternalStatus = StatusCode.BadJson; return response; } if(!docId.Equals(rev.GetDocId()) || rev.GetRevId() == null) { response.InternalStatus = StatusCode.BadId; return response; } var history = Database.ParseCouchDBRevisionHistory(body.GetProperties()); Status status = new Status(); try { db.ForceInsert(rev, history, null); } catch(CouchbaseLiteException e) { status = e.CBLStatus; } if(!status.IsError) { response.JsonBody = new Body(new Dictionary<string, object> { { "ok", true }, { "id", rev.GetDocId() }, { "rev", rev.GetRevId() } }); } response.InternalStatus = status.Code; return response; } }).AsDefaultState(); }
/// <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 rev = db.GetDocument(context.DocumentName, context.GetQueryParam("rev"), false, status); if(rev ==null) { return context.CreateResponse(status.Code); } if(context.CacheWithEtag(rev.GetRevId())) { 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, status); if(attachment == null) { return context.CreateResponse(status.Code); } 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(); }
// Update the given attachment using the provided info private static CouchbaseLiteResponse UpdateAttachment(ICouchbaseListenerContext context, Database db, string attachment, string docId, BlobStoreWriter body) { RevisionInternal rev = db.UpdateAttachment(attachment, body, context.RequestHeaders["Content-Type"], AttachmentEncoding.None, docId, context.GetQueryParam("rev") ?? context.IfMatch()); var response = context.CreateResponse(); response.JsonBody = new Body(new Dictionary<string, object> { { "ok", true }, { "id", rev.GetDocId() }, { "rev", rev.GetRevId() } }); context.CacheWithEtag(rev.GetRevId()); if (body != null) { response["Location"] = context.RequestUrl.AbsoluteUri; } return response; }
/// <summary> /// Creates (and executes) a temporary view based on the view function supplied in the JSON request. /// </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/database/temp-views.html#post--db-_temp_view /// </remarks> public static ICouchbaseResponseState ExecuteTemporaryViewFunction(ICouchbaseListenerContext context) { var response = context.CreateResponse(); if (context.RequestHeaders["Content-Type"] == null || !context.RequestHeaders["Content-Type"].StartsWith("application/json")) { response.InternalStatus = StatusCode.UnsupportedType; return response.AsDefaultState(); } IEnumerable<byte> json = context.BodyStream.ReadAllBytes(); var requestBody = new Body(json); if (!requestBody.IsValidJSON()) { response.InternalStatus = StatusCode.BadJson; return response.AsDefaultState(); } var props = requestBody.GetProperties(); if (props == null) { response.InternalStatus = StatusCode.BadJson; return response.AsDefaultState(); } var options = context.QueryOptions; if (options == null) { response.InternalStatus = StatusCode.BadRequest; return response.AsDefaultState(); } return PerformLogicWithDatabase(context, true, db => { if (context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) { response.InternalStatus = StatusCode.NotModified; return response; } var view = db.GetView("@@TEMPVIEW@@"); var status = view.Compile(props, "javascript"); if(status.IsError) { response.InternalStatus = status.Code; return response; } try { view.UpdateIndex_Internal(); return QueryView(context, null, view, options); } catch(CouchbaseLiteException e) { response.InternalStatus = e.CBLStatus.Code; } return response; }).AsDefaultState(); }
/// <summary> /// The entry point for routing a request received by an CouchbaseLiteServiceListener /// </summary> /// <param name="context">The context containing information about the /// request</param> public void HandleRequest(ICouchbaseListenerContext context) { Log.V(TAG, "Processing {0} request to {1}", context.Method, context.RequestUrl.AbsoluteUri); var method = context.Method; if (OnAccessCheck != null) { Status result = null; try { result = OnAccessCheck(method, context.RequestUrl.AbsolutePath); } catch(Exception e) { result = new Status(StatusCode.Exception); Log.E(TAG, "Unhandled non-Couchbase exception in OnAccessCheck", e); } if (result.IsError) { var r = context.CreateResponse(result.Code); ProcessResponse(context, r.AsDefaultState()); return; } } RestMethod logic = null; if (method.Equals("GET") || method.Equals("HEAD")) { logic = _Get.LogicForRequest(context.RequestUrl); } else if (method.Equals("POST")) { logic = _Post.LogicForRequest(context.RequestUrl); } else if (method.Equals("PUT")) { logic = _Put.LogicForRequest(context.RequestUrl); } else if (method.Equals("DELETE")) { logic = _Delete.LogicForRequest(context.RequestUrl); } else { logic = NOT_ALLOWED; // Shouldn't happen } ICouchbaseResponseState responseState = null; try { responseState = logic(context); } catch(Exception e) { var ce = e as CouchbaseLiteException; if (ce != null) { // This is in place so that a response can be written simply by throwing a couchbase lite exception // in the routing logic Log.I(TAG, "Couchbase exception in routing logic, this message can be ignored if intentional", e); responseState = context.CreateResponse(ce.CBLStatus.Code).AsDefaultState(); } else { Log.E(TAG, "Unhandled non-Couchbase exception in routing logic", e); responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState(); } } ProcessResponse(context, responseState); }
/// <summary> /// Performs the given logic with the specified database /// </summary> /// <returns>The result (in terms of response to the client) of the database operation</returns> /// <param name="context">The Couchbase Lite HTTP context</param> /// <param name="open">Whether or not to open the database, or just find it</param> /// <param name="action">The logic to perform on the database</param> public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open, Func<Database, CouchbaseLiteResponse> action) { string dbName = context.DatabaseName; Database db = context.DbManager.GetDatabase(dbName, false); if (db == null || !db.Exists()) { return context.CreateResponse(StatusCode.NotFound); } if (open) { try { db.Open(); } catch(CouchbaseLiteException e) { Log.To.Listener.W(TAG, "Exception in PerformLogicWithDatabase, returning 500", e); return context.CreateResponse(StatusCode.DbError); } catch(Exception e) { Log.To.Listener.E(TAG, "Exception in PerformLogicWithDatabase, returning 500", e); return context.CreateResponse(StatusCode.DbError); } } return action(db); }
/// <summary> /// Creates a new 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/database/common.html#put--db /// </remarks> public static ICouchbaseResponseState UpdateConfiguration(ICouchbaseListenerContext context) { string dbName = context.DatabaseName; Database db = context.DbManager.GetDatabase(dbName, false); if (db != null && db.Exists()) { return context.CreateResponse(StatusCode.PreconditionFailed).AsDefaultState(); } try { db.Open(); } catch(CouchbaseLiteException) { return context.CreateResponse(StatusCode.Exception).AsDefaultState(); } return context.CreateResponse(StatusCode.Created).AsDefaultState(); }
public static ICouchbaseResponseState RevsDiff(ICouchbaseListenerContext context) { // Collect all of the input doc/revision IDs as CBL_Revisions: var revs = new RevisionList(); var body = context.BodyAs<Dictionary<string, object>>(); if (body == null) { return context.CreateResponse(StatusCode.BadJson).AsDefaultState(); } foreach (var docPair in body) { var revIDs = docPair.Value.AsList<string>(); if (revIDs == null) { return context.CreateResponse(StatusCode.BadParam).AsDefaultState(); } foreach (var revID in revIDs) { var rev = new RevisionInternal(docPair.Key, revID.AsRevID(), false); revs.Add(rev); } } return PerformLogicWithDatabase(context, true, db => { var response = context.CreateResponse(); // Look them up, removing the existing ones from revs: db.Storage.FindMissingRevisions(revs); // Return the missing revs in a somewhat different format: IDictionary<string, object> diffs = new Dictionary<string, object>(); foreach(var rev in revs) { var docId = rev.DocID; IList<RevisionID> missingRevs = null; if(!diffs.ContainsKey(docId)) { missingRevs = new List<RevisionID>(); diffs[docId] = new Dictionary<string, IList<RevisionID>> { { "missing", missingRevs } }; } else { missingRevs = ((Dictionary<string, IList<RevisionID>>)diffs[docId])["missing"]; } missingRevs.Add(rev.RevID); } // Add the possible ancestors for each missing revision: foreach(var docPair in diffs) { IDictionary<string, IList<RevisionID>> docInfo = (IDictionary<string, IList<RevisionID>>)docPair.Value; int maxGen = 0; RevisionID maxRevID = null; foreach(var revId in docInfo["missing"]) { if(revId.Generation > maxGen) { maxGen = revId.Generation; maxRevID = revId; } } var rev = new RevisionInternal(docPair.Key, maxRevID, false); var ancestors = db.Storage.GetPossibleAncestors(rev, 0, ValueTypePtr<bool>.NULL)?.ToList(); if(ancestors != null && ancestors.Count > 0) { docInfo["possible_ancestors"] = ancestors; } } response.JsonBody = new Body(diffs); return response; }).AsDefaultState(); }
/// <summary> /// Returns a JSON structure of all of the documents in a given 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/database/bulk-api.html#get--db-_all_docs /// </remarks> public static ICouchbaseResponseState GetAllDocuments(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, true, db => { if(context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) { return context.CreateResponse(StatusCode.NotModified); } var options = context.QueryOptions; if(options == null) { return context.CreateResponse(StatusCode.BadParam); } return DoAllDocs(context, db, options); }).AsDefaultState(); }
/// <summary> /// Deletes the specified database, and all the documents and attachments contained within it. /// </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/database/common.html#delete--db /// </remarks> public static ICouchbaseResponseState DeleteConfiguration(ICouchbaseListenerContext context) { return PerformLogicWithDatabase(context, false, db => { if(context.GetQueryParam("rev") != null) { // CouchDB checks for this; probably meant to be a document deletion return context.CreateResponse(StatusCode.BadId); } try { db.Delete(); } catch (CouchbaseLiteException) { return context.CreateResponse(StatusCode.InternalServerError); } return context.CreateResponse(); }).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())); }); }
/// <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> /// Performs the given logic with the specified database /// </summary> /// <returns>The result (in terms of response to the client) of the database operation</returns> /// <param name="context">The Couchbase Lite HTTP context</param> /// <param name="open">Whether or not to open the database, or just find it</param> /// <param name="action">The logic to perform on the database</param> public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open, Func<Database, CouchbaseLiteResponse> action) { string dbName = context.DatabaseName; Database db = context.DbManager.GetDatabaseWithoutOpening(dbName, false); if (db == null || !db.Exists()) { return context.CreateResponse(StatusCode.NotFound); } if (open) { bool opened = db.Open(); if (!opened) { return context.CreateResponse(StatusCode.DbError); } } return action(db); }