Skip to content

Commit

Permalink
Add multipart POST method to WordPressComRESTAPIInterfacing (#765)
Browse files Browse the repository at this point in the history
  • Loading branch information
mokagio authored Mar 26, 2024
2 parents 77e2e58 + d7d0bf7 commit 01af35a
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ _None._
### Breaking Changes

- Reworked the `NSDate` RFC3339 / WordPress.com JSON conversions API [#759]
- Changed `FilePart` `filename` property to `fileName` [#765]

### New Features

Expand Down
8 changes: 4 additions & 4 deletions Sources/WordPressKit/Services/MediaServiceRemoteREST.m
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ - (void)uploadMedia:(NSArray *)mediaItems
if (remoteMedia.postID != nil && [remoteMedia.postID compare:@(0)] == NSOrderedDescending) {
parameters[@"attrs[0][parent_id]"] = remoteMedia.postID;
}
FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:remoteMedia.localURL filename:filename mimeType:type];
FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:remoteMedia.localURL fileName:filename mimeType:type];
[fileParts addObject:filePart];
}

[self.wordPressComRestApi multipartPOST:requestUrl
[self.wordPressComRESTAPI multipartPOST:requestUrl
parameters:parameters
fileParts:fileParts
requestEnqueued:^(NSNumber *taskID) {
Expand Down Expand Up @@ -203,8 +203,8 @@ - (void)uploadMedia:(RemoteMedia *)media
}
return;
}
FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL filename:filename mimeType:type];
__block NSProgress *localProgress = [self.wordPressComRestApi multipartPOST:requestUrl
FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL fileName:filename mimeType:type];
__block NSProgress *localProgress = [self.wordPressComRESTAPI multipartPOST:requestUrl
parameters:parameters
fileParts:@[filePart]
requestEnqueued:nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/WordPressKit/Services/PostServiceRemoteREST.m
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ - (void)createPost:(RemotePost *)post
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];
[self.wordPressComRestApi multipartPOST:requestUrl
FilePart *filePart = [[FilePart alloc] initWithParameterName:@"media[]" url:media.localURL fileName:filename mimeType:type];
[self.wordPressComRESTAPI multipartPOST:requestUrl
parameters:parameters
fileParts:@[filePart]
requestEnqueued:^(NSNumber *taskID) {
Expand Down
16 changes: 16 additions & 0 deletions Sources/WordPressKit/WordPressAPI/FilePart.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>

/// Represents the infomartion needed to encode a file on a multipart form request.
@interface FilePart: NSObject

@property (strong, nonatomic) NSString * _Nonnull parameterName;
@property (strong, nonatomic) NSURL * _Nonnull url;
@property (strong, nonatomic) NSString * _Nonnull fileName;
@property (strong, nonatomic) NSString * _Nonnull mimeType;

- (instancetype _Nonnull)initWithParameterName:(NSString * _Nonnull)parameterName
url:(NSURL * _Nonnull)url
fileName:(NSString * _Nonnull)fileName
mimeType:(NSString * _Nonnull)mimeType;

@end
18 changes: 18 additions & 0 deletions Sources/WordPressKit/WordPressAPI/FilePart.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import "FilePart.h"

@implementation FilePart

- (instancetype)initWithParameterName:(NSString *)parameterName
url:(NSURL *)url
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
{
self = [super init];
self.parameterName = parameterName;
self.url = url;
self.fileName = fileName;
self.mimeType = mimeType;
return self;
}

@end
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@import Foundation;

@class FilePart;

@protocol WordPressComRESTAPIInterfacing

@property (strong, nonatomic, readonly) NSURL * _Nonnull baseURL;
Expand All @@ -14,4 +16,11 @@
success:(void (^ _Nonnull)(id _Nonnull, NSHTTPURLResponse * _Nullable))success
failure:(void (^ _Nonnull)(NSError * _Nonnull, NSHTTPURLResponse * _Nullable))failure;

- (NSProgress * _Nullable)multipartPOST:(NSString * _Nonnull)URLString
parameters:(NSDictionary<NSString *, NSObject *> * _Nullable)parameters
fileParts:(NSArray<FilePart *> * _Nonnull)fileParts
requestEnqueued:(void (^ _Nullable)(NSNumber * _Nonnull))requestEnqueue
success:(void (^ _Nonnull)(id _Nonnull, NSHTTPURLResponse * _Nullable))success
failure:(void (^ _Nonnull)(NSError * _Nonnull, NSHTTPURLResponse * _Nullable))failure;

@end
60 changes: 34 additions & 26 deletions Sources/WordPressKit/WordPressAPI/WordPressComRestApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,18 @@ open class WordPressComRestApi: NSObject {
- parameter success: callback to be called on successful request
- parameter failure: callback to be called on failed request
- returns: a NSProgress object that can be used to track the progress of the upload and to cancel the upload. If the method
- returns: a `Progerss` object that can be used to track the progress of the upload and to cancel the upload. If the method
returns nil it's because something happened on the request serialization and the network request was not started, but the failure callback
will be invoked with the error specificing the serialization issues.
*/
@objc @discardableResult open func multipartPOST(_ URLString: String,
parameters: [String: AnyObject]?,
fileParts: [FilePart],
requestEnqueued: RequestEnqueuedBlock? = nil,
success: @escaping SuccessResponseBlock,
failure: @escaping FailureReponseBlock) -> Progress? {
@nonobjc @discardableResult open func multipartPOST(
_ URLString: String,
parameters: [String: AnyObject]?,
fileParts: [FilePart],
requestEnqueued: RequestEnqueuedBlock? = nil,
success: @escaping SuccessResponseBlock,
failure: @escaping FailureReponseBlock
) -> Progress? {
let progress = Progress.discreteProgress(totalUnitCount: 100)

Task { @MainActor in
Expand Down Expand Up @@ -452,7 +454,7 @@ open class WordPressComRestApi: NSObject {
let builder: HTTPRequestBuilder
do {
let form = try fileParts.map {
try MultipartFormField(fileAtPath: $0.url.path, name: $0.parameterName, filename: $0.filename, mimeType: $0.mimeType)
try MultipartFormField(fileAtPath: $0.url.path, name: $0.parameterName, filename: $0.fileName, mimeType: $0.mimeType)
}
builder = try requestBuilder(URLString: URLString)
.method(.post)
Expand All @@ -476,23 +478,6 @@ open class WordPressComRestApi: NSObject {

}

// MARK: - FilePart

/// 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
self.url = url
self.filename = filename
self.mimeType = mimeType
}
}

// MARK: - Error processing

extension WordPressComRestApi {
Expand Down Expand Up @@ -622,7 +607,6 @@ extension WordPressAPIError<WordPressComRestApiEndpointError> {
}

extension WordPressComRestApi: WordPressComRESTAPIInterfacing {

// A note on the naming: Even if defined as `GET` in Objective-C, then method gets converted to Swift as `get`.
//
// Also, there is no Objective-C direct equivalent of `AnyObject`, which here is used in `parameters: [String: AnyObject]?`.
Expand All @@ -646,4 +630,28 @@ extension WordPressComRestApi: WordPressComRESTAPIInterfacing {
) -> Progress? {
POST(URLString, parameters: parameters, success: success, failure: failure)
}

public func multipartPOST(
_ URLString: String,
parameters: [String: NSObject]?,
fileParts: [FilePart],
// Notice this does not require @escaping because it is Optional.
//
// Annotate with @escaping, and the compiler will fail with:
// > Closure is already escaping in optional type argument
//
// It is necessary to explicitly set this as Optional because of the _Nullable parameter requirement in the Objective-C protocol.
requestEnqueued: ((NSNumber) -> Void)?,
success: @escaping (Any, HTTPURLResponse?) -> Void,
failure: @escaping (any Error, HTTPURLResponse?) -> Void
) -> Progress? {
multipartPOST(
URLString,
parameters: parameters,
fileParts: fileParts,
requestEnqueued: requestEnqueued,
success: success as SuccessResponseBlock,
failure: failure as FailureReponseBlock
)
}
}
2 changes: 2 additions & 0 deletions Sources/WordPressKit/WordPressKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ FOUNDATION_EXPORT double WordPressKitVersionNumber;
//! Project version string for WordPressKit.
FOUNDATION_EXPORT const unsigned char WordPressKitVersionString[];

#import <WordPressKit/FilePart.h>
#import <WordPressKit/WordPressComRESTAPIInterfacing.h>

#import <WordPressKit/ServiceRemoteWordPressComREST.h>
#import <WordPressKit/ServiceRemoteWordPressXMLRPC.h>
#import <WordPressKit/SiteServiceRemoteWordPressComREST.h>
Expand Down
4 changes: 2 additions & 2 deletions Tests/WordPressKitTests/Tests/WordPressComRestApiTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ 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")
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: { (_: AnyObject, _: HTTPURLResponse?) in
expect.fulfill()
XCTFail("This call should fail")
Expand All @@ -319,7 +319,7 @@ 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 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: { (_: AnyObject, _: HTTPURLResponse?) in
XCTFail("This call should fail")
}, failure: { (error, _) in
Expand Down
8 changes: 8 additions & 0 deletions WordPressKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
3F758FD324F6C68200BBA2FC /* AnnouncementServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F758FD224F6C68200BBA2FC /* AnnouncementServiceRemote.swift */; };
3F8308A729EE683500354497 /* ActivityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8308A629EE683500354497 /* ActivityTests.swift */; };
3FB8642C2888089F003A86BE /* BuildkiteTestCollector in Frameworks */ = {isa = PBXBuildFile; productRef = 3FB8642B2888089F003A86BE /* BuildkiteTestCollector */; };
3FE2E94F2BB29A1B002CA2E1 /* FilePart.m in Sources */ = {isa = PBXBuildFile; fileRef = 3FE2E94D2BB29A1B002CA2E1 /* FilePart.m */; };
3FE2E9502BB29A1B002CA2E1 /* FilePart.h in Headers */ = {isa = PBXBuildFile; fileRef = 3FE2E94E2BB29A1B002CA2E1 /* FilePart.h */; settings = {ATTRIBUTES = (Public, ); }; };
3FFCC0412BA995290051D229 /* Date+WordPressComTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFCC0402BA995290051D229 /* Date+WordPressComTests.swift */; };
3FFCC0472BAA6EF40051D229 /* NSDate+WordPressCom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFCC0462BAA6EF40051D229 /* NSDate+WordPressCom.swift */; };
3FFCC0492BAB98130051D229 /* DateFormatter+WordPressCom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFCC0482BAB98130051D229 /* DateFormatter+WordPressCom.swift */; };
Expand Down Expand Up @@ -803,6 +805,8 @@
3F758FD224F6C68200BBA2FC /* AnnouncementServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementServiceRemote.swift; sourceTree = "<group>"; };
3F8308A629EE683500354497 /* ActivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTests.swift; sourceTree = "<group>"; };
3FB8642D288813E9003A86BE /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
3FE2E94D2BB29A1B002CA2E1 /* FilePart.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FilePart.m; sourceTree = "<group>"; };
3FE2E94E2BB29A1B002CA2E1 /* FilePart.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FilePart.h; sourceTree = "<group>"; };
3FFCC0402BA995290051D229 /* Date+WordPressComTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+WordPressComTests.swift"; sourceTree = "<group>"; };
3FFCC0462BAA6EF40051D229 /* NSDate+WordPressCom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSDate+WordPressCom.swift"; sourceTree = "<group>"; };
3FFCC0482BAB98130051D229 /* DateFormatter+WordPressCom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+WordPressCom.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1534,6 +1538,8 @@
3FE2E9352BB10D92002CA2E1 /* WordPressAPI */ = {
isa = PBXGroup;
children = (
3FE2E94E2BB29A1B002CA2E1 /* FilePart.h */,
3FE2E94D2BB29A1B002CA2E1 /* FilePart.m */,
3FFCC0552BABC78B0051D229 /* WordPressComRESTAPIInterfacing.h */,
4A05E7952B2FCB6400C25E3B /* NonceRetrieval.swift */,
4A05E7992B2FDC3200C25E3B /* WordPressOrgRestApi.swift */,
Expand Down Expand Up @@ -2579,6 +2585,7 @@
7430C9B11F1927C50051B8E6 /* RemoteReaderPost.h in Headers */,
7430C9A51F1927180051B8E6 /* ReaderSiteServiceRemote.h in Headers */,
7430C9A31F1927180051B8E6 /* ReaderPostServiceRemote.h in Headers */,
3FE2E9502BB29A1B002CA2E1 /* FilePart.h in Headers */,
742362D61F10250600BD0A7F /* MenusServiceRemote.h in Headers */,
740B23BA1F17EC7300067A2A /* PostServiceRemoteXMLRPC.h in Headers */,
740B23BD1F17ECB500067A2A /* PostServiceRemoteOptions.h in Headers */,
Expand Down Expand Up @@ -3291,6 +3298,7 @@
82FFBF501F45EFD100F4573F /* RemoteBlogJetpackSettings.swift in Sources */,
74650F741F0EA1E200188EDB /* RemoteGravatarProfile.swift in Sources */,
40E7FEB4221063480032834E /* StatsTodayInsight.swift in Sources */,
3FE2E94F2BB29A1B002CA2E1 /* FilePart.m in Sources */,
F41D98EA2B48602B004EC050 /* SessionDetails.swift in Sources */,
436D563C2118E18D00CEAA33 /* WPState.swift in Sources */,
439A44DA2107C93000795ED7 /* RemotePlan_ApiVersion1_3.swift in Sources */,
Expand Down

0 comments on commit 01af35a

Please sign in to comment.