Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: couchbaselabs/TouchDB-iOS
base: d59f86ce89
...
head fork: couchbaselabs/TouchDB-iOS
compare: 40ae2aee86
  • 2 commits
  • 16 files changed
  • 0 commit comments
  • 1 contributor
Commits on Mar 30, 2012
@snej snej Add router support for PUT of document in MIME multipart format
This is necessary for allowing TouchDB to be pushed to by another server.
ee1c751
@snej snej Fixed a race condition in the HTTP listener
Sometimes it would never send the body of a response or close the socket.
I saw this happen with error responses like 404s; not sure it it would happen otherwise.
40ae2ae
View
5 Listener/TDHTTPResponse.m
@@ -169,6 +169,7 @@ - (NSData*) readDataOfLength: (NSUInteger)length {
- (BOOL) isDone {
+ LogTo(TDListenerVerbose, @"%@ answers isDone=%d", self, _finished);
return _finished;
}
@@ -187,7 +188,7 @@ - (void) onFinished {
_router.onDataAvailable = nil;
_router.onFinished = nil;
- if (_chunked) {
+ if (_chunked && _offset > 0) {
[_connection responseHasAvailableData: self];
} else {
// Response finished immediately, before the connection asked for any data, so we're free
@@ -205,9 +206,9 @@ - (void) onFinished {
}
NSString* responseStr = [NSString stringWithFormat: @"{\"status\": %i, \"error\":\"%@\"}\n",
status, errorMsg];
+ [_response.headers setObject: @"text/plain; encoding=UTF-8" forKey: @"Content-Type"];
[self onDataAvailable: [responseStr dataUsingEncoding: NSUTF8StringEncoding]
finished: NO];
- [_response.headers setObject: @"text/plain; encoding=UTF-8" forKey: @"Content-Type"];
} else {
#if DEBUG
BOOL pretty = YES;
View
5 Source/TDBlobStore.h
@@ -85,8 +85,9 @@ typedef struct {
/** After finishing, this is the key for looking up the blob through the TDBlobStore. */
@property (readonly) TDBlobKey blobKey;
-/** After finishing, this is the MD5 digest of the blob.
+/** After finishing, this is the MD5 digest of the blob, in base64 with an "md5-" prefix.
(This is useful for compatibility with CouchDB, which stores MD5 digests of attachments.) */
-@property (readonly) TDMD5Key MD5Digest;
+@property (readonly) NSString* MD5DigestString;
+@property (readonly) NSString* SHA1DigestString;
@end
View
13 Source/TDBlobStore.m
@@ -14,6 +14,7 @@
// and limitations under the License.
#import "TDBlobStore.h"
+#import "TDBase64.h"
#import "TDMisc.h"
#define kFileExtension "blob"
@@ -218,7 +219,7 @@ - (NSString*) tempDir {
@implementation TDBlobStoreWriter
-@synthesize length=_length, blobKey=_blobKey, MD5Digest=_MD5Digest;
+@synthesize length=_length, blobKey=_blobKey;
- (id) initWithStore: (TDBlobStore*)store {
self = [super init];
@@ -265,6 +266,16 @@ - (void) finish {
CC_MD5_Final(_MD5Digest.bytes, &_md5Ctx);
}
+- (NSString*) MD5DigestString {
+ return [@"md5-" stringByAppendingString: [TDBase64 encode: &_MD5Digest
+ length: sizeof(_MD5Digest)]];
+}
+
+- (NSString*) SHA1DigestString {
+ return [@"sha1-" stringByAppendingString: [TDBase64 encode: &_blobKey
+ length: sizeof(_blobKey)]];
+}
+
- (BOOL) install {
if (!_tempPath)
return YES; // already installed
View
15 Source/TDDatabase+Attachments.m
@@ -60,6 +60,16 @@ - (void) rememberAttachmentWritersForDigests: (NSDictionary*)blobsByDigests {
}
+- (TDBlobStoreWriter*) attachmentWriterForAttachment: (NSDictionary*)attachment {
+ NSString* digest = $castIf(NSString, [attachment objectForKey: @"digest"]);
+ if (!digest)
+ return nil;
+ TDBlobStoreWriter* writer = [[_pendingAttachmentsByDigest objectForKey: digest] retain];
+ [_pendingAttachmentsByDigest removeObjectForKey: digest];
+ return [writer autorelease];
+}
+
+
- (NSData*) keyForAttachment: (NSData*)contents {
Assert(contents);
TDBlobKey key;
@@ -334,10 +344,9 @@ - (TDStatus) processAttachmentsForRevision: (TDRevision*)rev
// "follows" means the uploader provided the attachment in a separate MIME part.
// This means it's already been registered in _pendingAttachmentsByDigest;
// I just need to look it up by its "digest" property and install it into the store:
- NSString* digest = $castIf(NSString, [newAttach objectForKey: @"digest"]);
- if (!digest)
+ TDBlobStoreWriter *writer = [self attachmentWriterForAttachment: newAttach];
+ if (!writer)
return 400;
- TDBlobStoreWriter *writer = [_pendingAttachmentsByDigest objectForKey: digest];
if (![writer install])
return 500;
TDBlobKey key = writer.blobKey;
View
1  Source/TDInternal.h
@@ -41,6 +41,7 @@ extern NSString* const kTDAttachmentBlobKeyProperty;
@interface TDDatabase (Attachments_Internal)
- (void) rememberAttachmentWritersForDigests: (NSDictionary*)writersByDigests;
+- (TDBlobStoreWriter*) attachmentWriterForAttachment: (NSDictionary*)attachment;
- (NSData*) keyForAttachment: (NSData*)contents;
- (TDStatus) insertAttachmentWithKey: (NSData*)keyData
forSequence: (SequenceNumber)sequence
View
7 Source/TDMisc.h
@@ -27,5 +27,12 @@ NSString* TDEscapeID( NSString* param );
This does the usual %-escaping, but makes sure that '&' is also escaped. */
NSString* TDEscapeURLParam( NSString* param );
+/** Wraps a string in double-quotes and prepends backslashes to any existing double-quote or backslash characters in it. */
+NSString* TDQuoteString( NSString* param );
+
+/** Undoes effect of TDQuoteString, i.e. removes backslash escapes and any surrounding double-quotes.
+ If the string has no surrounding double-quotes it will be returned as-is. */
+NSString* TDUnquoteString( NSString* param );
+
/** Returns YES if this error appears to be due to the computer being offline or the remote host being unreachable. */
BOOL TDIsOfflineError( NSError* error );
View
61 Source/TDMisc.m
@@ -76,6 +76,44 @@ NSComparisonResult TDSequenceCompare( SequenceNumber a, SequenceNumber b) {
}
+NSString* TDQuoteString( NSString* param ) {
+ NSMutableString* quoted = [[param mutableCopy] autorelease];
+ [quoted replaceOccurrencesOfString: @"\\" withString: @"\\\\"
+ options: NSLiteralSearch
+ range: NSMakeRange(0, quoted.length)];
+ [quoted replaceOccurrencesOfString: @"\"" withString: @"\\\""
+ options: NSLiteralSearch
+ range: NSMakeRange(0, quoted.length)];
+ [quoted insertString: @"\"" atIndex: 0];
+ [quoted appendString: @"\""];
+ return quoted;
+}
+
+
+NSString* TDUnquoteString( NSString* param ) {
+ if (![param hasPrefix: @"\""])
+ return param;
+ if (![param hasSuffix: @"\""] || param.length < 2)
+ return nil;
+ param = [param substringWithRange: NSMakeRange(1, param.length - 2)];
+ if ([param rangeOfString: @"\\"].length == 0)
+ return param;
+ NSMutableString* unquoted = [[param mutableCopy] autorelease];
+ for (NSUInteger pos = 0; pos < unquoted.length; ) {
+ NSRange r = [unquoted rangeOfString: @"\\"
+ options: NSLiteralSearch
+ range: NSMakeRange(pos, unquoted.length-pos)];
+ if (r.length == 0)
+ break;
+ [unquoted deleteCharactersInRange: r];
+ pos = r.location + 1;
+ if (pos > unquoted.length)
+ return nil;
+ }
+ return unquoted;
+}
+
+
BOOL TDIsOfflineError( NSError* error ) {
NSString* domain = error.domain;
NSInteger code = error.code;
@@ -85,3 +123,26 @@ BOOL TDIsOfflineError( NSError* error ) {
|| code == NSURLErrorInternationalRoamingOff;
return NO;
}
+
+
+
+TestCase(TDQuoteString) {
+ CAssertEqual(TDQuoteString(@""), @"\"\"");
+ CAssertEqual(TDQuoteString(@"foo"), @"\"foo\"");
+ CAssertEqual(TDQuoteString(@"f\"o\"o"), @"\"f\\\"o\\\"o\"");
+ CAssertEqual(TDQuoteString(@"\\foo"), @"\"\\\\foo\"");
+ CAssertEqual(TDQuoteString(@"\""), @"\"\\\"\"");
+ CAssertEqual(TDQuoteString(@""), @"\"\"");
+
+ CAssertEqual(TDUnquoteString(@""), @"");
+ CAssertEqual(TDUnquoteString(@"\""), nil);
+ CAssertEqual(TDUnquoteString(@"\"\""), @"");
+ CAssertEqual(TDUnquoteString(@"\"foo"), nil);
+ CAssertEqual(TDUnquoteString(@"foo\""), @"foo\"");
+ CAssertEqual(TDUnquoteString(@"foo"), @"foo");
+ CAssertEqual(TDUnquoteString(@"\"foo\""), @"foo");
+ CAssertEqual(TDUnquoteString(@"\"f\\\"o\\\"o\""), @"f\"o\"o");
+ CAssertEqual(TDUnquoteString(@"\"\\foo\""), @"foo");
+ CAssertEqual(TDUnquoteString(@"\"\\\\foo\""), @"\\foo");
+ CAssertEqual(TDUnquoteString(@"\"foo\\\""), nil);
+}
View
43 Source/TDMultipartDocumentReader.h
@@ -0,0 +1,43 @@
+//
+// TDMultipartDocumentReader.h
+//
+//
+// Created by Jens Alfke on 3/29/12.
+// Copyright (c) 2012 Couchbase, Inc. All rights reserved.
+//
+
+#import "TDMultipartReader.h"
+@class TDDatabase, TDRevision, TDBlobStoreWriter;
+
+
+@interface TDMultipartDocumentReader : NSObject <TDMultipartReaderDelegate>
+{
+ @private
+ TDDatabase* _database;
+ int _status;
+ TDMultipartReader* _multipartReader;
+ NSMutableData* _jsonBuffer;
+ TDBlobStoreWriter* _curAttachment;
+ NSMutableDictionary* _attachmentsByName; // maps attachment name --> TDBlobStoreWriter
+ NSMutableDictionary* _attachmentsByDigest; // maps attachment MD5 --> TDBlobStoreWriter
+ NSMutableDictionary* _document;
+}
+
++ (NSDictionary*) readData: (NSData*)data
+ ofType: (NSString*)contentType
+ toDatabase: (TDDatabase*)database
+ status: (int*)outStatus;
+
+- (id) initWithDatabase: (TDDatabase*)database;
+
+@property (readonly, nonatomic) int status;
+@property (readonly, nonatomic) NSDictionary* document;
+@property (readonly, nonatomic) NSUInteger attachmentCount;
+
+- (BOOL) setContentType: (NSString*)contentType;
+
+- (BOOL) appendData: (NSData*)data;
+
+- (BOOL) finish;
+
+@end
View
257 Source/TDMultipartDocumentReader.m
@@ -0,0 +1,257 @@
+//
+// TDMultipartDocumentReader.m
+//
+//
+// Created by Jens Alfke on 3/29/12.
+// Copyright (c) 2012 Couchbase, Inc. All rights reserved.
+//
+
+#import "TDMultipartDocumentReader.h"
+#import "TDDatabase+Attachments.h"
+#import "TDBlobStore.h"
+#import "TDInternal.h"
+#import "TDBase64.h"
+#import "TDMisc.h"
+#import "CollectionUtils.h"
+
+
+@implementation TDMultipartDocumentReader
+
+
++ (NSDictionary*) readData: (NSData*)data
+ ofType: (NSString*)contentType
+ toDatabase: (TDDatabase*)database
+ status: (int*)outStatus
+{
+ NSDictionary* result = nil;
+ TDMultipartDocumentReader* reader = [[self alloc] initWithDatabase: database];
+ if ([reader setContentType: contentType]
+ && [reader appendData: data]
+ && [reader finish]) {
+ result = [[reader.document retain] autorelease];
+ }
+ if (outStatus)
+ *outStatus = reader.status;
+ [reader release];
+ return result;
+}
+
+
+
+- (id) initWithDatabase: (TDDatabase*)database
+{
+ Assert(database);
+ self = [super init];
+ if (self) {
+ _database = database;
+ }
+ return self;
+}
+
+
+- (void) dealloc {
+ [_curAttachment cancel];
+ [_curAttachment release];
+ [_multipartReader release];
+ [_jsonBuffer release];
+ [_document release];
+ [_attachmentsByName autorelease];
+ [_attachmentsByDigest autorelease];
+ [super dealloc];
+}
+
+
+@synthesize status=_status, document=_document;
+
+
+- (NSUInteger) attachmentCount {
+ return _attachmentsByDigest.count;
+}
+
+
+- (BOOL) setContentType: (NSString*)contentType {
+ if ([contentType hasPrefix: @"multipart/"]) {
+ // Multipart, so initialize the parser:
+ LogTo(SyncVerbose, @"%@: has attachments, %@", self, contentType);
+ _multipartReader = [[TDMultipartReader alloc] initWithContentType: contentType delegate: self];
+ if (_multipartReader) {
+ _attachmentsByName = [[NSMutableDictionary alloc] init];
+ _attachmentsByDigest = [[NSMutableDictionary alloc] init];
+ return YES;
+ }
+ } else if (contentType == nil || [contentType hasPrefix: @"application/json"]) {
+ // No multipart, so no attachments. Body is pure JSON:
+ _jsonBuffer = [[NSMutableData alloc] initWithCapacity: 1024];
+ return YES;
+ }
+ // Unknown/invalid MIME type:
+ _status = 406;
+ return NO;
+}
+
+
+- (BOOL) appendData:(NSData *)data {
+ if (_multipartReader) {
+ [_multipartReader appendData: data];
+ if (_multipartReader.failed) {
+ Warn(@"%@: received unparseable MIME multipart response", self);
+ _status = 502;
+ return NO;
+ }
+ } else {
+ [_jsonBuffer appendData: data];
+ }
+ return YES;
+}
+
+
+- (BOOL) finish {
+ LogTo(SyncVerbose, @"%@: Finished loading (%u attachments)", self, _attachmentsByDigest.count);
+ if (_multipartReader) {
+ if (!_multipartReader.finished) {
+ Warn(@"%@: received incomplete MIME multipart response", self);
+ _status = 502;
+ return NO;
+ }
+
+ if (![self registerAttachments]) {
+ _status = 400;
+ return NO;
+ }
+ } else {
+ if (![self parseJSONBuffer])
+ return NO;
+ }
+ _status = 201;
+ return YES;
+}
+
+
+#pragma mark - MIME PARSER CALLBACKS:
+
+
+/** Callback: A part's headers have been parsed, but not yet its data. */
+- (void) startedPart: (NSDictionary*)headers {
+ // First MIME part is the document's JSON body; the rest are attachments.
+ if (!_document)
+ _jsonBuffer = [[NSMutableData alloc] initWithCapacity: 1024];
+ else {
+ LogTo(SyncVerbose, @"%@: Starting attachment #%u...", self, _attachmentsByDigest.count + 1);
+ _curAttachment = [[_database attachmentWriter] retain];
+
+ // See whether the attachment name is in the headers.
+ NSString* disposition = [headers objectForKey: @"Content-Disposition"];
+ if ([disposition hasPrefix: @"attachment; filename="]) {
+ // TODO: Parse this less simplistically. Right now it assumes it's in exactly the same
+ // format generated by -[TDPusher uploadMultipartRevision:]. CouchDB (as of 1.2) doesn't
+ // output any headers at all on attachments so there's no compatibility issue yet.
+ NSString* name = TDUnquoteString([disposition substringFromIndex: 21]);
+ if (name)
+ [_attachmentsByName setObject: _curAttachment forKey: name];
+ }
+ }
+}
+
+
+/** Callback: Append data to a MIME part's body. */
+- (void) appendToPart: (NSData*)data {
+ if (_jsonBuffer)
+ [_jsonBuffer appendData: data];
+ else
+ [_curAttachment appendData: data];
+}
+
+
+/** Callback: A MIME part is complete. */
+- (void) finishedPart {
+ if (_jsonBuffer) {
+ [self parseJSONBuffer];
+ } else {
+ // Finished downloading an attachment. Remember the association from the MD5 digest
+ // (which appears in the body's _attachments dict) to the blob-store key of the data.
+ [_curAttachment finish];
+ NSString* md5Str = _curAttachment.MD5DigestString;
+#ifndef MY_DISABLE_LOGGING
+ if (WillLogTo(SyncVerbose)) {
+ TDBlobKey key = _curAttachment.blobKey;
+ NSData* keyData = [NSData dataWithBytes: &key length: sizeof(key)];
+ LogTo(SyncVerbose, @"%@: Finished attachment #%u: len=%uk, digest=%@, SHA1=%@",
+ self, _attachmentsByDigest.count+1, (unsigned)_curAttachment.length/1024,
+ md5Str, keyData);
+ }
+#endif
+ [_attachmentsByDigest setObject: _curAttachment forKey: md5Str];
+ setObj(&_curAttachment, nil);
+ }
+}
+
+
+#pragma mark - INTERNALS:
+
+
+- (BOOL) parseJSONBuffer {
+ id document = [TDJSON JSONObjectWithData: _jsonBuffer
+ options: TDJSONReadingMutableContainers
+ error: nil];
+ setObj(&_jsonBuffer, nil);
+ if (![document isKindOfClass: [NSDictionary class]]) {
+ Warn(@"%@: received unparseable JSON data '%@'",
+ self, [_jsonBuffer my_UTF8ToString]);
+ _status = 502;
+ return NO;
+ }
+ _document = [document retain];
+ return YES;
+}
+
+
+- (BOOL) registerAttachments {
+ NSDictionary* attachments = [_document objectForKey: @"_attachments"];
+ if (![attachments isKindOfClass: [NSDictionary class]])
+ return NO;
+ NSUInteger nAttachmentsInDoc = 0;
+ for (NSString* attachmentName in attachments) {
+ NSMutableDictionary* attachment = [attachments objectForKey: attachmentName];
+ if ([[attachment objectForKey: @"follows"] isEqual: $true]) {
+ // Check that each attachment in the JSON corresponds to an attachment MIME body.
+ // Look up the attachment by either its MIME Content-Disposition header or MD5 digest:
+ NSString* digest = [attachment objectForKey: @"digest"];
+ TDBlobStoreWriter* writer = [_attachmentsByName objectForKey: attachmentName];
+ if (writer) {
+ NSString* actualDigest = writer.MD5DigestString;
+ if (digest && !$equal(digest, actualDigest)
+ && !$equal(digest, writer.SHA1DigestString)) {
+ Log(@"TDMultipartDocumentReader: Attachment '%@' has incorrect MD5 digest "
+ "(%@; should be %@)",
+ attachmentName, digest, actualDigest);
+ return NO;
+ }
+ [attachment setObject: actualDigest forKey: @"digest"];
+ } else {
+ writer = [_attachmentsByDigest objectForKey: digest];
+ if (!writer) {
+ Warn(@"TDMultipartDocumentReader: Attachment '%@' does not appear in a MIME body",
+ attachmentName);
+ return NO;
+ }
+ }
+
+ // Check that the length matches:
+ NSNumber* lengthObj = [attachment objectForKey: @"encoded_length"]
+ ?: [attachment objectForKey: @"length"];
+ if (!lengthObj)
+ return NO;
+ if (writer.length != [$castIf(NSNumber, lengthObj) unsignedLongLongValue])
+ return NO;
+ ++nAttachmentsInDoc;
+ }
+ }
+ if (nAttachmentsInDoc < _attachmentsByDigest.count)
+ return NO; // Some MIME bodies didn't match attachments in the document
+ // If everything's copacetic, hand over the (uninstalled) blobs to the database to remember:
+ [_database rememberAttachmentWritersForDigests: _attachmentsByDigest];
+ return YES;
+}
+
+
+@end
View
15 Source/TDMultipartDownloader.h
@@ -7,30 +7,21 @@
//
#import "TDRemoteRequest.h"
-#import "TDMultipartReader.h"
-@class TDDatabase, TDRevision, TDBlobStoreWriter;
+@class TDMultipartDocumentReader, TDDatabase;
/** Downloads a remote CouchDB document in multipart format.
Attachments are added to the database, but the document body isn't. */
-@interface TDMultipartDownloader : TDRemoteRequest <TDMultipartReaderDelegate>
+@interface TDMultipartDownloader : TDRemoteRequest
{
@private
- TDDatabase* _database;
- TDRevision* _revision;
- TDMultipartReader* _multipartReader;
- NSMutableData* _jsonBuffer;
- TDBlobStoreWriter* _curAttachment;
- NSMutableDictionary* _attachmentsByDigest; // maps 'digest' property --> TDBlobStoreWriter
- NSDictionary* _document;
+ TDMultipartDocumentReader* _reader;
}
- (id) initWithURL: (NSURL*)url
database: (TDDatabase*)database
- revision: (TDRevision*)revision
onCompletion: (TDRemoteRequestCompletionBlock)onCompletion;
-@property (readonly) TDRevision* revision;
@property (readonly) NSDictionary* document;
@end
View
173 Source/TDMultipartDownloader.m
@@ -14,10 +14,9 @@
// and limitations under the License.
#import "TDMultipartDownloader.h"
-#import "TDDatabase+Attachments.h"
+#import "TDMultipartDocumentReader.h"
#import "TDBlobStore.h"
#import "TDInternal.h"
-#import "TDBase64.h"
#import "TDMisc.h"
#import "CollectionUtils.h"
@@ -25,18 +24,13 @@
@implementation TDMultipartDownloader
-@synthesize revision=_revision, document=_document;
-
-
- (id) initWithURL: (NSURL*)url
database: (TDDatabase*)database
- revision: (TDRevision*)revision
onCompletion: (TDRemoteRequestCompletionBlock)onCompletion
{
self = [super initWithMethod: @"GET" URL: url body: nil onCompletion: onCompletion];
if (self) {
- _database = database;
- _revision = [revision retain];
+ _reader = [[TDMultipartDocumentReader alloc] initWithDatabase: database];
}
return self;
}
@@ -54,68 +48,13 @@ - (void) setupRequest: (NSMutableURLRequest*)request withBody: (id)body {
- (void) dealloc {
- [_document release];
- [_revision release];
- [_attachmentsByDigest autorelease];
+ [_reader release];
[super dealloc];
}
-- (void) clearConnection {
- [_curAttachment cancel];
- setObj(&_curAttachment, nil);
- setObj(&_multipartReader, nil);
- setObj(&_jsonBuffer, nil);
- [super clearConnection];
-}
-
-
-- (BOOL) parseJSONBuffer {
- id document = [TDJSON JSONObjectWithData: _jsonBuffer options: 0 error: nil];
- setObj(&_jsonBuffer, nil);
- if (![document isKindOfClass: [NSDictionary class]]) {
- Warn(@"%@: received unparseable JSON data '%@'",
- self, [_jsonBuffer my_UTF8ToString]);
- [self cancelWithStatus: 502];
- return NO;
- }
- _document = [document copy];
- return YES;
-}
-
-
-- (TDBlobStoreWriter*)blobWriterForAttachment: (NSDictionary*)attachment {
- NSString* digest = [attachment objectForKey: @"digest"];
- return digest ? [_attachmentsByDigest objectForKey: digest] : nil;
-}
-
-
-- (BOOL) registerAttachments {
- NSDictionary* attachments = [_document objectForKey: @"_attachments"];
- if (![attachments isKindOfClass: [NSDictionary class]])
- return NO;
- NSUInteger nAttachmentsInDoc = 0;
- for (NSDictionary* attachment in attachments.allValues) {
- if ([[attachment objectForKey: @"follows"] isEqual: $true]) {
- // Check that each attachment in the JSON corresponds to an attachment MIME body:
- TDBlobStoreWriter* writer = [self blobWriterForAttachment: attachment];
- if (!writer)
- return NO;
- // Check that the length matches:
- NSNumber* lengthObj = [attachment objectForKey: @"encoded_length"]
- ?: [attachment objectForKey: @"length"];
- if (!lengthObj)
- return NO;
- if (writer.length != [$castIf(NSNumber, lengthObj) unsignedLongLongValue])
- return NO;
- ++nAttachmentsInDoc;
- }
- }
- if (nAttachmentsInDoc < _attachmentsByDigest.count)
- return NO; // Some MIME bodies didn't match attachments in the document
- // If everything's copacetic, hand over the (uninstalled) blobs to the database to remember:
- [_database rememberAttachmentWritersForDigests: _attachmentsByDigest];
- return YES;
+- (NSDictionary*) document {
+ return _reader.document;
}
@@ -123,7 +62,6 @@ - (BOOL) registerAttachments {
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
- Assert(_database, @"Didn't set database property");
[super connection: connection didReceiveResponse: response];
if (!_connection)
return;
@@ -131,53 +69,25 @@ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLRespon
// Check the content type to see whether it's a multipart response:
NSDictionary* headers = [(NSHTTPURLResponse*)response allHeaderFields];
NSString* contentType = [headers objectForKey: @"Content-Type"];
- if ([contentType hasPrefix: @"multipart/"]) {
- // Multipart, so initialize the parser:
- LogTo(SyncVerbose, @"%@: has attachments, %@", self, contentType);
- _multipartReader = [[TDMultipartReader alloc] initWithContentType: contentType delegate: self];
- if (!_multipartReader) {
- Warn(@"%@: received invalid content type '%@'", self, contentType);
- [self cancelWithStatus: 406];
- return;
- }
- _attachmentsByDigest = [[NSMutableDictionary alloc] init];
- } else {
- // No multipart, so no attachments. Body is pure JSON:
- _jsonBuffer = [[NSMutableData alloc] initWithCapacity: 1024];
- }
+ if ([contentType hasPrefix: @"text/plain"])
+ contentType = nil; // Workaround for CouchDB returning JSON docs with text/plain type
+ if (![_reader setContentType: contentType])
+ [self cancelWithStatus: _reader.status];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[super connection: connection didReceiveData: data];
- if (_multipartReader) {
- [_multipartReader appendData: data];
- if (_multipartReader.failed) {
- Warn(@"%@: received unparseable MIME multipart response", self);
- [self cancelWithStatus: 502];
- }
- } else {
- [_jsonBuffer appendData: data];
- }
+ if (![_reader appendData: data])
+ [self cancelWithStatus: _reader.status];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
- LogTo(SyncVerbose, @"%@: Finished loading (%u attachments)", self, _attachmentsByDigest.count);
- if (_multipartReader) {
- if (!_multipartReader.finished) {
- Warn(@"%@: received incomplete MIME multipart response", self);
- [self cancelWithStatus: 502];
- return;
- }
-
- if (![self registerAttachments]) {
- [self cancelWithStatus: 400];
- return;
- }
- } else {
- if (![self parseJSONBuffer])
- return;
+ LogTo(SyncVerbose, @"%@: Finished loading (%u attachments)", self, _reader.attachmentCount);
+ if (![_reader finish]) {
+ [self cancelWithStatus: _reader.status];
+ return;
}
[self clearConnection];
@@ -185,56 +95,6 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
}
-#pragma mark - MIME PARSER CALLBACKS:
-
-
-/** Callback: A part's headers have been parsed, but not yet its data. */
-- (void) startedPart: (NSDictionary*)headers {
- // First MIME part is the document's JSON body; the rest are attachments.
- if (!_document)
- _jsonBuffer = [[NSMutableData alloc] initWithCapacity: 1024];
- else {
- LogTo(SyncVerbose, @"%@: Starting attachment #%u...", self, _attachmentsByDigest.count + 1);
- _curAttachment = [[_database attachmentWriter] retain];
- }
-}
-
-
-/** Callback: Append data to a MIME part's body. */
-- (void) appendToPart: (NSData*)data {
- if (_jsonBuffer)
- [_jsonBuffer appendData: data];
- else
- [_curAttachment appendData: data];
-}
-
-
-/** Callback: A MIME part is complete. */
-- (void) finishedPart {
- if (_jsonBuffer) {
- [self parseJSONBuffer];
- } else {
- // Finished downloading an attachment. Remember the association from the MD5 digest
- // (which appears in the body's _attachments dict) to the blob-store key of the data.
- [_curAttachment finish];
- TDMD5Key md5 = _curAttachment.MD5Digest;
- NSString* md5Str = [@"md5-" stringByAppendingString: [TDBase64 encode: &md5
- length: sizeof(md5)]];
-#ifndef MY_DISABLE_LOGGING
- if (WillLogTo(SyncVerbose)) {
- TDBlobKey key = _curAttachment.blobKey;
- NSData* keyData = [NSData dataWithBytes: &key length: sizeof(key)];
- LogTo(SyncVerbose, @"%@: Finished attachment #%u: len=%uk, digest=%@, SHA1=%@",
- self, _attachmentsByDigest.count+1, (unsigned)_curAttachment.length/1024,
- md5Str, keyData);
- }
-#endif
- [_attachmentsByDigest setObject: _curAttachment forKey: md5Str];
- setObj(&_curAttachment, nil);
- }
-}
-
-
@end
@@ -257,7 +117,6 @@ - (void) finishedPart {
__block BOOL done = NO;
[[[TDMultipartDownloader alloc] initWithURL: url
database: db
- revision: nil
onCompletion: ^(id result, NSError * error)
{
CAssertNil(error);
@@ -267,7 +126,7 @@ - (void) finishedPart {
CAssert(attachments.count >= 1);
CAssertEq(db.attachmentStore.count, 0u);
for (NSDictionary* attachment in attachments.allValues) {
- TDBlobStoreWriter* writer = [request blobWriterForAttachment: attachment];
+ TDBlobStoreWriter* writer = [db attachmentWriterForAttachment: attachment];
CAssert(writer);
CAssert([writer install]);
NSData* blob = [db.attachmentStore blobForKey: writer.blobKey];
View
1  Source/TDPuller.m
@@ -284,7 +284,6 @@ - (void) pullRemoteRevision: (TDRevision*)rev
NSString* urlStr = [_remote.absoluteString stringByAppendingString: path];
[[[TDMultipartDownloader alloc] initWithURL: [NSURL URLWithString: urlStr]
database: _db
- revision: rev
onCompletion:
^(TDMultipartDownloader* download, NSError *error) {
// OK, now we've got the response revision:
View
5 Source/TDPusher.m
@@ -236,7 +236,8 @@ - (BOOL) uploadMultipartRevision: (TDRevision*)rev {
// Find all the attachments with "follows" instead of a body, and put 'em in a multipart stream:
TDMultipartWriter* bodyStream = nil;
NSDictionary* attachments = [rev.properties objectForKey: @"_attachments"];
- for (NSDictionary* attachment in attachments.allValues) {
+ for (NSString* attachmentName in attachments) {
+ NSDictionary* attachment = [attachments objectForKey: attachmentName];
if ([attachment objectForKey: @"follows"]) {
if (!bodyStream) {
// Create the HTTP multipart stream:
@@ -247,6 +248,8 @@ - (BOOL) uploadMultipartRevision: (TDRevision*)rev {
}
UInt64 length;
NSInputStream *stream = [_db inputStreamForAttachmentDict: attachment length: &length];
+ NSString* disposition = $sprintf(@"attachment; filename=%@", TDQuoteString(attachmentName));
+ [bodyStream setNextPartsHeaders: $dict({@"Content-Disposition", disposition})];
[bodyStream addStream: stream length: length];
}
}
View
65 Source/TDRouter+Handlers.m
@@ -21,6 +21,7 @@
#import "TDDatabase+Replication.h"
#import "TDView.h"
#import "TDBody.h"
+#import "TDMultipartDocumentReader.h"
#import "TDRevision.h"
#import "TDServer.h"
#import "TDReplicator.h"
@@ -30,20 +31,6 @@
#import "TDMisc.h"
-@interface TDRouter (Handlers_Internal)
-- (TDStatus) update: (TDDatabase*)db
- docID: (NSString*)docID
- body: (TDBody*)body
- deleting: (BOOL)deleting
- allowConflict: (BOOL)allowConflict
- createdRev: (TDRevision**)outRev;
-- (TDStatus) update: (TDDatabase*)db
- docID: (NSString*)docID
- json: (NSData*)json
- deleting: (BOOL)deleting;
-@end
-
-
@implementation TDRouter (Handlers)
@@ -226,14 +213,6 @@ - (TDStatus) do_DELETE: (TDDatabase*)db {
}
-- (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])
@@ -666,6 +645,9 @@ - (TDStatus) update: (TDDatabase*)db
allowConflict: (BOOL)allowConflict
createdRev: (TDRevision**)outRev
{
+ if (body && !body.isValidJSON)
+ return 400;
+
NSString* prevRevID;
if (!deleting) {
@@ -706,10 +688,9 @@ - (TDStatus) update: (TDDatabase*)db
- (TDStatus) update: (TDDatabase*)db
docID: (NSString*)docID
- json: (NSData*)json
+ body: (TDBody*)body
deleting: (BOOL)deleting
{
- TDBody* body = json ? [TDBody bodyWithJSON: json] : nil;
TDRevision* rev;
TDStatus status = [self update: db docID: docID body: body
deleting: deleting
@@ -730,17 +711,39 @@ - (TDStatus) update: (TDDatabase*)db
return status;
}
+
+- (TDBody*) documentBodyFromRequest: (TDStatus*)outStatus {
+ NSString* contentType = [_request valueForHTTPHeaderField: @"Content-Type"];
+ NSDictionary* properties = [TDMultipartDocumentReader readData: _request.HTTPBody
+ ofType: contentType
+ toDatabase: _db
+ status: outStatus];
+ return properties ? [TDBody bodyWithProperties: properties] : nil;
+}
+
+
+- (TDStatus) do_POST: (TDDatabase*)db {
+ TDStatus status = [self openDB];
+ if (status >= 300)
+ return status;
+ TDBody* body = [self documentBodyFromRequest: &status];
+ if (!body)
+ return status;
+ return [self update: db docID: nil body: body deleting: NO];
+}
+
+
- (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID {
- NSData* json = _request.HTTPBody;
- if (!json)
- return 400;
+ TDStatus status;
+ TDBody* body = [self documentBodyFromRequest: &status];
+ if (!body)
+ return status;
if (![self query: @"new_edits"] || [self boolQuery: @"new_edits"]) {
// Regular PUT:
- return [self update: db docID: docID json: json deleting: NO];
+ return [self update: db docID: docID body: body 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;
@@ -751,7 +754,7 @@ - (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID {
- (TDStatus) do_DELETE: (TDDatabase*)db docID: (NSString*)docID {
- return [self update: db docID: docID json: nil deleting: YES];
+ return [self update: db docID: docID body: nil deleting: YES];
}
@@ -888,6 +891,8 @@ - (TDStatus) do_POST_temp_view: (TDDatabase*)db {
if (![[_request valueForHTTPHeaderField: @"Content-Type"] hasPrefix: @"application/json"])
return 415;
TDBody* requestBody = [TDBody bodyWithJSON: _request.HTTPBody];
+ if (!requestBody.isValidJSON)
+ return 400;
NSDictionary* props = requestBody.properties;
if (!props)
return 400;
View
41 Source/TDRouter_Tests.m
@@ -41,9 +41,13 @@
for (NSString* header in headers)
[request setValue: [headers objectForKey: header] forHTTPHeaderField: header];
if (bodyObj) {
- NSError* error = nil;
- request.HTTPBody = [TDJSON dataWithJSONObject: bodyObj options:0 error:&error];
- CAssertNil(error);
+ if ([bodyObj isKindOfClass: [NSData class]])
+ request.HTTPBody = bodyObj;
+ else {
+ NSError* error = nil;
+ request.HTTPBody = [TDJSON dataWithJSONObject: bodyObj options:0 error:&error];
+ CAssertNil(error);
+ }
}
TDRouter* router = [[[TDRouter alloc] initWithDatabaseManager: server request: request] autorelease];
CAssert(router!=nil);
@@ -468,6 +472,37 @@ static id Send(TDDatabaseManager* server, NSString* method, NSString* path,
}
+TestCase(TDRouter_PutMultipart) {
+ RequireTestCase(TDRouter_Docs);
+ RequireTestCase(TDMultipartDownloader);
+ TDDatabaseManager* server = createDBManager();
+ Send(server, @"PUT", @"/db", 201, nil);
+
+ NSDictionary* attachmentDict = $dict({@"attach", $dict({@"content_type", @"text/plain"},
+ {@"length", $object(36)},
+ {@"content_type", @"text/plain"},
+ {@"follows", $true})});
+ NSDictionary* props = $dict({@"message", @"hello"},
+ {@"_attachments", attachmentDict});
+ NSString* attachmentString = @"This is the value of the attachment.";
+
+ NSString* body = $sprintf(@"\r\n--BOUNDARY\r\n\r\n"
+ "%@"
+ "\r\n--BOUNDARY\r\n"
+ "Content-ID: attach\r\n"
+ "Content-Type: text/plain\r\n\r\n"
+ "%@"
+ "\r\n--BOUNDARY--",
+ [TDJSON stringWithJSONObject: props options: 0 error: nil],
+ attachmentString);
+
+ TDResponse* response = SendRequest(server, @"PUT", @"/db/doc",
+ $dict({@"Content-Type", @"multipart/related; boundary=\"BOUNDARY\""}),
+ [body dataUsingEncoding: NSUTF8StringEncoding]);
+ CAssertEq(response.status, 201);
+}
+
+
TestCase(TDRouter_OpenRevs) {
RequireTestCase(TDRouter_Databases);
// PUT:
View
10 TouchDB.xcodeproj/project.pbxproj
@@ -182,6 +182,9 @@
27AA409E14AA86AE00E2A5FF /* TDDatabase+Insertion.m in Sources */ = {isa = PBXBuildFile; fileRef = 27AA409A14AA86AD00E2A5FF /* TDDatabase+Insertion.m */; };
27AA40A314AA8A6600E2A5FF /* TDDatabase+Replication.m in Sources */ = {isa = PBXBuildFile; fileRef = 27AA40A014AA8A6600E2A5FF /* TDDatabase+Replication.m */; };
27AA40A414AA8A6600E2A5FF /* TDDatabase+Replication.m in Sources */ = {isa = PBXBuildFile; fileRef = 27AA40A014AA8A6600E2A5FF /* TDDatabase+Replication.m */; };
+ 27ADC079152502EE001ABC1D /* TDMultipartDocumentReader.h in Headers */ = {isa = PBXBuildFile; fileRef = 27ADC077152502EE001ABC1D /* TDMultipartDocumentReader.h */; };
+ 27ADC07A152502EE001ABC1D /* TDMultipartDocumentReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 27ADC078152502EE001ABC1D /* TDMultipartDocumentReader.m */; };
+ 27ADC07B152502EE001ABC1D /* TDMultipartDocumentReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 27ADC078152502EE001ABC1D /* TDMultipartDocumentReader.m */; };
27B0B7801491E76200A817AD /* TDView_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B77F1491E73400A817AD /* TDView_Tests.m */; };
27B0B796149290AB00A817AD /* TDChangeTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 27B0B790149290AB00A817AD /* TDChangeTracker.h */; };
27B0B797149290AB00A817AD /* TDChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B791149290AB00A817AD /* TDChangeTracker.m */; };
@@ -511,6 +514,8 @@
27A82E3514A1145000C0B850 /* FMDatabaseAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseAdditions.m; sourceTree = "<group>"; };
27AA409A14AA86AD00E2A5FF /* TDDatabase+Insertion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TDDatabase+Insertion.m"; sourceTree = "<group>"; };
27AA40A014AA8A6600E2A5FF /* TDDatabase+Replication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TDDatabase+Replication.m"; sourceTree = "<group>"; };
+ 27ADC077152502EE001ABC1D /* TDMultipartDocumentReader.h */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.h; path = TDMultipartDocumentReader.h; sourceTree = "<group>"; tabWidth = 4; usesTabs = 0; wrapsLines = 1; };
+ 27ADC078152502EE001ABC1D /* TDMultipartDocumentReader.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = TDMultipartDocumentReader.m; sourceTree = "<group>"; tabWidth = 4; usesTabs = 0; wrapsLines = 1; };
27B0B77F1491E73400A817AD /* TDView_Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TDView_Tests.m; sourceTree = "<group>"; };
27B0B790149290AB00A817AD /* TDChangeTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TDChangeTracker.h; sourceTree = "<group>"; };
27B0B791149290AB00A817AD /* TDChangeTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TDChangeTracker.m; sourceTree = "<group>"; };
@@ -954,6 +959,8 @@
270B3E2A1489581E00E0A926 /* TDPuller.m */,
270B3E3C148D7F0000E0A926 /* TDPusher.h */,
270B3E3D148D7F0000E0A926 /* TDPusher.m */,
+ 27ADC077152502EE001ABC1D /* TDMultipartDocumentReader.h */,
+ 27ADC078152502EE001ABC1D /* TDMultipartDocumentReader.m */,
279CE40814D8AA23009F3FA6 /* TDMultipartDownloader.h */,
279CE40914D8AA23009F3FA6 /* TDMultipartDownloader.m */,
27C5305214DF3A050078F886 /* TDMultipartUploader.h */,
@@ -1226,6 +1233,7 @@
279C7E2E14F424090004A1E8 /* TDSequenceMap.h in Headers */,
2751D4E4151BAE7000F7FD57 /* TDDatabaseManager.h in Headers */,
272B85141523691700A90CB2 /* TDJSON.h in Headers */,
+ 27ADC079152502EE001ABC1D /* TDMultipartDocumentReader.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1748,6 +1756,7 @@
279C7E2F14F424090004A1E8 /* TDSequenceMap.m in Sources */,
2751D4E5151BAE7000F7FD57 /* TDDatabaseManager.m in Sources */,
272B85151523691700A90CB2 /* TDJSON.m in Sources */,
+ 27ADC07A152502EE001ABC1D /* TDMultipartDocumentReader.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1851,6 +1860,7 @@
279C7E3014F424090004A1E8 /* TDSequenceMap.m in Sources */,
2751D4E6151BAE7000F7FD57 /* TDDatabaseManager.m in Sources */,
272B85161523691700A90CB2 /* TDJSON.m in Sources */,
+ 27ADC07B152502EE001ABC1D /* TDMultipartDocumentReader.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

No commit comments for this range

Something went wrong with that request. Please try again.