From 2ff190956c7ce44e6917a583414593aca0ea1ccc Mon Sep 17 00:00:00 2001 From: kay Date: Sat, 30 May 2026 08:41:57 +0900 Subject: [PATCH] fix(image): correct upload token parsing and OSS file Content-Type The /resource/image/upload_token API returns the token object directly under `data`, but the client expected `data.tokens[]`, so the slice was always empty and image saves failed with "no upload token returned". After fixing the parsing, the OSS POST still failed with 403 because multipart.CreateFormFile hardcodes the file part Content-Type to application/octet-stream, violating the upload policy condition (`["eq", "$Content-Type", "image/png"]`). Set the file part Content-Type explicitly to the token's oss_content_type. Verified end-to-end against the live API: image note now uploads and saves. --- cmd/save/save.go | 4 ++-- internal/client/client.go | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cmd/save/save.go b/cmd/save/save.go index 51cc0b0..b758e8b 100644 --- a/cmd/save/save.go +++ b/cmd/save/save.go @@ -114,10 +114,10 @@ func saveImage(cmd *cobra.Command, c *client.Client, imagePath, title string, ta if err != nil { return fmt.Errorf("getting upload token: %w", err) } - if len(tokenResp.Data.Tokens) == 0 { + token := tokenResp.Data + if token.AccessID == "" || token.Policy == "" { return fmt.Errorf("no upload token returned") } - token := tokenResp.Data.Tokens[0] // Step 2: upload to OSS if !isJSON { diff --git a/internal/client/client.go b/internal/client/client.go index 7dea1cd..a793b09 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -8,9 +8,11 @@ import ( "io" "mime/multipart" "net/http" + "net/textproto" "net/url" "os" "path/filepath" + "strings" "time" "github.com/iswalle/getnote-cli/internal/config" @@ -666,15 +668,12 @@ type ImageUploadToken struct { OSSContentType string `json:"oss_content_type"` } -// ImageUploadTokenData is the data field of the upload token response. -type ImageUploadTokenData struct { - Tokens []ImageUploadToken `json:"tokens"` -} - // ImageUploadTokenResponse is the response from the upload token endpoint. +// The API returns the token object directly under `data` (it is not wrapped +// in a `tokens` array), so Data is a single ImageUploadToken. type ImageUploadTokenResponse struct { - Success bool `json:"success"` - Data ImageUploadTokenData `json:"data"` + Success bool `json:"success"` + Data ImageUploadToken `json:"data"` } // ImageGetUploadToken retrieves OSS upload credentials for the given mime type. @@ -725,7 +724,16 @@ func (c *Client) ImageUploadToOSS(token ImageUploadToken, imagePath string) erro return err } - fw, err := w.CreateFormFile("file", filepath.Base(imagePath)) + // The OSS upload policy enforces a Content-Type condition (e.g. image/png), + // so the file part must carry that exact MIME type. multipart.CreateFormFile + // hardcodes "application/octet-stream", which fails the policy check, so set + // the part header explicitly. + quoteEscaper := strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + partHeader := make(textproto.MIMEHeader) + partHeader.Set("Content-Disposition", + fmt.Sprintf(`form-data; name="file"; filename="%s"`, quoteEscaper.Replace(filepath.Base(imagePath)))) + partHeader.Set("Content-Type", token.OSSContentType) + fw, err := w.CreatePart(partHeader) if err != nil { return err }