Adds the presigned-upload plumbing Outline's own web client uses for any file upload, not just avatars: attachments.create requests an upload target (server-assigned key, ACL, a short-lived signed form), then a direct multipart POST to that target (uploadUrl/form) actually uploads the bytes — confirmed against a live server's own request/ response shapes pulled from network capture. MultipartFormDataBuilder is a pure, fully-tested function (no network) building that S3-style presigned-POST body. uploadAttachmentFile deliberately sends no Bearer/CSRF header — the presigned form's `sig` field is what authorizes that specific request, same as an S3 presigned POST; flagged as best-effort pending live confirmation, cheap to add if the server turns out to also want one. UpdateUserAvatarRequest (users.update, avatar only) needed a custom encode(to:) — Swift's synthesized Encodable omits nil Optional keys via encodeIfPresent, but removing an avatar needs a literal "avatarUrl": null in the body, not the key missing. Confirmed the request shape (id + avatarUrl) from the captured request's Content-Length matching that shape and no shorter alternative. 6 new tests (multipart body structure/field ordering, explicit-null encoding, create/upload/update round trip) — 63/63 passing.
41 lines
1.4 KiB
Swift
41 lines
1.4 KiB
Swift
import Foundation
|
|
|
|
/// Builds the multipart/form-data body for Outline's presigned-upload
|
|
/// targets (`attachments.create`'s `uploadUrl`/`form`) — an S3-style
|
|
/// presigned POST: every `form` field has to ride along as its own part in
|
|
/// the same request as the file, not as query params or headers.
|
|
enum MultipartFormDataBuilder {
|
|
static func build(
|
|
fields: [String: String],
|
|
fileFieldName: String,
|
|
fileName: String,
|
|
fileData: Data,
|
|
fileContentType: String,
|
|
boundary: String = "Boundary-\(UUID().uuidString)"
|
|
) -> (body: Data, contentType: String) {
|
|
var body = Data()
|
|
|
|
for (key, value) in fields {
|
|
body.append("--\(boundary)\r\n".utf8Data)
|
|
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".utf8Data)
|
|
body.append(value.utf8Data)
|
|
body.append("\r\n".utf8Data)
|
|
}
|
|
|
|
body.append("--\(boundary)\r\n".utf8Data)
|
|
body.append(
|
|
"Content-Disposition: form-data; name=\"\(fileFieldName)\"; filename=\"\(fileName)\"\r\n".utf8Data
|
|
)
|
|
body.append("Content-Type: \(fileContentType)\r\n\r\n".utf8Data)
|
|
body.append(fileData)
|
|
body.append("\r\n".utf8Data)
|
|
body.append("--\(boundary)--\r\n".utf8Data)
|
|
|
|
return (body, "multipart/form-data; boundary=\(boundary)")
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
var utf8Data: Data { Data(utf8) }
|
|
}
|