diff --git a/WordPressKit/MediaServiceRemoteREST.m b/WordPressKit/MediaServiceRemoteREST.m index 030065e0e..95bd0b996 100644 --- a/WordPressKit/MediaServiceRemoteREST.m +++ b/WordPressKit/MediaServiceRemoteREST.m @@ -131,22 +131,24 @@ - (void)uploadMedia:(NSArray *)mediaItems NSString *apiPath = [NSString stringWithFormat:@"sites/%@/media/new", self.siteID]; NSString *requestUrl = [self pathForEndpoint:apiPath withVersion:ServiceRemoteWordPressComRESTApiVersion_1_1]; - NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:@{}]; - NSMutableArray *fileParts = [NSMutableArray array]; + + NSMutableArray *bodyParts = [NSMutableArray array]; for (RemoteMedia *remoteMedia in mediaItems) { NSString *type = remoteMedia.mimeType; NSString *filename = remoteMedia.file; - if (remoteMedia.postID != nil && [remoteMedia.postID compare:@(0)] == NSOrderedDescending) { - parameters[@"attrs[0][parent_id]"] = remoteMedia.postID; + NSNumber* postID = remoteMedia.postID; + if (postID != nil && [postID compare:@(0)] == NSOrderedDescending) { + BodyPart *parentIDPart = [[BodyPart alloc] initWithName:@"attrs[0][parent_id]" data:[NSData dataWithBytes:&postID length:sizeof(postID)]]; + [bodyParts addObject: parentIDPart]; } - FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:remoteMedia.localURL filename:filename mimeType:type]; - [fileParts addObject:filePart]; + BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:remoteMedia.localURL fileName:filename mimeType:type]; + [bodyParts addObject:mediaPart]; } [self.wordPressComRestApi multipartPOST:requestUrl - parameters:parameters - fileParts:fileParts + parameters: nil + bodyParts:bodyParts requestEnqueued:^(NSNumber *taskID) { if (requestEnqueued) { requestEnqueued(taskID); @@ -193,8 +195,6 @@ - (void)uploadMedia:(RemoteMedia *)media NSString *requestUrl = [self pathForEndpoint:apiPath withVersion:ServiceRemoteWordPressComRESTApiVersion_1_1]; - NSDictionary *parameters = [self parametersForUploadMedia:media]; - if (media.localURL == nil || filename == nil || type == nil) { if (failure) { NSError *error = [NSError errorWithDomain:NSURLErrorDomain @@ -204,10 +204,11 @@ - (void)uploadMedia:(RemoteMedia *)media } return; } - FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL filename:filename mimeType:type]; + + NSArray *bodyParts = [self bodyPartsForUploadMedia: media]; __block NSProgress *localProgress = [self.wordPressComRestApi multipartPOST:requestUrl - parameters:parameters - fileParts:@[filePart] + parameters:nil + bodyParts:bodyParts requestEnqueued:nil success:^(id _Nonnull responseObject, NSHTTPURLResponse * _Nullable httpResponse) { NSDictionary *response = (NSDictionary *)responseObject; @@ -402,18 +403,25 @@ - (NSDictionary *)parametersFromRemoteMedia:(RemoteMedia *)remoteMedia return [NSDictionary dictionaryWithDictionary:parameters]; } -- (NSDictionary *)parametersForUploadMedia:(RemoteMedia *)media +- (NSArray *)bodyPartsForUploadMedia:(RemoteMedia *)media { - NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; - - if (media.caption != nil) { - parameters[@"attrs[0][caption]"] = media.caption; + NSMutableArray *bodyParts = [NSMutableArray array]; + NSString *caption = media.caption; + NSNumber *postID = media.postID; + + if (caption != nil) { + BodyPart *captionPart = [[BodyPart alloc] initWithName:@"attrs[0][caption]" data:[caption dataUsingEncoding:NSUTF8StringEncoding]]; + [bodyParts addObject: captionPart]; } - if (media.postID != nil && [media.postID compare:@(0)] == NSOrderedDescending) { - parameters[@"attrs[0][parent_id]"] = media.postID; + if (postID != nil && [postID compare:@(0)] == NSOrderedDescending) { + BodyPart *parentIDPart = [[BodyPart alloc] initWithName:@"attrs[0][parent_id]" data:[NSData dataWithBytes:&postID length:sizeof(postID)]]; + [bodyParts addObject: parentIDPart]; } - return [NSDictionary dictionaryWithDictionary:parameters]; + BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:media.localURL fileName:media.file mimeType:media.mimeType]; + [bodyParts addObject: mediaPart]; + + return bodyParts; } @end diff --git a/WordPressKit/PostServiceRemoteREST.m b/WordPressKit/PostServiceRemoteREST.m index 2e8bf1e62..c7910426a 100644 --- a/WordPressKit/PostServiceRemoteREST.m +++ b/WordPressKit/PostServiceRemoteREST.m @@ -158,14 +158,15 @@ - (void)createPost:(RemotePost *)post NSString *requestUrl = [self pathForEndpoint:path withVersion:ServiceRemoteWordPressComRESTApiVersion_1_2]; - NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:@{}]; - parameters[@"content"] = post.content; - parameters[@"title"] = post.title; - parameters[@"status"] = post.status; - FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL filename:filename mimeType:type]; + + BodyPart *contentPart =[[BodyPart alloc] initWithName:@"content" data:[post.content dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *titlePart = [[BodyPart alloc] initWithName:@"title" data:[post.title dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *statusPart = [[BodyPart alloc] initWithName:@"status" data:[post.status dataUsingEncoding:NSUTF8StringEncoding]]; + BodyPart *mediaPart = [[BodyPart alloc] initWithName:@"media[]" url:media.localURL fileName:filename mimeType:type]; + [self.wordPressComRestApi multipartPOST:requestUrl - parameters:parameters - fileParts:@[filePart] + parameters:nil + bodyParts:@[contentPart, titlePart, statusPart, mediaPart] requestEnqueued:^(NSNumber *taskID) { if (requestEnqueued) { requestEnqueued(taskID); diff --git a/WordPressKit/WordPressComRestApi.swift b/WordPressKit/WordPressComRestApi.swift index 402a9627f..09bf9023d 100644 --- a/WordPressKit/WordPressComRestApi.swift +++ b/WordPressKit/WordPressComRestApi.swift @@ -290,12 +290,12 @@ open class WordPressComRestApi: NSObject { } /** - Executes a multipart POST using the current serializer, the parameters defined and the fileParts defined in the request + Executes a multipart POST to the specified endpoint defined on URLString, including the parameters and bodyParts defined in the request. This request will be streamed from disk, so it's ideally to be used for large media post uploads. - parameter URLString: the endpoint to connect - parameter parameters: the parameters to use on the request - - parameter fileParts: the file parameters that are added to the multipart request + - parameter bodyParts: the body parameters that are added to the multipart request - parameter requestEnqueued: callback to be called when the fileparts are serialized and request is added to the background session. Defaults to nil - parameter success: callback to be called on successful request - parameter failure: callback to be called on failed request @@ -306,7 +306,7 @@ open class WordPressComRestApi: NSObject { */ @objc @discardableResult open func multipartPOST(_ URLString: String, parameters: [String: AnyObject]?, - fileParts: [FilePart], + bodyParts: [BodyPart], requestEnqueued: RequestEnqueuedBlock? = nil, success: @escaping SuccessResponseBlock, failure: @escaping FailureReponseBlock) -> Progress? { @@ -324,9 +324,10 @@ open class WordPressComRestApi: NSObject { } uploadSessionManager.upload(multipartFormData: { (multipartFormData) in - for filePart in fileParts { - multipartFormData.append(filePart.url, withName: filePart.parameterName, fileName: filePart.filename, mimeType: filePart.mimeType) + for part in bodyParts { + part.appendToFormData(multipartFormData) } + }, to: URLString, encodingCompletion: { (encodingResult) in switch encodingResult { case .success(let upload, _, _): @@ -423,21 +424,64 @@ open class WordPressComRestApi: NSObject { } } -// MARK: - FilePart +// MARK: - BodyPart -/// FilePart represents the infomartion needed to encode a file on a multipart form request -public final class FilePart: NSObject { - @objc let parameterName: String - @objc let url: URL - @objc let filename: String - @objc let mimeType: String - - @objc public init(parameterName: String, url: URL, filename: String, mimeType: String) { - self.parameterName = parameterName +/// BodyPart represents the information needed to encode a part on a multipart form request +public final class BodyPart: NSObject { + @objc let name: String + @objc let data: Data? + @objc let url: URL? + @objc let fileName: String? + @objc let mimeType: String? + + @objc public init(name: String, data: Data) { + self.name = name + self.data = data + self.url = nil + self.fileName = nil + self.mimeType = nil + } + + @objc public init(name: String, url: URL) { + self.name = name self.url = url - self.filename = filename + self.data = nil + self.fileName = nil + self.mimeType = nil + } + + @objc public init(name: String, url: URL, fileName: String?, mimeType: String?) { + self.name = name + self.url = url + self.data = nil + self.fileName = fileName + self.mimeType = mimeType + } + + @objc public init(name: String, data: Data, fileName: String?, mimeType: String?) { + self.name = name + self.data = data + self.url = nil + self.fileName = fileName self.mimeType = mimeType } + + public func appendToFormData(_ multipartFormData: MultipartFormData) { + if let url = self.url { + if let fileName = self.fileName, let mimeType = self.mimeType { + multipartFormData.append(url, withName: self.name, fileName: fileName, mimeType: mimeType) + } else { + multipartFormData.append(url, withName: self.name) + } + } + else if let data = self.data { + if let fileName = self.fileName, let mimeType = self.mimeType { + multipartFormData.append(data, withName: self.name, fileName: fileName, mimeType: mimeType) + } else { + multipartFormData.append(data, withName: self.name) + } + } + } } // MARK: - Error processing diff --git a/WordPressKitTests/MockWordPressComRestApi.swift b/WordPressKitTests/MockWordPressComRestApi.swift index 3c783d399..8f6514fde 100644 --- a/WordPressKitTests/MockWordPressComRestApi.swift +++ b/WordPressKitTests/MockWordPressComRestApi.swift @@ -31,7 +31,7 @@ class MockWordPressComRestApi: WordPressComRestApi { override func multipartPOST(_ URLString: String, parameters: [String : AnyObject]?, - fileParts: [FilePart], + bodyParts: [BodyPart], requestEnqueued: RequestEnqueuedBlock? = nil, success: @escaping SuccessResponseBlock, failure: @escaping FailureReponseBlock) -> Progress? { diff --git a/WordPressKitTests/WordPressComRestApiTests.swift b/WordPressKitTests/WordPressComRestApiTests.swift index 486e7402c..1f1fd0612 100644 --- a/WordPressKitTests/WordPressComRestApiTests.swift +++ b/WordPressKitTests/WordPressComRestApiTests.swift @@ -187,7 +187,7 @@ class WordPressComRestApiTests: XCTestCase { } let expect = self.expectation(description: "One callback should be invoked") let api = WordPressComRestApi(oAuthToken: "fakeToken") - api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in expect.fulfill() XCTFail("This call should fail") }, failure: { (error, httpResponse) in @@ -206,8 +206,8 @@ class WordPressComRestApiTests: XCTestCase { let expect = self.expectation(description: "One callback should be invoked") let api = WordPressComRestApi(oAuthToken: "fakeToken") - let filePart = FilePart(parameterName: "file", url: URL(fileURLWithPath: "/a.txt") as URL, filename: "a.txt", mimeType: "image/jpeg") - api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + let filePart = BodyPart(name: "file", url: URL(fileURLWithPath: "/a.txt") as URL, fileName: "a.txt", mimeType: "image/jpeg") + api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in expect.fulfill() XCTFail("This call should fail") }, failure: { (error, httpResponse) in @@ -230,8 +230,8 @@ class WordPressComRestApiTests: XCTestCase { let mediaURL = URL(fileURLWithPath: mediaPath) let expect = self.expectation(description: "One callback should be invoked") let api = WordPressComRestApi(oAuthToken: "fakeToken") - let filePart = FilePart(parameterName: "media[]", url: mediaURL as URL, filename: "test-image.jpg", mimeType: "image/jpeg") - let progress1 = api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + let mediaPart = BodyPart(name: "media[]", url: mediaURL as URL, fileName: "test-image.jpg", mimeType: "image/jpeg") + let progress1 = api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [mediaPart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in XCTFail("This call should fail") }, failure: { (error, httpResponse) in print(error) @@ -240,7 +240,7 @@ class WordPressComRestApiTests: XCTestCase { } ) progress1?.cancel() - api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, fileParts: [filePart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in + api.multipartPOST(wordPressMediaNewEndpointPath, parameters: nil, bodyParts: [mediaPart], success: { (responseObject: AnyObject, httpResponse: HTTPURLResponse?) in expect.fulfill() }, failure: { (error, httpResponse) in