fdcf82757d
Android UI Tests / ui-tests (push) Has been cancelled
Backblaze B2's S3-compatible endpoint does not implement the S3 POST
Object operation — every POST returns HTTP 501 regardless of URL form
(path-style or virtual-hosted-style). The previous multipart-POST flow
has been failing for every task-completion image upload.
Server-side companion change (honeyDueAPI master @7cc5448) replaces
PresignedPostPolicy with PresignHeader/PUT and renames the response
field from "fields" to "headers". This commit aligns both clients.
PresignUploadResponse model: field renamed `fields` → `headers`,
added `method` (default "PUT"). Both new fields have defaults so a
build talking to a stale server still decodes — albeit with empty
headers, which would then 403 at signature time. The server is
already on the new shape in prod.
iOS PresignedUploader.swift: dropped the ~70-line multipart body
builder and S3 form-field ordering logic. Replaced with a single PUT
request that applies server-supplied headers verbatim (skipping
Content-Length, which URLSession sets automatically and refuses to
override).
Android UploadApi.kt: same shape change. `postToStorage` →
`putToStorage`. Single Ktor `client.put()` with headers passthrough.
`uploadOne`'s `fileName` parameter kept for source compatibility but
marked @Suppress("UNUSED_PARAMETER") since PUT doesn't need it.
Verified end-to-end against api.myhoneydue.com:
presign → PUT 12 bytes → HTTP 200 in 0.6s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 lines
1.9 KiB
Kotlin
55 lines
1.9 KiB
Kotlin
package com.tt.honeyDue.models
|
|
|
|
import kotlinx.serialization.SerialName
|
|
import kotlinx.serialization.Serializable
|
|
|
|
/**
|
|
* Task completion create request matching Go API CreateTaskCompletionRequest
|
|
*/
|
|
@Serializable
|
|
data class TaskCompletionCreateRequest(
|
|
@SerialName("task_id") val taskId: Int,
|
|
@SerialName("completed_at") val completedAt: String? = null, // Defaults to now on server
|
|
val notes: String? = null,
|
|
@SerialName("actual_cost") val actualCost: Double? = null,
|
|
val rating: Int? = null, // 1-5 star rating
|
|
@SerialName("upload_ids") val uploadIds: List<Int>? = null // pending_uploads.id values from /api/uploads/presign + direct B2 POST
|
|
)
|
|
|
|
/**
|
|
* Presigned upload session — request body for POST /api/uploads/presign.
|
|
*
|
|
* Category: "completion" | "document_image" | "document_file"
|
|
* ContentType: the MIME type the client will upload (must match the policy
|
|
* exactly when POSTing to B2).
|
|
* ContentLength: byte count of the upload (server permits ±256 bytes slack).
|
|
*/
|
|
@Serializable
|
|
data class PresignUploadRequest(
|
|
val category: String,
|
|
@SerialName("content_type") val contentType: String,
|
|
@SerialName("content_length") val contentLength: Long
|
|
)
|
|
|
|
/**
|
|
* Presigned upload session — response from POST /api/uploads/presign.
|
|
*
|
|
* The client makes one PUT request to [uploadUrl] with the raw object
|
|
* bytes as the body and [headers] as the request headers. On success,
|
|
* pass [id] back in the upload_ids[] field of the next
|
|
* /api/task-completions/ or /api/documents/ create call.
|
|
*
|
|
* PUT (not POST) because B2's S3-compatible endpoint does not implement
|
|
* the S3 POST Object form upload (returns HTTP 501).
|
|
*/
|
|
@Serializable
|
|
data class PresignUploadResponse(
|
|
val id: Int,
|
|
@SerialName("upload_url") val uploadUrl: String,
|
|
val method: String = "PUT",
|
|
val headers: Map<String, String> = emptyMap(),
|
|
val key: String,
|
|
@SerialName("expires_at") val expiresAt: String
|
|
)
|
|
|