Files
Outpost/OutlineKit/Sources/OutlineKit/Models/OutlineAttachment.swift
T
Puranjay Savar Mattas 373c681c10 feat(profile): avatar upload/remove backend (attachments + users.update)
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.
2026-08-15 16:19:33 +01:00

27 lines
1.1 KiB
Swift

import Foundation
/// One uploaded file — created via a two-step presigned upload
/// (`attachments.create` for the upload target, then a direct POST to
/// `uploadUrl`/`form`). Confirmed live against a self-hosted instance's
/// local-storage backend; `size` comes back as a string there, not a
/// number — same "don't trust the vendored spec's implied types" lesson as
/// everywhere else this codebase has hit it.
public struct OutlineAttachment: Decodable, Identifiable, Sendable {
public let id: String
public let documentId: String?
public let contentType: String?
public let name: String?
public let url: String?
public let size: String?
}
/// `attachments.create`'s response — everything needed to perform the
/// actual upload. `form` fields (Content-Type, key, acl, sig, `_csrf`, …)
/// must all be included as their own multipart parts, in the same request
/// as the file itself, POSTed to `uploadUrl`.
public struct CreateAttachmentResult: Decodable, Sendable {
public let attachment: OutlineAttachment
public let uploadUrl: String
public let form: [String: String]
}