189 lines
7.3 KiB
Go
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
|
|
}
|