Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

189 lines
7.3 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"strconv"
"strings"
)
func journalCollectionPath(gardenID int) string {
return "v1/gardens/" + strconv.Itoa(gardenID) + "/journal"
}
// Supported journal entry types.
const (
JournalEntryTypeJournal = "journal"
JournalEntryTypePinboard = "pinboard"
)
func journalEntryPath(gardenID, entryID int) string {
return journalCollectionPath(gardenID) + "/" + strconv.Itoa(entryID)
}
func journalAttachmentPath(gardenID, entryID, attachmentID int) string {
return journalEntryPath(gardenID, entryID) + "/attachments/" + strconv.Itoa(attachmentID)
}
// CreateJournalEntry adds a journal or pinboard entry to a garden.
func (c *Client) CreateJournalEntry(ctx context.Context, gardenID int, input JournalEntryInput) (JournalEntry, *Response, error) {
return c.writeJournalEntry(ctx, http.MethodPost, journalCollectionPath(gardenID), input)
}
// JournalEntries lists all journal and pinboard entries in a garden.
func (c *Client) JournalEntries(ctx context.Context, gardenID int) ([]JournalEntry, *Response, error) {
return c.JournalEntriesByType(ctx, gardenID, JournalEntryTypeJournal)
}
// JournalEntriesByType lists garden entries of one journal entry type.
func (c *Client) JournalEntriesByType(ctx context.Context, gardenID int, entryType string) ([]JournalEntry, *Response, error) {
var envelope struct {
JournalEntries []JournalEntry `json:"journal_entries"`
}
path := journalCollectionPath(gardenID)
if entryType != "" && entryType != JournalEntryTypeJournal {
path += "?type=" + url.QueryEscape(entryType)
}
response, err := c.do(ctx, http.MethodGet, path, nil, &envelope)
return envelope.JournalEntries, response, err
}
// GardenTags lists tag names available within a garden.
func (c *Client) GardenTags(ctx context.Context, gardenID int) ([]string, *Response, error) {
var envelope struct {
Tags []string `json:"tags"`
}
response, err := c.do(ctx, http.MethodGet, "v1/gardens/"+strconv.Itoa(gardenID)+"/tags", nil, &envelope)
return envelope.Tags, response, err
}
// JournalEntry returns one entry within its garden.
func (c *Client) JournalEntry(ctx context.Context, gardenID, entryID int) (JournalEntry, *Response, error) {
var envelope struct {
JournalEntry JournalEntry `json:"journal_entry"`
}
response, err := c.do(ctx, http.MethodGet, journalEntryPath(gardenID, entryID), nil, &envelope)
return envelope.JournalEntry, response, err
}
// UpdateJournalEntry changes an entry within its garden.
func (c *Client) UpdateJournalEntry(ctx context.Context, gardenID, entryID int, input JournalEntryInput) (JournalEntry, *Response, error) {
return c.writeJournalEntry(ctx, http.MethodPatch, journalEntryPath(gardenID, entryID), input)
}
func (c *Client) writeJournalEntry(ctx context.Context, method, path string, input JournalEntryInput) (JournalEntry, *Response, error) {
var envelope struct {
JournalEntry JournalEntry `json:"journal_entry"`
}
response, err := c.do(ctx, method, path, input, &envelope)
return envelope.JournalEntry, response, err
}
// DeleteJournalEntry removes an entry and its attachment records.
func (c *Client) DeleteJournalEntry(ctx context.Context, gardenID, entryID int) (*Response, error) {
return c.do(ctx, http.MethodDelete, journalEntryPath(gardenID, entryID), nil, nil)
}
// UploadJournalAttachment adds binary media to an entry.
func (c *Client) UploadJournalAttachment(ctx context.Context, gardenID, entryID int, fileName, mediaType string, data []byte) (JournalAttachment, *Response, error) {
var body bytes.Buffer
w := multipart.NewWriter(&body)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", mime.FormatMediaType("form-data", map[string]string{"name": "file", "filename": fileName}))
header.Set("Content-Type", mediaType)
part, err := w.CreatePart(header)
if err != nil {
return JournalAttachment{}, nil, err
}
if _, err = part.Write(data); err != nil {
return JournalAttachment{}, nil, err
}
if err = w.Close(); err != nil {
return JournalAttachment{}, nil, err
}
response, responseBody, err := c.doBinary(ctx, http.MethodPost, journalEntryPath(gardenID, entryID)+"/attachments", &body, w.FormDataContentType(), 2<<20)
if err != nil {
return JournalAttachment{}, response, err
}
var envelope struct {
Attachment JournalAttachment `json:"attachment"`
}
if err = json.Unmarshal(responseBody, &envelope); err != nil {
return JournalAttachment{}, response, fmt.Errorf("client: decode response: %w", err)
}
return envelope.Attachment, response, nil
}
// AttachJournalLibraryImage links an existing garden image to an entry.
func (c *Client) AttachJournalLibraryImage(ctx context.Context, gardenID, entryID, imageID int) (JournalAttachment, *Response, error) {
var envelope struct {
Attachment JournalAttachment `json:"attachment"`
}
response, err := c.do(ctx, http.MethodPost, journalEntryPath(gardenID, entryID)+"/attachments/library", map[string]int{"image_id": imageID}, &envelope)
return envelope.Attachment, response, err
}
// JournalAttachment downloads attachment metadata and binary data.
func (c *Client) JournalAttachment(ctx context.Context, gardenID, entryID, attachmentID int) (JournalAttachmentData, *Response, error) {
response, data, err := c.doBinary(ctx, http.MethodGet, journalAttachmentPath(gardenID, entryID, attachmentID), nil, "", (25<<20)+1)
if err != nil {
return JournalAttachmentData{}, response, err
}
attachment := JournalAttachmentData{Data: data}
attachment.ID, attachment.EntryID = attachmentID, entryID
attachment.MediaType = strings.Split(response.Header.Get("Content-Type"), ";")[0]
if _, params, parseErr := mime.ParseMediaType(response.Header.Get("Content-Disposition")); parseErr == nil {
attachment.FileName = params["filename"]
}
attachment.Size = int64(len(data))
return attachment, response, nil
}
// DeleteJournalAttachment removes an attachment from an entry.
func (c *Client) DeleteJournalAttachment(ctx context.Context, gardenID, entryID, attachmentID int) (*Response, error) {
return c.do(ctx, http.MethodDelete, journalAttachmentPath(gardenID, entryID, attachmentID), nil, nil)
}
func (c *Client) doBinary(ctx context.Context, method, path string, body io.Reader, contentType string, limit int64) (*Response, []byte, error) {
relativeURL, err := url.Parse(strings.TrimPrefix(path, "/"))
if err != nil {
return nil, nil, err
}
request, err := http.NewRequestWithContext(ctx, method, c.baseURL.ResolveReference(relativeURL).String(), body)
if err != nil {
return nil, nil, err
}
request.Header = c.headers.Clone()
request.Header.Set("Accept", "application/json, image/*, video/*, audio/*")
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
if c.bearerToken != "" {
request.Header.Set("Authorization", "Bearer "+c.bearerToken)
}
httpResponse, err := c.httpClient.Do(request)
if err != nil {
return nil, nil, fmt.Errorf("client: execute request: %w", err)
}
response := &Response{httpResponse}
defer httpResponse.Body.Close()
data, err := io.ReadAll(io.LimitReader(httpResponse.Body, limit+1))
if err != nil {
return response, nil, err
}
if int64(len(data)) > limit {
return response, nil, fmt.Errorf("client: response exceeds %d bytes", limit)
}
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
return response, nil, newAPIError(httpResponse, data)
}
return response, data, nil
}