Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Split up TDRouter.m. No code changes.

  • Loading branch information...
commit 2ae2a10337cbc4be7c653bcb872054a654dfb023 1 parent 9e846a8
@snej snej authored
View
573 Source/TDRouter+Handlers.m
@@ -0,0 +1,573 @@
+//
+// TDRouter+Handlers.m
+// TouchDB
+//
+// Created by Jens Alfke on 1/5/12.
+// Copyright (c) 2012 Couchbase, Inc. All rights reserved.
+//
+
+#import "TDRouter.h"
+#import "TDDatabase.h"
+#import "TDView.h"
+#import "TDBody.h"
+#import "TDRevision.h"
+#import "TDServer.h"
+#import "TDReplicator.h"
+
+
+@interface TDRouter ()
+- (TDStatus) update: (TDDatabase*)db docID: (NSString*)docID json: (NSData*)json
+ deleting: (BOOL)deleting;
+@end
+
+
+@implementation TDRouter (Handlers)
+
+
+#pragma mark - SERVER REQUESTS:
+
+
+- (TDStatus) do_GETRoot {
+ NSDictionary* info = $dict({@"TouchDB", @"Welcome"},
+ {@"couchdb", @"Welcome"}, // for compatibility
+ {@"version", kTDVersionString});
+ _response.body = [TDBody bodyWithProperties: info];
+ return 200;
+}
+
+- (TDStatus) do_GET_all_dbs {
+ NSArray* dbs = _server.allDatabaseNames ?: $array();
+ _response.body = [[[TDBody alloc] initWithArray: dbs] autorelease];
+ return 200;
+}
+
+- (TDStatus) do_POST_replicate {
+ // Extract the parameters from the JSON request body:
+ // http://wiki.apache.org/couchdb/Replication
+ id body = [NSJSONSerialization JSONObjectWithData: _request.HTTPBody options: 0 error: nil];
+ if (![body isKindOfClass: [NSDictionary class]])
+ return 400;
+ NSString* source = $castIf(NSString, [body objectForKey: @"source"]);
+ NSString* target = $castIf(NSString, [body objectForKey: @"target"]);
+ BOOL createTarget = [$castIf(NSNumber, [body objectForKey: @"create_target"]) boolValue];
+ BOOL continuous = [$castIf(NSNumber, [body objectForKey: @"continuous"]) boolValue];
+ BOOL cancel = [$castIf(NSNumber, [body objectForKey: @"cancel"]) boolValue];
+
+ // Map the 'source' and 'target' JSON params to a local database and remote URL:
+ if (!source || !target)
+ return 400;
+ BOOL push = NO;
+ TDDatabase* db = [_server existingDatabaseNamed: source];
+ NSString* remoteStr;
+ if (db) {
+ remoteStr = target;
+ push = YES;
+ } else {
+ remoteStr = source;
+ if (createTarget && !cancel) {
+ db = [_server databaseNamed: target];
+ if (![db open])
+ return 500;
+ } else {
+ db = [_server existingDatabaseNamed: target];
+ }
+ if (!db)
+ return 404;
+ }
+ NSURL* remote = [NSURL URLWithString: remoteStr];
+ if (!remote || ![remote.scheme hasPrefix: @"http"])
+ return 400;
+
+ if (!cancel) {
+ // Start replication:
+ TDReplicator* repl = [db replicateWithRemoteURL: remote push: push continuous: continuous];
+ if (!repl)
+ return 500;
+ _response.bodyObject = $dict({@"session_id", repl.sessionID});
+ } else {
+ // Cancel replication:
+ TDReplicator* repl = [db activeReplicatorWithRemoteURL: remote push: push];
+ if (!repl)
+ return 404;
+ [repl stop];
+ }
+ return 200;
+}
+
+
+- (TDStatus) do_GET_uuids {
+ int count = MIN(1000, [self intQuery: @"count" defaultValue: 1]);
+ NSMutableArray* uuids = [NSMutableArray arrayWithCapacity: count];
+ for (int i=0; i<count; i++)
+ [uuids addObject: [TDDatabase generateDocumentID]];
+ _response.bodyObject = $dict({@"uuids", uuids});
+ return 200;
+}
+
+
+- (TDStatus) do_GET_active_tasks {
+ // http://wiki.apache.org/couchdb/HttpGetActiveTasks
+ NSMutableArray* activity = $marray();
+ for (TDDatabase* db in _server.allOpenDatabases) {
+ for (TDReplicator* repl in db.activeReplicators) {
+ NSString* source = repl.remote.absoluteString;
+ NSString* target = db.name;
+ if (repl.isPush) {
+ NSString* temp = source;
+ source = target;
+ target = temp;
+ }
+ NSUInteger processed = repl.changesProcessed;
+ NSUInteger total = repl.changesTotal;
+ NSString* status = $sprintf(@"Processed %u / %u changes",
+ (unsigned)processed, (unsigned)total);
+ long progress = (total > 0) ? lroundf(100*(processed / (float)total)) : 0;
+ [activity addObject: $dict({@"type", @"Replication"},
+ {@"task", repl.sessionID},
+ {@"source", source},
+ {@"target", target},
+ {@"status", status},
+ {@"progress", $object(progress)})];
+ }
+ }
+ _response.body = [[[TDBody alloc] initWithArray: activity] autorelease];
+ return 200;
+}
+
+
+#pragma mark - DATABASE REQUESTS:
+
+
+- (TDStatus) do_GET: (TDDatabase*)db {
+ // http://wiki.apache.org/couchdb/HTTP_database_API#Database_Information
+ TDStatus status = [self openDB];
+ if (status >= 300)
+ return status;
+ NSUInteger num_docs = db.documentCount;
+ SequenceNumber update_seq = db.lastSequence;
+ if (num_docs == NSNotFound || update_seq == NSNotFound)
+ return 500;
+ _response.bodyObject = $dict({@"db_name", db.name},
+ {@"doc_count", $object(num_docs)},
+ {@"update_seq", $object(update_seq)});
+ return 200;
+}
+
+
+- (TDStatus) do_PUT: (TDDatabase*)db {
+ if (db.exists)
+ return 412;
+ if (![db open])
+ return 500;
+ [_response setValue: _request.URL.absoluteString ofHeader: @"Location"];
+ return 201;
+}
+
+
+- (TDStatus) do_DELETE: (TDDatabase*)db {
+ if ([self query: @"rev"])
+ return 400; // CouchDB checks for this; probably meant to be a document deletion
+ return [_server deleteDatabaseNamed: db.name] ? 200 : 404;
+}
+
+
+- (TDStatus) do_POST: (TDDatabase*)db {
+ TDStatus status = [self openDB];
+ if (status >= 300)
+ return status;
+ return [self update: db docID: nil json: _request.HTTPBody deleting: NO];
+}
+
+
+- (TDStatus) do_GET_all_docs: (TDDatabase*)db {
+ TDQueryOptions options;
+ if (![self getQueryOptions: &options])
+ return 400;
+ NSDictionary* result = [db getAllDocs: &options];
+ if (!result)
+ return 500;
+ _response.bodyObject = result;
+ return 200;
+}
+
+
+- (TDStatus) do_POST_all_docs: (TDDatabase*)db {
+ // http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
+ TDQueryOptions options;
+ if (![self getQueryOptions: &options])
+ return 400;
+
+ NSDictionary* body = [NSJSONSerialization JSONObjectWithData: _request.HTTPBody
+ options: 0 error: nil];
+ if (![body isKindOfClass: [NSDictionary class]])
+ return 400;
+ NSArray* docIDs = [body objectForKey: @"keys"];
+ if (![docIDs isKindOfClass: [NSArray class]])
+ return 400;
+
+ NSDictionary* result = [db getDocsWithIDs: docIDs options: &options];
+ if (!result)
+ return 500;
+ _response.bodyObject = result;
+ return 200;
+}
+
+
+- (TDStatus) do_POST_compact: (TDDatabase*)db {
+ return [db compact];
+}
+
+- (TDStatus) do_POST_ensure_full_commit: (TDDatabase*)db {
+ return 200;
+}
+
+
+#pragma mark - CHANGES:
+
+
+- (NSDictionary*) changeDictForRev: (TDRevision*)rev {
+ return $dict({@"seq", $object(rev.sequence)},
+ {@"id", rev.docID},
+ {@"changes", $marray($dict({@"rev", rev.revID}))},
+ {@"deleted", rev.deleted ? $true : nil},
+ {@"doc", (_changesIncludeDocs ? rev.properties : nil)});
+}
+
+- (NSDictionary*) responseBodyForChanges: (NSArray*)changes since: (UInt64)since {
+ NSArray* results = [changes my_map: ^(id rev) {return [self changeDictForRev: rev];}];
+ if (changes.count > 0)
+ since = [[changes lastObject] sequence];
+ return $dict({@"results", results}, {@"last_seq", $object(since)});
+}
+
+
+- (NSDictionary*) responseBodyForChangesWithConflicts: (NSArray*)changes since: (UInt64)since {
+ // Assumes the changes are grouped by docID so that conflicts will be adjacent.
+ NSMutableArray* entries = [NSMutableArray arrayWithCapacity: changes.count];
+ NSString* lastDocID = nil;
+ NSDictionary* lastEntry = nil;
+ for (TDRevision* rev in changes) {
+ NSString* docID = rev.docID;
+ if ($equal(docID, lastDocID)) {
+ [[lastEntry objectForKey: @"changes"] addObject: $dict({@"rev", rev.revID})];
+ } else {
+ lastEntry = [self changeDictForRev: rev];
+ [entries addObject: lastEntry];
+ lastDocID = docID;
+ }
+ }
+ // After collecting revisions, sort by sequence:
+ [entries sortUsingComparator: ^NSComparisonResult(id e1, id e2) {
+ return [[e1 objectForKey: @"seq"] longLongValue] - [[e2 objectForKey: @"seq"] longLongValue];
+ }];
+ return $dict({@"results", entries}, {@"last_seq", $object(since)});
+}
+
+
+- (void) sendContinuousChange: (TDRevision*)rev {
+ NSDictionary* changeDict = [self changeDictForRev: rev];
+ NSMutableData* json = [[NSJSONSerialization dataWithJSONObject: changeDict
+ options: 0 error: nil] mutableCopy];
+ [json appendBytes: @"\n" length: 1];
+ _onDataAvailable(json);
+ [json release];
+}
+
+
+- (void) dbChanged: (NSNotification*)n {
+ TDRevision* rev = [n.userInfo objectForKey: @"rev"];
+
+ if (_changesFilter && !_changesFilter(rev))
+ return;
+
+ if (_longpoll) {
+ Log(@"TDRouter: Sending longpoll response");
+ [self sendResponse];
+ NSDictionary* body = [self responseBodyForChanges: $array(rev) since: 0];
+ _onDataAvailable([NSJSONSerialization dataWithJSONObject: body
+ options: 0 error: nil]);
+ _onFinished();
+ [self stop];
+ } else {
+ Log(@"TDRouter: Sending continous change chunk");
+ [self sendContinuousChange: rev];
+ }
+}
+
+
+- (TDStatus) do_GET_changes: (TDDatabase*)db {
+ // http://wiki.apache.org/couchdb/HTTP_database_API#Changes
+ TDChangesOptions options = kDefaultTDChangesOptions;
+ _changesIncludeDocs = [self boolQuery: @"include_docs"];
+ options.includeDocs = _changesIncludeDocs;
+ options.includeConflicts = $equal([self query: @"style"], @"all_docs");
+ options.contentOptions = [self contentOptions];
+ options.sortBySequence = !options.includeConflicts;
+ options.limit = [self intQuery: @"limit" defaultValue: options.limit];
+ int since = [[self query: @"since"] intValue];
+
+ NSString* filterName = [self query: @"filter"];
+ if (filterName) {
+ _changesFilter = [[_db filterNamed: filterName] retain];
+ if (!_changesFilter)
+ return 404;
+ }
+
+ TDRevisionList* changes = [db changesSinceSequence: since
+ options: &options
+ filter: _changesFilter];
+ if (!changes)
+ return 500;
+
+ NSString* feed = [self query: @"feed"];
+ _longpoll = $equal(feed, @"longpoll");
+ BOOL continuous = !_longpoll && $equal(feed, @"continuous");
+
+ if (continuous || (_longpoll && changes.count==0)) {
+ if (continuous) {
+ [self sendResponse];
+ for (TDRevision* rev in changes)
+ [self sendContinuousChange: rev];
+ }
+ [[NSNotificationCenter defaultCenter] addObserver: self
+ selector: @selector(dbChanged:)
+ name: TDDatabaseChangeNotification
+ object: db];
+ // Don't close connection; more data to come
+ _waiting = YES;
+ return 0;
+ } else {
+ if (options.includeConflicts)
+ _response.bodyObject = [self responseBodyForChangesWithConflicts: changes.allRevisions
+ since: since];
+ else
+ _response.bodyObject = [self responseBodyForChanges: changes.allRevisions since: since];
+ return 200;
+ }
+}
+
+
+#pragma mark - DOCUMENT REQUESTS:
+
+
+- (NSString*) setResponseEtag: (TDRevision*)rev {
+ NSString* eTag = $sprintf(@"\"%@\"", rev.revID);
+ [_response setValue: eTag ofHeader: @"Etag"];
+ return eTag;
+}
+
+
+- (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID {
+ // http://wiki.apache.org/couchdb/HTTP_Document_API#GET
+ TDRevision* rev = [db getDocumentWithID: docID
+ revisionID: [self query: @"rev"] // often nil
+ options: [self contentOptions]];
+ if (!rev)
+ return 404;
+
+ // Check for conditional GET:
+ NSString* eTag = [self setResponseEtag: rev];
+ if ($equal(eTag, [_request valueForHTTPHeaderField: @"If-None-Match"]))
+ return 304;
+
+ _response.body = rev.body;
+ return 200;
+}
+
+
+- (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID attachment: (NSString*)attachment {
+ //OPT: This gets the JSON body too, which is a waste. Could add a 'withBody:' attribute?
+ TDRevision* rev = [db getDocumentWithID: docID
+ revisionID: [self query: @"rev"] // often nil
+ options: 0];
+ if (!rev)
+ return 404;
+
+ // Check for conditional GET:
+ NSString* eTag = [self setResponseEtag: rev];
+ if ($equal(eTag, [_request valueForHTTPHeaderField: @"If-None-Match"]))
+ return 304;
+
+ NSString* type = nil;
+ TDStatus status;
+ NSData* contents = [_db getAttachmentForSequence: rev.sequence
+ named: attachment
+ type: &type
+ status: &status];
+ if (!contents)
+ return status;
+ if (type)
+ [_response setValue: type ofHeader: @"Content-Type"];
+ _response.body = [TDBody bodyWithJSON: contents]; //FIX: This is a lie, it's not JSON
+ return 200;
+}
+
+
+- (TDStatus) update: (TDDatabase*)db
+ docID: (NSString*)docID
+ json: (NSData*)json
+ deleting: (BOOL)deleting
+{
+ BOOL posting = (docID == nil);
+ TDBody* body = json ? [TDBody bodyWithJSON: json] : nil;
+
+ NSString* prevRevID;
+ if (!deleting) {
+ deleting = $castIf(NSNumber, [body propertyForKey: @"_deleted"]).boolValue;
+ if (!docID) {
+ // POST's doc ID may come from the _id field of the JSON body, else generate a random one.
+ docID = [body propertyForKey: @"_id"];
+ if (!docID) {
+ if (deleting)
+ return 400;
+ docID = [TDDatabase generateDocumentID];
+ }
+ }
+ // PUT's revision ID comes from the JSON body.
+ prevRevID = [body propertyForKey: @"_rev"];
+ } else {
+ // DELETE's revision ID can come either from the ?rev= query param or an If-Match header.
+ prevRevID = [self query: @"rev"];
+ if (!prevRevID) {
+ NSString* ifMatch = [_request valueForHTTPHeaderField: @"If-Match"];
+ if (ifMatch) {
+ // Value of If-Match is an ETag, so have to trim the quotes around it:
+ if (ifMatch.length > 2 && [ifMatch hasPrefix: @"\""] && [ifMatch hasSuffix: @"\""])
+ prevRevID = [ifMatch substringWithRange: NSMakeRange(1, ifMatch.length-2)];
+ else
+ return 400;
+ }
+ }
+ }
+
+ TDRevision* rev = [[[TDRevision alloc] initWithDocID: docID revID: nil deleted: deleting]
+ autorelease];
+ if (!rev)
+ return 400;
+ rev.body = body;
+
+ TDStatus status;
+ rev = [db putRevision: rev prevRevisionID: prevRevID status: &status];
+ if (status < 300) {
+ [self setResponseEtag: rev];
+ if (!deleting) {
+ NSURL* url = _request.URL;
+ if (posting)
+ url = [url URLByAppendingPathComponent: docID];
+ [_response.headers setObject: url.absoluteString forKey: @"Location"];
+ }
+ _response.bodyObject = $dict({@"ok", $true},
+ {@"id", rev.docID},
+ {@"rev", rev.revID});
+ }
+ return status;
+}
+
+
+- (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID {
+ NSData* json = _request.HTTPBody;
+ if (!json)
+ return 400;
+
+ if (![self query: @"new_edits"] || [self boolQuery: @"new_edits"]) {
+ // Regular PUT:
+ return [self update: db docID: docID json: json deleting: NO];
+ } else {
+ // PUT with new_edits=false -- forcible insertion of existing revision:
+ TDBody* body = [TDBody bodyWithJSON: json];
+ TDRevision* rev = [[[TDRevision alloc] initWithBody: body] autorelease];
+ if (!rev || !$equal(rev.docID, docID) || !rev.revID)
+ return 400;
+ NSArray* history = [TDDatabase parseCouchDBRevisionHistory: body.properties];
+ if (!history)
+ history = $array(rev.revID);
+ return [_db forceInsert: rev revisionHistory: history source: nil];
+ }
+}
+
+
+- (TDStatus) do_DELETE: (TDDatabase*)db docID: (NSString*)docID {
+ return [self update: db docID: docID json: nil deleting: YES];
+}
+
+
+#pragma mark - VIEW QUERIES:
+
+
+- (TDStatus) do_GET: (TDDatabase*)db designDocID: (NSString*)designDoc view: (NSString*)viewName {
+ viewName = $sprintf(@"%@/%@", designDoc, viewName);
+ TDView* view = [db existingViewNamed: viewName];
+ if (!view)
+ return 404;
+
+ TDQueryOptions options;
+ if (![self getQueryOptions: &options])
+ return 400;
+
+ TDStatus status;
+ NSArray* rows = [view queryWithOptions: &options status: &status];
+ if (!rows)
+ return status;
+ id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
+ _response.bodyObject = $dict({@"rows", rows},
+ {@"total_rows", $object(rows.count)},
+ {@"offset", $object(options.skip)},
+ {@"update_seq", updateSeq});
+ return 200;
+}
+
+
+- (TDStatus) do_POST_temp_view: (TDDatabase*)db {
+ if (![[_request valueForHTTPHeaderField: @"Content-Type"] hasPrefix: @"application/json"])
+ return 415;
+ TDBody* requestBody = [TDBody bodyWithJSON: _request.HTTPBody];
+ NSDictionary* props = requestBody.properties;
+ if (!props)
+ return 400;
+
+ TDQueryOptions options;
+ if (![self getQueryOptions: &options])
+ return 400;
+
+ TDView* view = [_db viewNamed: @"@@TEMP@@"];
+ if (!view)
+ return 500;
+ @try {
+ NSString* language = [props objectForKey: @"language"] ?: @"javascript";
+ NSString* mapSource = [props objectForKey: @"map"];
+ TDMapBlock mapBlock = [[TDView compiler] compileMapFunction: mapSource language: language];
+ if (!mapBlock) {
+ Warn(@"Unknown map function source: %@", mapSource);
+ return 500;
+ }
+ NSString* reduceSource = [props objectForKey: @"reduce"];
+ TDReduceBlock reduceBlock = NULL;
+ if (reduceSource) {
+ reduceBlock =[[TDView compiler] compileReduceFunction: reduceSource language: language];
+ if (!reduceBlock) {
+ Warn(@"Unknown reduce function source: %@", reduceSource);
+ return 500;
+ }
+ }
+
+ [view setMapBlock: mapBlock reduceBlock: reduceBlock version: @"1"];
+ if (reduceBlock)
+ options.reduce = YES;
+
+ TDStatus status;
+ NSArray* rows = [view queryWithOptions: &options status: &status];
+ if (!rows)
+ return status;
+ id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
+ _response.bodyObject = $dict({@"rows", rows},
+ {@"total_rows", $object(rows.count)},
+ {@"offset", $object(options.skip)},
+ {@"update_seq", updateSeq});
+ return 200;
+ } @finally {
+ [view deleteView];
+ }
+}
+
+
+@end
View
15 Source/TDRouter.h
@@ -39,9 +39,6 @@ typedef void (^OnFinishedBlock)();
- (id) initWithServer: (TDServer*)server request: (NSURLRequest*)request;
-- (NSString*) query: (NSString*)param;
-- (BOOL) boolQuery: (NSString*)param;
-
@property (copy) OnResponseReadyBlock onResponseReady;
@property (copy) OnDataAvailableBlock onDataAvailable;
@property (copy) OnFinishedBlock onFinished;
@@ -54,6 +51,18 @@ typedef void (^OnFinishedBlock)();
@end
+@interface TDRouter (Internal)
+- (NSString*) query: (NSString*)param;
+- (BOOL) boolQuery: (NSString*)param;
+- (int) intQuery: (NSString*)param defaultValue: (int)defaultValue;
+- (id) jsonQuery: (NSString*)param error: (NSError**)outError;
+- (TDContentOptions) contentOptions;
+- (BOOL) getQueryOptions: (struct TDQueryOptions*)options;
+- (TDStatus) openDB;
+- (void) sendResponse;
+@end
+
+
@interface TDResponse : NSObject
{
View
559 Source/TDRouter.m
@@ -15,25 +15,18 @@
#import "TDRouter.h"
#import "TDDatabase.h"
+#import "TDServer.h"
#import "TDView.h"
#import "TDBody.h"
-#import "TDRevision.h"
-#import "TDServer.h"
-#import "TDReplicator.h"
#import <objc/message.h>
NSString* const kTDVersionString = @"0.2";
-@interface TDRouter ()
-- (TDStatus) update: (TDDatabase*)db docID: (NSString*)docID json: (NSData*)json
- deleting: (BOOL)deleting;
-@end
-
-
@implementation TDRouter
+
- (id) initWithServer: (TDServer*)server request: (NSURLRequest*)request {
NSParameterAssert(server);
NSParameterAssert(request);
@@ -319,557 +312,9 @@ - (TDStatus) do_UNKNOWN {
}
-#pragma mark - SERVER REQUESTS:
-
-
-- (TDStatus) do_GETRoot {
- NSDictionary* info = $dict({@"TouchDB", @"Welcome"},
- {@"couchdb", @"Welcome"}, // for compatibility
- {@"version", kTDVersionString});
- _response.body = [TDBody bodyWithProperties: info];
- return 200;
-}
-
-- (TDStatus) do_GET_all_dbs {
- NSArray* dbs = _server.allDatabaseNames ?: $array();
- _response.body = [[[TDBody alloc] initWithArray: dbs] autorelease];
- return 200;
-}
-
-- (TDStatus) do_POST_replicate {
- // Extract the parameters from the JSON request body:
- // http://wiki.apache.org/couchdb/Replication
- id body = [NSJSONSerialization JSONObjectWithData: _request.HTTPBody options: 0 error: nil];
- if (![body isKindOfClass: [NSDictionary class]])
- return 400;
- NSString* source = $castIf(NSString, [body objectForKey: @"source"]);
- NSString* target = $castIf(NSString, [body objectForKey: @"target"]);
- BOOL createTarget = [$castIf(NSNumber, [body objectForKey: @"create_target"]) boolValue];
- BOOL continuous = [$castIf(NSNumber, [body objectForKey: @"continuous"]) boolValue];
- BOOL cancel = [$castIf(NSNumber, [body objectForKey: @"cancel"]) boolValue];
-
- // Map the 'source' and 'target' JSON params to a local database and remote URL:
- if (!source || !target)
- return 400;
- BOOL push = NO;
- TDDatabase* db = [_server existingDatabaseNamed: source];
- NSString* remoteStr;
- if (db) {
- remoteStr = target;
- push = YES;
- } else {
- remoteStr = source;
- if (createTarget && !cancel) {
- db = [_server databaseNamed: target];
- if (![db open])
- return 500;
- } else {
- db = [_server existingDatabaseNamed: target];
- }
- if (!db)
- return 404;
- }
- NSURL* remote = [NSURL URLWithString: remoteStr];
- if (!remote || ![remote.scheme hasPrefix: @"http"])
- return 400;
-
- if (!cancel) {
- // Start replication:
- TDReplicator* repl = [db replicateWithRemoteURL: remote push: push continuous: continuous];
- if (!repl)
- return 500;
- _response.bodyObject = $dict({@"session_id", repl.sessionID});
- } else {
- // Cancel replication:
- TDReplicator* repl = [db activeReplicatorWithRemoteURL: remote push: push];
- if (!repl)
- return 404;
- [repl stop];
- }
- return 200;
-}
-
-
-- (TDStatus) do_GET_uuids {
- int count = MIN(1000, [self intQuery: @"count" defaultValue: 1]);
- NSMutableArray* uuids = [NSMutableArray arrayWithCapacity: count];
- for (int i=0; i<count; i++)
- [uuids addObject: [TDDatabase generateDocumentID]];
- _response.bodyObject = $dict({@"uuids", uuids});
- return 200;
-}
-
-
-- (TDStatus) do_GET_active_tasks {
- // http://wiki.apache.org/couchdb/HttpGetActiveTasks
- NSMutableArray* activity = $marray();
- for (TDDatabase* db in _server.allOpenDatabases) {
- for (TDReplicator* repl in db.activeReplicators) {
- NSString* source = repl.remote.absoluteString;
- NSString* target = db.name;
- if (repl.isPush) {
- NSString* temp = source;
- source = target;
- target = temp;
- }
- NSUInteger processed = repl.changesProcessed;
- NSUInteger total = repl.changesTotal;
- NSString* status = $sprintf(@"Processed %u / %u changes",
- (unsigned)processed, (unsigned)total);
- long progress = (total > 0) ? lroundf(100*(processed / (float)total)) : 0;
- [activity addObject: $dict({@"type", @"Replication"},
- {@"task", repl.sessionID},
- {@"source", source},
- {@"target", target},
- {@"status", status},
- {@"progress", $object(progress)})];
- }
- }
- _response.body = [[[TDBody alloc] initWithArray: activity] autorelease];
- return 200;
-}
-
-
-#pragma mark - DATABASE REQUESTS:
-
-
-- (TDStatus) do_GET: (TDDatabase*)db {
- // http://wiki.apache.org/couchdb/HTTP_database_API#Database_Information
- TDStatus status = [self openDB];
- if (status >= 300)
- return status;
- NSUInteger num_docs = db.documentCount;
- SequenceNumber update_seq = db.lastSequence;
- if (num_docs == NSNotFound || update_seq == NSNotFound)
- return 500;
- _response.bodyObject = $dict({@"db_name", db.name},
- {@"doc_count", $object(num_docs)},
- {@"update_seq", $object(update_seq)});
- return 200;
-}
-
-
-- (TDStatus) do_PUT: (TDDatabase*)db {
- if (db.exists)
- return 412;
- if (![db open])
- return 500;
- [_response setValue: _request.URL.absoluteString ofHeader: @"Location"];
- return 201;
-}
-
-
-- (TDStatus) do_DELETE: (TDDatabase*)db {
- if ([self query: @"rev"])
- return 400; // CouchDB checks for this; probably meant to be a document deletion
- return [_server deleteDatabaseNamed: db.name] ? 200 : 404;
-}
-
-
-- (TDStatus) do_POST: (TDDatabase*)db {
- TDStatus status = [self openDB];
- if (status >= 300)
- return status;
- return [self update: db docID: nil json: _request.HTTPBody deleting: NO];
-}
-
-
-- (TDStatus) do_GET_all_docs: (TDDatabase*)db {
- TDQueryOptions options;
- if (![self getQueryOptions: &options])
- return 400;
- NSDictionary* result = [db getAllDocs: &options];
- if (!result)
- return 500;
- _response.bodyObject = result;
- return 200;
-}
-
-
-- (TDStatus) do_POST_all_docs: (TDDatabase*)db {
- // http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
- TDQueryOptions options;
- if (![self getQueryOptions: &options])
- return 400;
-
- NSDictionary* body = [NSJSONSerialization JSONObjectWithData: _request.HTTPBody
- options: 0 error: nil];
- if (![body isKindOfClass: [NSDictionary class]])
- return 400;
- NSArray* docIDs = [body objectForKey: @"keys"];
- if (![docIDs isKindOfClass: [NSArray class]])
- return 400;
-
- NSDictionary* result = [db getDocsWithIDs: docIDs options: &options];
- if (!result)
- return 500;
- _response.bodyObject = result;
- return 200;
-}
-
-
-- (TDStatus) do_POST_compact: (TDDatabase*)db {
- return [db compact];
-}
-
-- (TDStatus) do_POST_ensure_full_commit: (TDDatabase*)db {
- return 200;
-}
-
-
-#pragma mark - CHANGES:
-
-
-- (NSDictionary*) changeDictForRev: (TDRevision*)rev {
- return $dict({@"seq", $object(rev.sequence)},
- {@"id", rev.docID},
- {@"changes", $marray($dict({@"rev", rev.revID}))},
- {@"deleted", rev.deleted ? $true : nil},
- {@"doc", (_changesIncludeDocs ? rev.properties : nil)});
-}
-
-- (NSDictionary*) responseBodyForChanges: (NSArray*)changes since: (UInt64)since {
- NSArray* results = [changes my_map: ^(id rev) {return [self changeDictForRev: rev];}];
- if (changes.count > 0)
- since = [[changes lastObject] sequence];
- return $dict({@"results", results}, {@"last_seq", $object(since)});
-}
-
-
-- (NSDictionary*) responseBodyForChangesWithConflicts: (NSArray*)changes since: (UInt64)since {
- // Assumes the changes are grouped by docID so that conflicts will be adjacent.
- NSMutableArray* entries = [NSMutableArray arrayWithCapacity: changes.count];
- NSString* lastDocID = nil;
- NSDictionary* lastEntry = nil;
- for (TDRevision* rev in changes) {
- NSString* docID = rev.docID;
- if ($equal(docID, lastDocID)) {
- [[lastEntry objectForKey: @"changes"] addObject: $dict({@"rev", rev.revID})];
- } else {
- lastEntry = [self changeDictForRev: rev];
- [entries addObject: lastEntry];
- lastDocID = docID;
- }
- }
- // After collecting revisions, sort by sequence:
- [entries sortUsingComparator: ^NSComparisonResult(id e1, id e2) {
- return [[e1 objectForKey: @"seq"] longLongValue] - [[e2 objectForKey: @"seq"] longLongValue];
- }];
- return $dict({@"results", entries}, {@"last_seq", $object(since)});
-}
-
-
-- (void) sendContinuousChange: (TDRevision*)rev {
- NSDictionary* changeDict = [self changeDictForRev: rev];
- NSMutableData* json = [[NSJSONSerialization dataWithJSONObject: changeDict
- options: 0 error: nil] mutableCopy];
- [json appendBytes: @"\n" length: 1];
- _onDataAvailable(json);
- [json release];
-}
-
-
-- (void) dbChanged: (NSNotification*)n {
- TDRevision* rev = [n.userInfo objectForKey: @"rev"];
-
- if (_changesFilter && !_changesFilter(rev))
- return;
-
- if (_longpoll) {
- Log(@"TDRouter: Sending longpoll response");
- [self sendResponse];
- NSDictionary* body = [self responseBodyForChanges: $array(rev) since: 0];
- _onDataAvailable([NSJSONSerialization dataWithJSONObject: body
- options: 0 error: nil]);
- _onFinished();
- [self stop];
- } else {
- Log(@"TDRouter: Sending continous change chunk");
- [self sendContinuousChange: rev];
- }
-}
-
-
-- (TDStatus) do_GET_changes: (TDDatabase*)db {
- // http://wiki.apache.org/couchdb/HTTP_database_API#Changes
- TDChangesOptions options = kDefaultTDChangesOptions;
- _changesIncludeDocs = [self boolQuery: @"include_docs"];
- options.includeDocs = _changesIncludeDocs;
- options.includeConflicts = $equal([self query: @"style"], @"all_docs");
- options.contentOptions = [self contentOptions];
- options.sortBySequence = !options.includeConflicts;
- options.limit = [self intQuery: @"limit" defaultValue: options.limit];
- int since = [[self query: @"since"] intValue];
-
- NSString* filterName = [self query: @"filter"];
- if (filterName) {
- _changesFilter = [[_db filterNamed: filterName] retain];
- if (!_changesFilter)
- return 404;
- }
-
- TDRevisionList* changes = [db changesSinceSequence: since
- options: &options
- filter: _changesFilter];
- if (!changes)
- return 500;
-
- NSString* feed = [self query: @"feed"];
- _longpoll = $equal(feed, @"longpoll");
- BOOL continuous = !_longpoll && $equal(feed, @"continuous");
-
- if (continuous || (_longpoll && changes.count==0)) {
- if (continuous) {
- [self sendResponse];
- for (TDRevision* rev in changes)
- [self sendContinuousChange: rev];
- }
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(dbChanged:)
- name: TDDatabaseChangeNotification
- object: db];
- // Don't close connection; more data to come
- _waiting = YES;
- return 0;
- } else {
- if (options.includeConflicts)
- _response.bodyObject = [self responseBodyForChangesWithConflicts: changes.allRevisions
- since: since];
- else
- _response.bodyObject = [self responseBodyForChanges: changes.allRevisions since: since];
- return 200;
- }
-}
-
-
-#pragma mark - DOCUMENT REQUESTS:
-
-
-- (NSString*) setResponseEtag: (TDRevision*)rev {
- NSString* eTag = $sprintf(@"\"%@\"", rev.revID);
- [_response setValue: eTag ofHeader: @"Etag"];
- return eTag;
-}
-
-
-- (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID {
- // http://wiki.apache.org/couchdb/HTTP_Document_API#GET
- TDRevision* rev = [db getDocumentWithID: docID
- revisionID: [self query: @"rev"] // often nil
- options: [self contentOptions]];
- if (!rev)
- return 404;
-
- // Check for conditional GET:
- NSString* eTag = [self setResponseEtag: rev];
- if ($equal(eTag, [_request valueForHTTPHeaderField: @"If-None-Match"]))
- return 304;
-
- _response.body = rev.body;
- return 200;
-}
-
-
-- (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID attachment: (NSString*)attachment {
- //OPT: This gets the JSON body too, which is a waste. Could add a 'withBody:' attribute?
- TDRevision* rev = [db getDocumentWithID: docID
- revisionID: [self query: @"rev"] // often nil
- options: 0];
- if (!rev)
- return 404;
-
- // Check for conditional GET:
- NSString* eTag = [self setResponseEtag: rev];
- if ($equal(eTag, [_request valueForHTTPHeaderField: @"If-None-Match"]))
- return 304;
-
- NSString* type = nil;
- TDStatus status;
- NSData* contents = [_db getAttachmentForSequence: rev.sequence
- named: attachment
- type: &type
- status: &status];
- if (!contents)
- return status;
- if (type)
- [_response setValue: type ofHeader: @"Content-Type"];
- _response.body = [TDBody bodyWithJSON: contents]; //FIX: This is a lie, it's not JSON
- return 200;
-}
-
-
-- (TDStatus) update: (TDDatabase*)db
- docID: (NSString*)docID
- json: (NSData*)json
- deleting: (BOOL)deleting
-{
- BOOL posting = (docID == nil);
- TDBody* body = json ? [TDBody bodyWithJSON: json] : nil;
-
- NSString* prevRevID;
- if (!deleting) {
- deleting = $castIf(NSNumber, [body propertyForKey: @"_deleted"]).boolValue;
- if (!docID) {
- // POST's doc ID may come from the _id field of the JSON body, else generate a random one.
- docID = [body propertyForKey: @"_id"];
- if (!docID) {
- if (deleting)
- return 400;
- docID = [TDDatabase generateDocumentID];
- }
- }
- // PUT's revision ID comes from the JSON body.
- prevRevID = [body propertyForKey: @"_rev"];
- } else {
- // DELETE's revision ID can come either from the ?rev= query param or an If-Match header.
- prevRevID = [self query: @"rev"];
- if (!prevRevID) {
- NSString* ifMatch = [_request valueForHTTPHeaderField: @"If-Match"];
- if (ifMatch) {
- // Value of If-Match is an ETag, so have to trim the quotes around it:
- if (ifMatch.length > 2 && [ifMatch hasPrefix: @"\""] && [ifMatch hasSuffix: @"\""])
- prevRevID = [ifMatch substringWithRange: NSMakeRange(1, ifMatch.length-2)];
- else
- return 400;
- }
- }
- }
-
- TDRevision* rev = [[[TDRevision alloc] initWithDocID: docID revID: nil deleted: deleting]
- autorelease];
- if (!rev)
- return 400;
- rev.body = body;
-
- TDStatus status;
- rev = [db putRevision: rev prevRevisionID: prevRevID status: &status];
- if (status < 300) {
- [self setResponseEtag: rev];
- if (!deleting) {
- NSURL* url = _request.URL;
- if (posting)
- url = [url URLByAppendingPathComponent: docID];
- [_response.headers setObject: url.absoluteString forKey: @"Location"];
- }
- _response.bodyObject = $dict({@"ok", $true},
- {@"id", rev.docID},
- {@"rev", rev.revID});
- }
- return status;
-}
-
-
-- (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID {
- NSData* json = _request.HTTPBody;
- if (!json)
- return 400;
-
- if (![self query: @"new_edits"] || [self boolQuery: @"new_edits"]) {
- // Regular PUT:
- return [self update: db docID: docID json: json deleting: NO];
- } else {
- // PUT with new_edits=false -- forcible insertion of existing revision:
- TDBody* body = [TDBody bodyWithJSON: json];
- TDRevision* rev = [[[TDRevision alloc] initWithBody: body] autorelease];
- if (!rev || !$equal(rev.docID, docID) || !rev.revID)
- return 400;
- NSArray* history = [TDDatabase parseCouchDBRevisionHistory: body.properties];
- if (!history)
- history = $array(rev.revID);
- return [_db forceInsert: rev revisionHistory: history source: nil];
- }
-}
-
-
-- (TDStatus) do_DELETE: (TDDatabase*)db docID: (NSString*)docID {
- return [self update: db docID: docID json: nil deleting: YES];
-}
-
-
-#pragma mark - VIEW QUERIES:
-
-
-- (TDStatus) do_GET: (TDDatabase*)db designDocID: (NSString*)designDoc view: (NSString*)viewName {
- viewName = $sprintf(@"%@/%@", designDoc, viewName);
- TDView* view = [db existingViewNamed: viewName];
- if (!view)
- return 404;
-
- TDQueryOptions options;
- if (![self getQueryOptions: &options])
- return 400;
-
- TDStatus status;
- NSArray* rows = [view queryWithOptions: &options status: &status];
- if (!rows)
- return status;
- id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
- _response.bodyObject = $dict({@"rows", rows},
- {@"total_rows", $object(rows.count)},
- {@"offset", $object(options.skip)},
- {@"update_seq", updateSeq});
- return 200;
-}
-
-
-- (TDStatus) do_POST_temp_view: (TDDatabase*)db {
- if (![[_request valueForHTTPHeaderField: @"Content-Type"] hasPrefix: @"application/json"])
- return 415;
- TDBody* requestBody = [TDBody bodyWithJSON: _request.HTTPBody];
- NSDictionary* props = requestBody.properties;
- if (!props)
- return 400;
-
- TDQueryOptions options;
- if (![self getQueryOptions: &options])
- return 400;
-
- TDView* view = [_db viewNamed: @"@@TEMP@@"];
- if (!view)
- return 500;
- @try {
- NSString* language = [props objectForKey: @"language"] ?: @"javascript";
- NSString* mapSource = [props objectForKey: @"map"];
- TDMapBlock mapBlock = [[TDView compiler] compileMapFunction: mapSource language: language];
- if (!mapBlock) {
- Warn(@"Unknown map function source: %@", mapSource);
- return 500;
- }
- NSString* reduceSource = [props objectForKey: @"reduce"];
- TDReduceBlock reduceBlock = NULL;
- if (reduceSource) {
- reduceBlock =[[TDView compiler] compileReduceFunction: reduceSource language: language];
- if (!reduceBlock) {
- Warn(@"Unknown reduce function source: %@", reduceSource);
- return 500;
- }
- }
-
- [view setMapBlock: mapBlock reduceBlock: reduceBlock version: @"1"];
- if (reduceBlock)
- options.reduce = YES;
-
- TDStatus status;
- NSArray* rows = [view queryWithOptions: &options status: &status];
- if (!rows)
- return status;
- id updateSeq = options.updateSeq ? $object(view.lastSequenceIndexed) : nil;
- _response.bodyObject = $dict({@"rows", rows},
- {@"total_rows", $object(rows.count)},
- {@"offset", $object(options.skip)},
- {@"update_seq", updateSeq});
- return 200;
- } @finally {
- [view deleteView];
- }
-}
-
-
@end
-
-
#pragma mark - TDRESPONSE
@implementation TDResponse
View
6 TouchDB.xcodeproj/project.pbxproj
@@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */
2700BC5114B3864B00B5B297 /* HTTPFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 2753156914ACEFC90065964D /* HTTPFileResponse.m */; };
+ 2700BC5E14B64AA600B5B297 /* TDRouter+Handlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 2700BC5B14B64AA600B5B297 /* TDRouter+Handlers.m */; };
+ 2700BC5F14B64AA600B5B297 /* TDRouter+Handlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 2700BC5B14B64AA600B5B297 /* TDRouter+Handlers.m */; };
270B3DFD1489359000E0A926 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270B3DFC1489359000E0A926 /* SenTestingKit.framework */; };
270B3DFE1489359000E0A926 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27C70697148864BA00F0F099 /* Cocoa.framework */; };
270B3E011489359000E0A926 /* TouchDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270B3DEA1489359000E0A926 /* TouchDB.framework */; };
@@ -311,6 +313,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 2700BC5B14B64AA600B5B297 /* TDRouter+Handlers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TDRouter+Handlers.m"; sourceTree = "<group>"; };
270B3DEA1489359000E0A926 /* TouchDB.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TouchDB.framework; sourceTree = BUILT_PRODUCTS_DIR; };
270B3DEE1489359000E0A926 /* TouchDB-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TouchDB-Info.plist"; sourceTree = "<group>"; };
270B3DFB1489359000E0A926 /* TouchDBTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TouchDBTests.octest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -825,6 +828,7 @@
children = (
27C706431486BE7100F0F099 /* TDRouter.h */,
27C706441486BE7100F0F099 /* TDRouter.m */,
+ 2700BC5B14B64AA600B5B297 /* TDRouter+Handlers.m */,
27C706461487584300F0F099 /* TDURLProtocol.h */,
27C706471487584300F0F099 /* TDURLProtocol.m */,
);
@@ -1492,6 +1496,7 @@
2753160014ACF9330065964D /* Logging.m in Sources */,
2753160114ACF9330065964D /* CollectionUtils.m in Sources */,
2753160214ACF9330065964D /* Test.m in Sources */,
+ 2700BC5E14B64AA600B5B297 /* TDRouter+Handlers.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1569,6 +1574,7 @@
27A82E3914A1145000C0B850 /* FMDatabaseAdditions.m in Sources */,
27AA409E14AA86AE00E2A5FF /* TDDatabase+Insertion.m in Sources */,
27AA40A414AA8A6600E2A5FF /* TDDatabase+Replication.m in Sources */,
+ 2700BC5F14B64AA600B5B297 /* TDRouter+Handlers.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Please sign in to comment.
Something went wrong with that request. Please try again.