Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,6 @@ private fun FetchAndParseApiRootFailure.getRequestExecutionErrorReason(): Reques
private fun RequestExecutionException.reason(): RequestExecutionErrorReason? {
return when (this) {
is RequestExecutionException.RequestExecutionFailed -> this.reason
is RequestExecutionException.MediaFileNotFound -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,7 @@ class MediaEndpointTest {
val title = "Testing media upload from Kotlin"
val response = client.request { requestBuilder ->
requestBuilder.media().create(
params = MediaCreateParams(title = title),
"test_media.jpg",
"image/jpeg",
null
params = MediaCreateParams(title = title, filePath = "test_media.jpg")
)
}.assertSuccessAndRetrieveData().data
assertEquals(title, response.title.rendered)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package rs.wordpress.api.kotlin

import kotlinx.coroutines.delay
import okio.FileNotFoundException
import uniffi.wp_api.MediaUploadRequest
import uniffi.wp_api.RequestContext
import uniffi.wp_api.RequestExecutor
import uniffi.wp_api.WpMultipartFormRequest
import uniffi.wp_api.WpNetworkHeaderMap
import uniffi.wp_api.WpNetworkRequest
import uniffi.wp_api.WpNetworkResponse
Expand Down Expand Up @@ -41,7 +41,7 @@ class MockRequestExecutor(private var stubs: List<Stub> = listOf()) : RequestExe
throw NoStubFoundException("No stub found for ${request.url()}")
}

override suspend fun uploadMedia(mediaUploadRequest: MediaUploadRequest): WpNetworkResponse {
override suspend fun upload(request: WpMultipartFormRequest): WpNetworkResponse {
TODO("Not yet implemented")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import uniffi.wp_api.InvalidSslErrorReason
import uniffi.wp_api.MediaUploadRequest
import uniffi.wp_api.MediaUploadRequestExecutionException
import uniffi.wp_api.RequestContext
import uniffi.wp_api.RequestExecutionErrorReason
import uniffi.wp_api.RequestExecutionException
import uniffi.wp_api.RequestExecutor
import uniffi.wp_api.RequestMethod
import uniffi.wp_api.WpMultipartFormRequest
import uniffi.wp_api.WpNetworkHeaderMap
import uniffi.wp_api.WpNetworkRequest
import uniffi.wp_api.WpNetworkResponse
Expand Down Expand Up @@ -85,54 +84,57 @@ class WpRequestExecutor(
}
}

override suspend fun uploadMedia(mediaUploadRequest: MediaUploadRequest): WpNetworkResponse =
override suspend fun upload(request: WpMultipartFormRequest): WpNetworkResponse =
withContext(dispatcher) {
val requestBuilder = Request.Builder().url(mediaUploadRequest.url())
val requestBuilder = Request.Builder().url(request.url())
val multipartBodyBuilder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
mediaUploadRequest.mediaParams().forEach { (k, v) ->
request.fields().forEach { (k, v) ->
multipartBodyBuilder.addFormDataPart(k, v)
}
val file = fileResolver.getFile(mediaUploadRequest.filePath())
if (file == null || !file.canBeUploaded()) {
throw MediaUploadRequestExecutionException.MediaFileNotFound(mediaUploadRequest.filePath())
request.files().forEach { (name, fileInfo) ->
val file = fileResolver.getFile(fileInfo.filePath)
if (file == null || !file.canBeUploaded()) {
throw RequestExecutionException.MediaFileNotFound(filePath = fileInfo.filePath)
}
val mimeType = fileInfo.mimeType ?: "application/octet-stream"
val requestBody = getRequestBody(file, mimeType, uploadListener)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oguzkocer I'm not sure if the progress reporting is still working as expected. The previous implementation only supported uploading one file, but the new implementation supports uploading many files. I'm not sure if the uploadListener still works.

val filename = fileInfo.fileName ?: file.name
multipartBodyBuilder.addFormDataPart(
name = name,
filename = filename,
body = requestBody
)
}
val progressRequestBody = getRequestBody(file, mediaUploadRequest, uploadListener)
multipartBodyBuilder.addFormDataPart(
name = "file",
filename = file.name,
body = progressRequestBody
)
requestBuilder.method(
method = mediaUploadRequest.method().toString(),
method = request.method().toString(),
body = multipartBodyBuilder.build()
)
mediaUploadRequest.headerMap().toMap().forEach { (key, values) ->
request.headerMap().toMap().forEach { (key, values) ->
values.forEach { value ->
requestBuilder.addHeader(key, value)
}
}

val call = httpClient.getClient().newCall(requestBuilder.build())
// Notify about the call creation so it can be cancelled if needed
uploadListener?.onUploadStarted(CancellableCall(call))
call.execute().use { response ->
return@withContext WpNetworkResponse(
body = response.body?.bytes() ?: ByteArray(0),
statusCode = response.code.toUShort(),
responseHeaderMap = WpNetworkHeaderMap.fromMultiMap(response.headers.toMultimap()),
requestUrl = mediaUploadRequest.url(),
requestHeaderMap = mediaUploadRequest.headerMap()
requestUrl = request.url(),
requestHeaderMap = request.headerMap()
)
}
}

private fun getRequestBody(
file: File,
mediaUploadRequest: MediaUploadRequest,
mimeType: String,
uploadListener: UploadListener?
): RequestBody {
val fileRequestBody = file.asRequestBody(mediaUploadRequest.fileContentType().toMediaType())
val fileRequestBody = file.asRequestBody(mimeType.toMediaType())
return if (uploadListener != null) {
ProgressRequestBody(
delegate = fileRequestBody,
Expand Down
3 changes: 1 addition & 2 deletions native/swift/Example/Example/UI/UploadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ private class UploadViewModel: ObservableObject {

NSLog("Uploading \(item)")
_ = try await api.uploadMedia(
params: .init(),
fromLocalFileURL: file,
params: .init(filePath: file.path),
fulfilling: child
)

Expand Down
1 change: 0 additions & 1 deletion native/swift/Sources/wordpress-api/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ public typealias RevisionsRequestListWithEmbedContextResponse = WordPressAPIInte

// MARK: - Media
public typealias SparseMedia = WordPressAPIInternal.SparseMedia
public typealias MediaUploadRequest = WordPressAPIInternal.MediaUploadRequest
public typealias MediaWithEditContext = WordPressAPIInternal.MediaWithEditContext
public typealias MediaWithViewContext = WordPressAPIInternal.MediaWithViewContext
public typealias MediaWithEmbedContext = WordPressAPIInternal.MediaWithEmbedContext
Expand Down
53 changes: 31 additions & 22 deletions native/swift/Sources/wordpress-api/SafeRequestExecutor.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Foundation
import WordPressAPIInternal

#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
Expand All @@ -11,9 +15,7 @@ import Combine

public protocol SafeRequestExecutor: RequestExecutor, Sendable {
func execute(_ request: WpNetworkRequest) async -> Result<WpNetworkResponse, RequestExecutionError>
func uploadMedia(
mediaUploadRequest: MediaUploadRequest
) async -> Result<WpNetworkResponse, MediaUploadRequestExecutionError>
func upload(request: WpMultipartFormRequest) async -> Result<WpNetworkResponse, RequestExecutionError>

#if PROGRESS_REPORTING_ENABLED
/// Returns a publisher that emits zero or one `Progress` instance representing the overall progress of the task
Expand All @@ -28,8 +30,8 @@ extension SafeRequestExecutor {
return try result.get()
}

public func uploadMedia(mediaUploadRequest: MediaUploadRequest) async throws -> WpNetworkResponse {
let result = await uploadMedia(mediaUploadRequest: mediaUploadRequest)
public func upload(request: WpMultipartFormRequest) async throws -> WpNetworkResponse {
let result = await upload(request: request)
return try result.get()
}
}
Expand Down Expand Up @@ -59,20 +61,8 @@ public final class WpRequestExecutor: SafeRequestExecutor {
await perform(request)
}

public func uploadMedia(
mediaUploadRequest: MediaUploadRequest
) async -> Result<WpNetworkResponse, MediaUploadRequestExecutionError> {
(await perform(mediaUploadRequest))
.mapError { error in
switch error {
case let .RequestExecutionFailed(statusCode, redirects, reason):
MediaUploadRequestExecutionError.RequestExecutionFailed(
statusCode: statusCode,
redirects: redirects,
reason: reason
)
}
}
public func upload(request: WpMultipartFormRequest) async -> Result<WpNetworkResponse, RequestExecutionError> {
await perform(request)
}

public func cancel(context: RequestContext) {
Expand All @@ -93,6 +83,10 @@ public final class WpRequestExecutor: SafeRequestExecutor {

return .success(try WpNetworkResponse(data: data, request: request, response: response))
} catch {
if let error = error as? RequestExecutionError {
return .failure(error)
}

if errorIsHttpsError(error) {
return handleHttpsError(error, for: request)
}
Expand Down Expand Up @@ -380,7 +374,7 @@ extension WpNetworkRequest: NetworkRequestContent {
}
}

extension MediaUploadRequest: NetworkRequestContent {
extension WpMultipartFormRequest: NetworkRequestContent {

func encodeBody(into request: inout URLRequest) throws {
// Do nothing.
Expand All @@ -394,10 +388,24 @@ extension MediaUploadRequest: NetworkRequestContent {
var request = try buildURLRequest(additionalHeaders: headers)

var form = [MultipartFormField]()
for (name, value) in mediaParams() {
for (name, value) in fields() {
form.append(.init(text: value, name: name))
}
try form.append(.init(fileAtPath: filePath(), name: "file"))
for (name, file) in files() {
var mimeType = file.mimeType

#if canImport(UniformTypeIdentifiers)
if mimeType == nil {
mimeType = UTType(filenameExtension: URL(fileURLWithPath: file.filePath).pathExtension)?.preferredMIMEType
}
#endif

do {
try form.append(.init(fileAtPath: file.filePath, name: name, filename: file.fileName, mimeType: mimeType))
} catch {
throw RequestExecutionError.MediaFileNotFound(filePath: file.filePath)
}
}

let boundery = String(format: "wordpressrs.%08x", Int.random(in: Int.min..<Int.max))
request.setValue("multipart/form-data; boundary=\(boundery)", forHTTPHeaderField: "Content-Type")
Expand All @@ -421,3 +429,4 @@ extension MediaUploadRequest: NetworkRequestContent {
}

}

61 changes: 28 additions & 33 deletions native/swift/Sources/wordpress-api/WordPressAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import FoundationNetworking
import Combine
#endif

#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif

public actor WordPressAPI {

enum Errors: Error {
Expand Down Expand Up @@ -174,47 +170,46 @@ public actor WordPressAPI {
}

#if PROGRESS_REPORTING_ENABLED
public func uploadMedia(
params: MediaCreateParams,
fromLocalFileURL localFileURL: URL,
fulfilling progress: Progress,
mimeType: String? = nil,
) async throws -> MediaRequestCreateResponse {
precondition(localFileURL.isFileURL)
public func uploadMedia(params: MediaCreateParams, fulfilling progress: Progress) async throws -> MediaRequestCreateResponse {
precondition(progress.completedUnitCount == 0 && progress.totalUnitCount > 0)
precondition(progress.cancellationHandler == nil)

let requestId = WpUuid()
let context = RequestContext()

let fileContentType: String
if let mimeType {
fileContentType = mimeType
} else if let mimeType = UTType(filenameExtension: localFileURL.pathExtension)?.preferredMIMEType {
fileContentType = mimeType
} else {
fileContentType = "application/octet-stream"
let uploadTask = Task {
try await media.createCancellation(params: params, context: context)
}

let cancellable = requestExecutor
.progress(forRequestWithId: requestId.uuidString())
.sink {
progress.addChild($0, withPendingUnitCount: progress.totalUnitCount - progress.completedUnitCount)
let progressObserver = Task {
// A request id will be put into the `RequestContext` during the execution of the `media.create` above.
// This loop waits for the request id becomes available
let requestId: String
while true {
try await Task.sleep(nanoseconds: 100_000)
try Task.checkCancellation()

guard let id = context.requestIds().first else {
continue
}

requestId = id
break
}
defer {
cancellable.cancel()
}

let uploadTask = Task {
try await media.create(
params: params,
filePath: localFileURL.path,
fileContentType: fileContentType,
requestId: requestId
)
// Get the progress of the `URLSessionTask` of the given request id.
guard let task = await requestExecutor
.progress(forRequestWithId: requestId)
.values
.first(where: { _ in true }) else { return }

try Task.checkCancellation()

progress.addChild(task, withPendingUnitCount: progress.totalUnitCount - progress.completedUnitCount)
}

progress.cancellationHandler = {
uploadTask.cancel()
progressObserver.cancel()
}

return try await withTaskCancellationHandler {
Expand Down
Loading