Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Support for direct PUT and DELETE of attachments

Now passes most of the attachments.js test suite.
  • Loading branch information...
commit 0928fe1e0672540307cb85a037f4c73cc03e1140 1 parent d88fb60
@snej snej authored
View
78 Source/TDDatabase+Attachments.m
@@ -17,8 +17,9 @@
*/
#import "TDDatabase.h"
-#import "TDBlobStore.h"
#import "TDBase64.h"
+#import "TDBlobStore.h"
+#import "TDBody.h"
#import "TDInternal.h"
#import "FMDatabase.h"
@@ -212,6 +213,81 @@ - (TDStatus) processAttachmentsForRevision: (TDRevision*)rev
}
+- (TDRevision*) updateAttachment: (NSString*)filename
+ body: (NSData*)body
+ type: (NSString*)contentType
+ ofDocID: (NSString*)docID
+ revID: (NSString*)oldRevID
+ status: (TDStatus*)outStatus
+{
+ *outStatus = 400;
+ if (filename.length == 0 || (body && !contentType) || (oldRevID && !docID) || (body && !docID))
+ return nil;
+
+ [self beginTransaction];
+ @try {
+ TDRevision* oldRev = [[TDRevision alloc] initWithDocID: docID revID: oldRevID deleted: NO];
+ if (oldRevID) {
+ // Load existing revision if this is a replacement:
+ *outStatus = [self loadRevisionBody: oldRev options: 0];
+ if (*outStatus >= 300) {
+ if (*outStatus == 404 && [self existsDocumentWithID: docID revisionID: nil])
+ *outStatus = 409; // if some other revision exists, it's a conflict
+ return nil;
+ }
+ NSDictionary* attachments = [oldRev.properties objectForKey: @"_attachments"];
+ if (!body && ![attachments objectForKey: filename]) {
+ *outStatus = 404;
+ return nil;
+ }
+ // Remove the _attachments stubs so putRevision: doesn't copy the rows for me
+ // OPT: Would be better if I could tell loadRevisionBody: not to add it
+ if (attachments) {
+ NSMutableDictionary* properties = [oldRev.properties mutableCopy];
+ [properties removeObjectForKey: @"_attachments"];
+ oldRev.body = [TDBody bodyWithProperties: properties];
+ [properties release];
+ }
+ } else {
+ // If this creates a new doc, it needs a body:
+ oldRev.body = [TDBody bodyWithProperties: $dict()];
+ }
+
+ // Create a new revision:
+ TDRevision* newRev = [self putRevision: oldRev prevRevisionID: oldRevID status: outStatus];
+ if (!newRev)
+ return nil;
+
+ if (oldRevID) {
+ // Copy all attachment rows _except_ for the one being updated:
+ if (![_fmdb executeUpdate: @"INSERT INTO attachments "
+ "(sequence, filename, key, type, length, revpos) "
+ "SELECT ?, filename, key, type, length, revpos FROM attachments "
+ "WHERE sequence=? AND filename != ?",
+ $object(newRev.sequence), $object(oldRev.sequence),
+ filename]) {
+ *outStatus = 500;
+ return nil;
+ }
+ }
+
+ if (body) {
+ // If not deleting, add a new attachment entry:
+ *outStatus = [self insertAttachment: body forSequence: newRev.sequence
+ named: filename type: contentType
+ revpos: newRev.generation];
+ if (*outStatus >= 300)
+ return nil;
+ }
+
+ *outStatus = body ? 201 : 200;
+ return newRev;
+ } @finally {
+ [self endTransaction: (*outStatus < 300)];
+ }
+}
+
+
- (TDStatus) garbageCollectAttachments {
// First delete attachment rows for already-cleared revisions:
// OPT: Could start after last sequence# we GC'd up to
View
8 Source/TDDatabase.h
@@ -199,6 +199,14 @@ extern const TDChangesOptions kDefaultTDChangesOptions;
/** Deletes obsolete attachments from the database and blob store. */
- (TDStatus) garbageCollectAttachments;
+/** Updates or deletes an attachment, creating a new document revision in the process.
+ Used by the PUT / DELETE methods called on attachment URLs. */
+- (TDRevision*) updateAttachment: (NSString*)filename
+ body: (NSData*)body
+ type: (NSString*)contentType
+ ofDocID: (NSString*)docID
+ revID: (NSString*)oldRevID
+ status: (TDStatus*)outStatus;
@end
View
50 Source/TDDatabase_Tests.m
@@ -304,6 +304,7 @@ static void verifyHistory(TDDatabase* db, TDRevision* rev, NSArray* history) {
// Start with a fresh database in /tmp:
TDDatabase* db = createDB();
+ // Put a revision that includes an _attachments dict:
NSData* attach1 = [@"This is the body of attach1" dataUsingEncoding: NSUTF8StringEncoding];
NSString* base64 = [TDBase64 encode: attach1];
NSDictionary* attachmentDict = $dict({@"attach", $dict({@"content_type", @"text/plain"},
@@ -320,6 +321,7 @@ static void verifyHistory(TDDatabase* db, TDRevision* rev, NSArray* history) {
// Examine the attachment store:
CAssertEq(db.attachmentStore.count, 1u);
+ // Get the revision:
TDRevision* gotRev1 = [db getDocumentWithID: rev1.docID revisionID: rev1.revID
options: 0];
attachmentDict = [gotRev1.properties objectForKey: @"_attachments"];
@@ -328,6 +330,54 @@ static void verifyHistory(TDDatabase* db, TDRevision* rev, NSArray* history) {
{@"length", $object(27)},
{@"stub", $true},
{@"revpos", $object(1)})}));
+
+ // Update the attachment directly:
+ NSData* attachv2 = [@"Replaced body of attach" dataUsingEncoding: NSUTF8StringEncoding];
+ [db updateAttachment: @"attach" body: attachv2 type: @"application/foo"
+ ofDocID: rev1.docID revID: nil
+ status: &status];
+ CAssertEq(status, 409);
+ [db updateAttachment: @"attach" body: attachv2 type: @"application/foo"
+ ofDocID: rev1.docID revID: @"1-bogus"
+ status: &status];
+ CAssertEq(status, 409);
+ TDRevision* rev2 = [db updateAttachment: @"attach" body: attachv2 type: @"application/foo"
+ ofDocID: rev1.docID revID: rev1.revID
+ status: &status];
+ CAssertEq(status, 201);
+ CAssertEqual(rev2.docID, rev1.docID);
+ CAssertEq(rev2.generation, 2u);
+
+ // Get the updated revision:
+ TDRevision* gotRev2 = [db getDocumentWithID: rev2.docID revisionID: rev2.revID
+ options: 0];
+ attachmentDict = [gotRev2.properties objectForKey: @"_attachments"];
+ CAssertEqual(attachmentDict, $dict({@"attach", $dict({@"content_type", @"application/foo"},
+ {@"digest", @"sha1-mbT3208HI3PZgbG4zYWbDW2HsPk="},
+ {@"length", $object(23)},
+ {@"stub", $true},
+ {@"revpos", $object(2)})}));
+
+ // Delete the attachment:
+ [db updateAttachment: @"nosuchattach" body: nil type: nil
+ ofDocID: rev2.docID revID: rev2.revID
+ status: &status];
+ CAssertEq(status, 404);
+ [db updateAttachment: @"nosuchattach" body: nil type: nil
+ ofDocID: @"nosuchdoc" revID: @"nosuchrev"
+ status: &status];
+ CAssertEq(status, 404);
+ TDRevision* rev3 = [db updateAttachment: @"attach" body: nil type: nil
+ ofDocID: rev2.docID revID: rev2.revID
+ status: &status];
+ CAssertEq(status, 200);
+ CAssertEqual(rev2.docID, rev1.docID);
+ CAssertEq(rev2.generation, 2u);
+
+ // Get the updated revision:
+ TDRevision* gotRev3 = [db getDocumentWithID: rev3.docID revisionID: rev3.revID
+ options: 0];
+ CAssertNil([gotRev3.properties objectForKey: @"_attachments"]);
}
View
84 Source/TDRouter+Handlers.m
@@ -32,6 +32,25 @@ - (TDStatus) update: (TDDatabase*)db
@implementation TDRouter (Handlers)
+- (void) setResponseLocation: (NSURL*)url {
+ // Strip anything after the URL's path (i.e. the query string)
+ CFURLRef cfURL = (CFURLRef)url;
+ CFRange range = CFURLGetByteRangeForComponent(cfURL, kCFURLComponentResourceSpecifier, NULL);
+ if (range.length == 0) {
+ [_response setValue: url.absoluteString ofHeader: @"Location"];
+ } else {
+ CFIndex size = CFURLGetBytes(cfURL, NULL, 0);
+ if (size > 8000)
+ return;
+ UInt8 bytes[size];
+ CFURLGetBytes(cfURL, bytes, size);
+ cfURL = CFURLCreateWithBytes(NULL, bytes, range.location - 1, kCFStringEncodingUTF8, NULL);
+ [_response setValue: (id)CFURLGetString(cfURL) ofHeader: @"Location"];
+ CFRelease(cfURL);
+ }
+}
+
+
#pragma mark - SERVER REQUESTS:
@@ -168,7 +187,7 @@ - (TDStatus) do_PUT: (TDDatabase*)db {
return 412;
if (![db open])
return 500;
- [_response setValue: _request.URL.absoluteString ofHeader: @"Location"];
+ [self setResponseLocation: _request.URL];
return 201;
}
@@ -284,7 +303,8 @@ - (TDStatus) do_POST_bulk_docs: (TDDatabase*)db {
- (TDStatus) do_POST_compact: (TDDatabase*)db {
- return [db compact];
+ TDStatus status = [db compact];
+ return status<300 ? 202 : status; // CouchDB returns 202 'cause it's an async operation
}
- (TDStatus) do_POST_ensure_full_commit: (TDDatabase*)db {
@@ -420,6 +440,18 @@ - (TDStatus) do_GET_changes: (TDDatabase*)db {
#pragma mark - DOCUMENT REQUESTS:
+- (NSString*) revIDFromIfMatchHeader {
+ NSString* ifMatch = [_request valueForHTTPHeaderField: @"If-Match"];
+ if (!ifMatch)
+ return nil;
+ // Value of If-Match is an ETag, so have to trim the quotes around it:
+ if (ifMatch.length > 2 && [ifMatch hasPrefix: @"\""] && [ifMatch hasSuffix: @"\""])
+ return [ifMatch substringWithRange: NSMakeRange(1, ifMatch.length-2)];
+ else
+ return nil;
+}
+
+
- (NSString*) setResponseEtag: (TDRevision*)rev {
NSString* eTag = $sprintf(@"\"%@\"", rev.revID);
[_response setValue: eTag ofHeader: @"Etag"];
@@ -446,7 +478,7 @@ - (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID {
- (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?
+ //OPT: This gets the JSON body too, which is a waste. Could add a kNoBody option?
TDRevision* rev = [db getDocumentWithID: docID
revisionID: [self query: @"rev"] // often nil
options: 0];
@@ -473,18 +505,6 @@ - (TDStatus) do_GET: (TDDatabase*)db docID: (NSString*)docID attachment: (NSStri
}
-- (NSString*) revIDFromIfMatchHeader {
- NSString* ifMatch = [_request valueForHTTPHeaderField: @"If-Match"];
- if (!ifMatch)
- return nil;
- // Value of If-Match is an ETag, so have to trim the quotes around it:
- if (ifMatch.length > 2 && [ifMatch hasPrefix: @"\""] && [ifMatch hasSuffix: @"\""])
- return [ifMatch substringWithRange: NSMakeRange(1, ifMatch.length-2)];
- else
- return nil;
-}
-
-
- (TDStatus) update: (TDDatabase*)db
docID: (NSString*)docID
body: (TDBody*)body
@@ -547,7 +567,7 @@ - (TDStatus) update: (TDDatabase*)db
NSURL* url = _request.URL;
if (!docID)
url = [url URLByAppendingPathComponent: rev.docID];
- [_response.headers setObject: url.absoluteString forKey: @"Location"];
+ [self setResponseLocation: url];
}
_response.bodyObject = $dict({@"ok", $true},
{@"id", rev.docID},
@@ -581,6 +601,38 @@ - (TDStatus) do_DELETE: (TDDatabase*)db docID: (NSString*)docID {
}
+- (TDStatus) updateAttachment: (NSString*)attachment docID: (NSString*)docID body: (NSData*)body {
+ TDStatus status;
+ TDRevision* rev = [_db updateAttachment: attachment
+ body: body
+ type: [_request valueForHTTPHeaderField: @"Content-Type"]
+ ofDocID: docID
+ revID: ([self query: @"rev"] ?: [self revIDFromIfMatchHeader])
+ status: &status];
+ if (status < 300) {
+ _response.bodyObject = $dict({@"ok", $true}, {@"id", rev.docID}, {@"rev", rev.revID});
+ [self setResponseEtag: rev];
+ if (body)
+ [self setResponseLocation: _request.URL];
+ }
+ return status;
+}
+
+
+- (TDStatus) do_PUT: (TDDatabase*)db docID: (NSString*)docID attachment: (NSString*)attachment {
+ return [self updateAttachment: attachment
+ docID: docID
+ body: (_request.HTTPBody ?: [NSData data])];
+}
+
+
+- (TDStatus) do_DELETE: (TDDatabase*)db docID: (NSString*)docID attachment: (NSString*)attachment {
+ return [self updateAttachment: attachment
+ docID: docID
+ body: nil];
+}
+
+
#pragma mark - VIEW QUERIES:
Please sign in to comment.
Something went wrong with that request. Please try again.