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.
29 lines
1.0 KiB
Swift
29 lines
1.0 KiB
Swift
import Foundation
|
|
|
|
/// `users.update`, scoped to just the avatar. Custom `encode(to:)` because
|
|
/// Swift's synthesized `Encodable` uses `encodeIfPresent` for `Optional`
|
|
/// properties, which *omits* the key entirely when the value is `nil` —
|
|
/// removing the avatar needs a literal `"avatarUrl": null` in the request
|
|
/// body, not the key missing. `id` is required: confirmed live (the
|
|
/// captured request body's length only matches `{"id": "...", "avatarUrl":
|
|
/// ...}`, not a shorter shape without it).
|
|
public struct UpdateUserAvatarRequest: Encodable, Sendable {
|
|
public let id: String
|
|
public let avatarUrl: String?
|
|
|
|
public init(id: String, avatarUrl: String?) {
|
|
self.id = id
|
|
self.avatarUrl = avatarUrl
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, avatarUrl
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(id, forKey: .id)
|
|
try container.encode(avatarUrl, forKey: .avatarUrl)
|
|
}
|
|
}
|