A Go server that takes product uploads with hub upload tokens, checks them by t5de-api's pack rules, stores files by sha256, registers revisions with the hub (POST /api/v1/catalog/revisions), and serves files with immutable caching; files used only by adult revisions need an adult access token. Contract: t5de-hub's docs/products.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
8742829e77
20 files changed
+2348
No files matched your search
@@ -0,0 +1,113 @@
|
||||
// Package hub is the catalog's client for T5DE's hub (server to server, docs/products.md "For catalogs").
|
||||
package hub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.t5de.dev/t5de/t5de-catalog/internal/store"
|
||||
)
|
||||
|
||||
// Client calls the hub as one catalog.
|
||||
type Client struct {
|
||||
url string
|
||||
auth string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// New returns a client for the hub at hubURL (its API), as catalog id with its key. client may be nil.
|
||||
func New(hubURL, id, key string, client *http.Client) *Client {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 60 * time.Second}
|
||||
}
|
||||
return &Client{url: strings.TrimRight(hubURL, "/"), auth: "Catalog " + id + ":" + key, client: client}
|
||||
}
|
||||
|
||||
// NewRevision is what the catalog registers.
|
||||
type NewRevision struct {
|
||||
UploadToken string `json:"uploadToken"`
|
||||
Files []store.File `json:"files"`
|
||||
Rating string `json:"rating"`
|
||||
Parent int64 `json:"parent"`
|
||||
Name string `json:"name"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// Registered is the hub's answer.
|
||||
type Registered struct {
|
||||
Product string `json:"product"`
|
||||
Revision int `json:"revision"`
|
||||
}
|
||||
|
||||
// Error is the hub refusing a request: its status and, when it sent them, its error code and message.
|
||||
type Error struct {
|
||||
Status int
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.Code != "" {
|
||||
return fmt.Sprintf("hub: %d %s: %s", e.Status, e.Code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("hub: %d", e.Status)
|
||||
}
|
||||
|
||||
var productID = regexp.MustCompile(`^[a-z2-7]{10}$`)
|
||||
|
||||
// ValidProduct reports whether s is a product ID (10 of a-z2-7).
|
||||
func ValidProduct(s string) bool { return productID.MatchString(s) }
|
||||
|
||||
// RegisterRevision posts a revision to the hub (POST /api/v1/catalog/revisions). A refusal is an *Error; other
|
||||
// errors mean the hub couldn't be reached or answered nonsense.
|
||||
func (c *Client) RegisterRevision(ctx context.Context, r NewRevision) (*Registered, error) {
|
||||
if r.Files == nil {
|
||||
r.Files = []store.File{}
|
||||
}
|
||||
body, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url+"/api/v1/catalog/revisions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", c.auth)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
e := &Error{Status: resp.StatusCode}
|
||||
var msg struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if json.Unmarshal(data, &msg) == nil {
|
||||
e.Code, e.Message = msg.Error, msg.Message
|
||||
}
|
||||
return nil, e
|
||||
}
|
||||
var reg Registered
|
||||
if err := json.Unmarshal(data, ®); err != nil {
|
||||
return nil, fmt.Errorf("hub: bad answer: %v", err)
|
||||
}
|
||||
if !ValidProduct(reg.Product) || reg.Revision < 1 {
|
||||
return nil, fmt.Errorf("hub: bad answer: product %q, revision %d", reg.Product, reg.Revision)
|
||||
}
|
||||
return ®, nil
|
||||
}
|
||||
Reference in new issue
Block a user