251 lines
6.2 KiB
Go
251 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.kornbrand.biz/plankalkul/autoixpert-sample/pkg/models"
|
|
)
|
|
|
|
type apiError struct {
|
|
StatusCode int `json:"status_code"`
|
|
Endpoint string `json:"endpoint"`
|
|
ErrorCode string `json:"error_code"`
|
|
ErrorMessage string `json:"error_message"`
|
|
StackTrace string `json:"stack_trace"`
|
|
}
|
|
|
|
// could be more elaborate depending on the users needs, for simplicity just the API error messageu
|
|
// depending on usecase, might be useful to include all fields of this in the slog as parsebable output
|
|
func (e *apiError) Error() string {
|
|
if e.ErrorMessage == "" {
|
|
return fmt.Sprintf("api request: %s failed with status %d", e.Endpoint, e.StatusCode)
|
|
}
|
|
return e.ErrorMessage
|
|
}
|
|
|
|
func decodeAPIError(resp *http.Response) error {
|
|
body, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return fmt.Errorf("api request failed with status %d (failed to read error response: %w)", resp.StatusCode, readErr)
|
|
}
|
|
|
|
var apiErr apiError
|
|
if err := json.Unmarshal(body, &apiErr); err != nil {
|
|
return fmt.Errorf("api request failed with status %d (failed to decode error response: %w)", resp.StatusCode, err)
|
|
}
|
|
|
|
return &apiErr
|
|
}
|
|
|
|
func createReport(report models.CrashReport) (string, error) {
|
|
|
|
var body = report.ToClaimRequest()
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", cfg.APIBase+"/reports", bytes.NewBuffer(data))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := DoWithRateLimitReset(context.Background(), req)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 400 {
|
|
return "", decodeAPIError(resp)
|
|
}
|
|
|
|
var claimResp models.ClaimResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&claimResp); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return claimResp.Document.ID, nil
|
|
}
|
|
|
|
func uploadImages(imageURLs []string, id string, eventID string) error {
|
|
images, err := collectImages(imageURLs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
metaResp, err := requestPhotoMetadata(id, eventID, images)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uploadResp, err := requestUploadURLs(id, eventID, metaResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return uploadPhotoBinaries(images, metaResp, uploadResp)
|
|
}
|
|
|
|
func collectImages(imageURLs []string) ([]imageWithMeta, error) {
|
|
images := make([]imageWithMeta, 0, len(imageURLs))
|
|
|
|
for idx, url := range imageURLs {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mime := http.DetectContentType(data)
|
|
size := len(data)
|
|
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
images = append(images, imageWithMeta{
|
|
Data: data,
|
|
Meta: models.ImageMetaRequest{
|
|
Title: strconv.Itoa(idx),
|
|
Description: "",
|
|
OriginalName: strconv.Itoa(idx),
|
|
Mimetype: mime,
|
|
Size: int64(size),
|
|
Width: cfg.Width,
|
|
Height: cfg.Height,
|
|
IncludedInReport: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
return images, nil
|
|
}
|
|
|
|
func requestPhotoMetadata(reportID string, eventID string, images []imageWithMeta) (models.ImageMetaResponse, error) {
|
|
metas := make([]models.ImageMetaRequest, 0, len(images))
|
|
for _, img := range images {
|
|
metas = append(metas, img.Meta)
|
|
}
|
|
|
|
body, err := json.Marshal(models.ImageMetaRequestWrapper{Photos: metas})
|
|
if err != nil {
|
|
return models.ImageMetaResponse{}, err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", cfg.APIBase+"/reports/"+reportID+"/photos/batch", bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return models.ImageMetaResponse{}, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := DoWithRateLimitReset(context.Background(), req)
|
|
if err != nil {
|
|
return models.ImageMetaResponse{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return models.ImageMetaResponse{}, decodeAPIError(resp)
|
|
}
|
|
|
|
var metaResp models.ImageMetaResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&metaResp); err != nil {
|
|
return models.ImageMetaResponse{}, err
|
|
}
|
|
|
|
return metaResp, nil
|
|
}
|
|
|
|
func requestUploadURLs(reportID string, eventID string, metaResp models.ImageMetaResponse) (models.UploadURLResponse, error) {
|
|
ids := make([]string, 0, len(metaResp.Photos))
|
|
for _, photo := range metaResp.Photos {
|
|
ids = append(ids, photo.ID)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", cfg.APIBase+"/reports/"+reportID+"/photos/batch/upload?photo_ids="+strings.Join(ids, ","), nil)
|
|
if err != nil {
|
|
return models.UploadURLResponse{}, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := DoWithRateLimitReset(context.Background(), req)
|
|
if err != nil {
|
|
return models.UploadURLResponse{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return models.UploadURLResponse{}, decodeAPIError(resp)
|
|
}
|
|
|
|
var uploadResp models.UploadURLResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
|
|
return models.UploadURLResponse{}, err
|
|
}
|
|
|
|
return uploadResp, nil
|
|
}
|
|
|
|
func uploadPhotoBinaries(images []imageWithMeta, metaResp models.ImageMetaResponse, uploadResp models.UploadURLResponse) error {
|
|
imageByTitle := make(map[string]imageWithMeta, len(images))
|
|
for _, img := range images {
|
|
imageByTitle[img.Meta.Title] = img
|
|
}
|
|
|
|
titleByPhotoID := make(map[string]string, len(metaResp.Photos))
|
|
for _, metaImg := range metaResp.Photos {
|
|
titleByPhotoID[metaImg.ID] = metaImg.Title
|
|
}
|
|
|
|
for _, upload := range uploadResp.UploadURLs {
|
|
title, ok := titleByPhotoID[upload.PhotoID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
img, ok := imageByTitle[title]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
putReq, err := http.NewRequest("PUT", upload.UploadURL, bytes.NewReader(img.Data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
putReq.Header.Set("Content-Type", img.Meta.Mimetype)
|
|
|
|
resp, err := Do(context.Background(), putReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("failed to upload photo %s: status %d", title, resp.StatusCode)
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|