ipfs-cluster/api/pinsvcapi/pinsvc/pinsvc.go
Hector Sanjuan a97ed10d0b Adopt api.Cid type - replaces cid.Cid everwhere.
This commit introduces an api.Cid type and replaces the usage of cid.Cid
everywhere.

The main motivation here is to override MarshalJSON so that Cids are
JSON-ified as '"Qm...."' instead of '{ "/": "Qm....." }', as this "ipld"
representation of IDs is horrible to work with, and our APIs are not issuing
IPLD objects to start with.

Unfortunately, there is no way to do this cleanly, and the best way is to just
switch everything to our own type.
2022-04-07 14:27:39 +02:00

307 lines
7.6 KiB
Go

// Package pinsvc contains type definitions for the Pinning Services API
package pinsvc
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
types "github.com/ipfs/ipfs-cluster/api"
)
func init() {
// intialize trackerStatusString
stringStatus = make(map[string]Status)
for k, v := range statusString {
stringStatus[v] = k
}
}
// APIError is returned by the API as a body when an error
// occurs. It implements the error interface.
type APIError struct {
Reason string `json:"reason"`
Details string `json:"details"`
}
func (apiErr APIError) Error() string {
return apiErr.Reason
}
// PinName is a string limited to 255 chars when serializing JSON.
type PinName string
// MarshalJSON converts the string to JSON.
func (pname PinName) MarshalJSON() ([]byte, error) {
return json.Marshal(string(pname))
}
// UnmarshalJSON reads the JSON string and errors if over 256 chars.
func (pname *PinName) UnmarshalJSON(data []byte) error {
if len(data) > 257 { // "a_string" 255 + 2 for quotes
return errors.New("pin name is over 255 chars")
}
var v string
err := json.Unmarshal(data, &v)
*pname = PinName(v)
return err
}
// Pin contains basic information about a Pin and pinning options.
type Pin struct {
Cid types.Cid `json:"cid"`
Name PinName `json:"name"`
Origins []types.Multiaddr `json:"origins"`
Meta map[string]string `json:"meta"`
}
// Defined returns if the pinis empty (Cid not set).
func (p Pin) Defined() bool {
return p.Cid.Defined()
}
// MatchesName returns in a pin status matches a name option with a given
// match strategy.
func (p Pin) MatchesName(nameOpt string, strategy MatchingStrategy) bool {
if nameOpt == "" {
return true
}
name := string(p.Name)
switch strategy {
case MatchingStrategyUndefined:
return true
case MatchingStrategyExact:
return nameOpt == name
case MatchingStrategyIexact:
return strings.EqualFold(name, nameOpt)
case MatchingStrategyPartial:
return strings.Contains(name, nameOpt)
case MatchingStrategyIpartial:
return strings.Contains(strings.ToLower(name), strings.ToLower(nameOpt))
default:
return true
}
}
// MatchesMeta returns true if the pin status metadata matches the given. The
// metadata should have all the keys in the given metaOpts and the values
// should, be the same (metadata map includes metaOpts).
func (p Pin) MatchesMeta(metaOpts map[string]string) bool {
for k, v := range metaOpts {
if p.Meta[k] != v {
return false
}
}
return true
}
// Status represents a pin status, which defines the current state of the pin
// in the system.
type Status int
// Values for the Status type.
const (
StatusUndefined Status = 0
StatusQueued = 1 << iota
StatusPinned
StatusPinning
StatusFailed
)
var statusString = map[Status]string{
StatusUndefined: "undefined",
StatusQueued: "queued",
StatusPinned: "pinned",
StatusPinning: "pinning",
StatusFailed: "failed",
}
// values autofilled in init()
var stringStatus map[string]Status
// String converts a Status into a readable string.
// If the given Status is a filter (with several
// bits set), it will return a comma-separated list.
func (st Status) String() string {
var values []string
// simple and known composite values
if v, ok := statusString[st]; ok {
return v
}
// other filters
for k, v := range statusString {
if st&k > 0 {
values = append(values, v)
}
}
return strings.Join(values, ",")
}
// Match returns true if the tracker status matches the given filter.
func (st Status) Match(filter Status) bool {
return filter == StatusUndefined ||
st == StatusUndefined ||
st&filter > 0
}
// MarshalJSON uses the string representation of Status for JSON
// encoding.
func (st Status) MarshalJSON() ([]byte, error) {
return json.Marshal(st.String())
}
// UnmarshalJSON sets a tracker status from its JSON representation.
func (st *Status) UnmarshalJSON(data []byte) error {
var v string
err := json.Unmarshal(data, &v)
if err != nil {
return err
}
*st = StatusFromString(v)
return nil
}
// StatusFromString parses a string and returns the matching
// Status value. The string can be a comma-separated list
// representing a Status filter. Unknown status names are
// ignored.
func StatusFromString(str string) Status {
values := strings.Split(strings.Replace(str, " ", "", -1), ",")
status := StatusUndefined
for _, v := range values {
st, ok := stringStatus[v]
if ok {
status |= st
}
}
return status
}
// MatchingStrategy defines a type of match for filtering pin lists.
type MatchingStrategy int
// Values for MatchingStrategy.
const (
MatchingStrategyUndefined MatchingStrategy = iota
MatchingStrategyExact
MatchingStrategyIexact
MatchingStrategyPartial
MatchingStrategyIpartial
)
// MatchingStrategyFromString converts a string to its MatchingStrategy value.
func MatchingStrategyFromString(str string) MatchingStrategy {
switch str {
case "exact":
return MatchingStrategyExact
case "iexact":
return MatchingStrategyIexact
case "partial":
return MatchingStrategyPartial
case "ipartial":
return MatchingStrategyIpartial
default:
return MatchingStrategyUndefined
}
}
// PinStatus provides information about a Pin stored by the Pinning API.
type PinStatus struct {
RequestID string `json:"requestid"`
Status Status `json:"status"`
Created time.Time `json:"created"`
Pin Pin `json:"pin"`
Delegates []types.Multiaddr `json:"delegates"`
Info map[string]string `json:"info"`
}
// PinList is the result of a call to List pins
type PinList struct {
Count int `json:"count"`
Results []PinStatus `json:"results"`
}
// ListOptions represents possible options given to the List endpoint.
type ListOptions struct {
Cids []types.Cid
Name string
MatchingStrategy MatchingStrategy
Status Status
Before time.Time
After time.Time
Limit int
Meta map[string]string
}
// FromQuery parses ListOptions from url.Values.
func (lo *ListOptions) FromQuery(q url.Values) error {
cidq := q.Get("cid")
if len(cidq) > 0 {
for _, cstr := range strings.Split(cidq, ",") {
c, err := types.DecodeCid(cstr)
if err != nil {
return fmt.Errorf("error decoding cid %s: %w", cstr, err)
}
lo.Cids = append(lo.Cids, c)
}
}
n := q.Get("name")
if len(n) > 255 {
return fmt.Errorf("error in 'name' query param: longer than 255 chars")
}
lo.Name = n
lo.MatchingStrategy = MatchingStrategyFromString(q.Get("match"))
if lo.MatchingStrategy == MatchingStrategyUndefined {
lo.MatchingStrategy = MatchingStrategyExact // default
}
statusStr := q.Get("status")
lo.Status = StatusFromString(statusStr)
// FIXME: This is a bit lazy, as "invalidxx,pinned" would result in a
// valid "pinned" filter.
if statusStr != "" && lo.Status == StatusUndefined {
return fmt.Errorf("error decoding 'status' query param: no valid filter")
}
if bef := q.Get("before"); bef != "" {
err := lo.Before.UnmarshalText([]byte(bef))
if err != nil {
return fmt.Errorf("error decoding 'before' query param: %s: %w", bef, err)
}
}
if after := q.Get("after"); after != "" {
err := lo.After.UnmarshalText([]byte(after))
if err != nil {
return fmt.Errorf("error decoding 'after' query param: %s: %w", after, err)
}
}
if v := q.Get("limit"); v != "" {
lim, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return fmt.Errorf("error parsing 'limit' query param: %s: %w", v, err)
}
lo.Limit = int(lim)
}
if meta := q.Get("meta"); meta != "" {
err := json.Unmarshal([]byte(meta), &lo.Meta)
if err != nil {
return fmt.Errorf("error unmarshalling 'meta' query param: %s: %w", meta, err)
}
}
return nil
}