Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

Commit

Permalink
feat: add pinning service support (#17)
Browse files Browse the repository at this point in the history
Initial support for pinning service API. Adds a new client method `Pin` which pins a cid. 

Also adds a number of simple happy path tests for client methods, which can be built on to cover more cases.
  • Loading branch information
iand authored Mar 7, 2022
1 parent 386d654 commit 4828c8b
Show file tree
Hide file tree
Showing 12 changed files with 601 additions and 8 deletions.
6 changes: 4 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,26 @@ type Client interface {
PutCar(context.Context, io.Reader) (cid.Cid, error)
Status(context.Context, cid.Cid) (*Status, error)
List(context.Context, ...ListOption) (*UploadIterator, error)
Pin(context.Context, cid.Cid, ...PinOption) (*PinResponse, error)
}

type clientConfig struct {
token string
endpoint string
ds ds.Batching
hc *http.Client
}

type client struct {
cfg *clientConfig
bsvc bserv.BlockService
hc *http.Client
}

// NewClient creates a new web3.storage API client.
func NewClient(options ...Option) (Client, error) {
cfg := clientConfig{
endpoint: "https://api.web3.storage",
hc: &http.Client{},
}
for _, opt := range options {
if err := opt(&cfg); err != nil {
Expand All @@ -51,7 +53,7 @@ func NewClient(options ...Option) (Client, error) {
if cfg.token == "" {
return nil, fmt.Errorf("missing auth token")
}
c := client{cfg: &cfg, hc: &http.Client{}}
c := client{cfg: &cfg}
if cfg.ds != nil {
c.bsvc = bserv.New(blockstore.NewBlockstore(cfg.ds), nil)
} else {
Expand Down
2 changes: 1 addition & 1 deletion get.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ func (c *client) Get(ctx context.Context, cid cid.Cid) (*w3http.Web3Response, er
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.cfg.token))
req.Header.Add("X-Client", clientName)
res, err := c.hc.Do(req)
res, err := c.cfg.hc.Do(req)
return w3http.NewWeb3Response(res, c.bsvc), err
}
137 changes: 137 additions & 0 deletions get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package w3s

import (
"context"
"encoding/hex"
"fmt"
"io/fs"
"net/http"
"net/http/httptest"
"net/url"
"path"
"testing"

"github.com/ipfs/go-cid"
)

const (
validToken = "validtoken"

// a car containing a single file called helloword.txt
helloRoot = "bafybeicymili4gmgoa4xpx5jfghi7leffvai4fd47f6nxgrhq4ug6ekiga"
helloCarHex = "3aa265726f6f747381d82a582500017012205862168e1986703977dfa9298e8fac852d408e147cf97cdb9a2787286f1148306776657273696f6e0162017012205862168e1986703977dfa9298e8fac852d408e147cf97cdb9a2787286f11483012380a2401551220315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3120e68656c6c6f776f726c642e747874180d0a0208013101551220315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd348656c6c6f2c20776f726c6421"

// a car containing a single file called thanks.txt
thanksRoot = "bafybeid7orcaehmy2lzlkr4wnfgexmm2xoonmamaimdsjycex7wu4pjip4"
thanksCarHex = "3aa265726f6f747381d82a582500017012207f7444021d98d2f2b54796694c4bb19abb9cd60180430724e044bfed4e3d287f6776657273696f6e015e017012207f7444021d98d2f2b54796694c4bb19abb9cd60180430724e044bfed4e3d287f12340a24015512200386a02a5f79b12d40569f36f0e3623d71f6655d00c5c0fc3826b4a945670685120a7468616e6b732e74787418170a0208013b015512200386a02a5f79b12d40569f36f0e3623d71f6655d00c5c0fc3826b4a9456706855468616e6b7320666f7220616c6c207468652066697368"
)

type routeMap map[string]map[string]http.HandlerFunc

func startTestServer(t *testing.T, routes routeMap) (*http.Client, func()) {
mux := http.NewServeMux()
for path, methodHandlers := range routes {
for method, handler := range methodHandlers {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.Method != method {
w.WriteHeader(http.StatusNotFound)
return
}
handler(w, r)
})
}
}

ts := httptest.NewServer(mux)

u, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse httptest.Server URL: %v", err)
}

hc := &http.Client{
Transport: urlRewriteTransport{URL: u},
}

return hc, func() {
ts.Close()
}
}

type urlRewriteTransport struct {
Transport http.RoundTripper
URL *url.URL
}

func (t urlRewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.URL.Scheme = t.URL.Scheme
req.URL.Host = t.URL.Host
req.URL.Path = path.Join(t.URL.Path, req.URL.Path)
rt := t.Transport
if rt == nil {
rt = http.DefaultTransport
}
return rt.RoundTrip(req)
}

var getHelloCarHandler = func(w http.ResponseWriter, r *http.Request) {
carbytes, err := hex.DecodeString(helloCarHex)
if err != nil {
fmt.Printf("DecodeString: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/car")
w.Header().Set("Content-Disposition", `attachment; filename="`+helloRoot+`.car"`)

w.WriteHeader(http.StatusOK)
w.Write(carbytes)
}

func TestGetHappyPath(t *testing.T) {
routes := routeMap{
"/car/" + helloRoot: {
http.MethodGet: getHelloCarHandler,
},
}

hc, cleanup := startTestServer(t, routes)
defer cleanup()

client, err := NewClient(WithHTTPClient(hc), WithToken("validtoken"))
if err != nil {
t.Fatalf("failed to create client: %v", err)
}

c, _ := cid.Parse(helloRoot)
resp, err := client.Get(context.Background(), c)
if err != nil {
t.Fatalf("failed to send request: %v", err)
}

if resp.StatusCode != 200 {
t.Fatalf("got status %d, wanted %d", resp.StatusCode, 200)
}

f, fsys, err := resp.Files()
if err != nil {
t.Fatalf("failed to read files: %v", err)
}

info, err := f.Stat()
if err != nil {
t.Fatalf("failed to send stat car: %v", err)
}

if !info.IsDir() {
t.Fatalf("expected a car containing a directory of files")
}
err = fs.WalkDir(fsys, "/", func(path string, d fs.DirEntry, werr error) error {
_, err := d.Info()
return err
})
if err != nil {
t.Fatalf("failed to send walk car: %v", err)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ require (
github.com/ipld/go-codec-dagpb v1.3.0
github.com/ipld/go-ipld-prime v0.14.3
github.com/libp2p/go-libp2p-core v0.11.0
github.com/multiformats/go-multiaddr v0.4.1
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
)
2 changes: 1 addition & 1 deletion list.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (c *client) List(ctx context.Context, options ...ListOption) (*UploadIterat
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.cfg.token))
req.Header.Add("Access-Control-Request-Headers", "Link")
req.Header.Add("X-Client", clientName)
res, err := c.hc.Do(req)
res, err := c.cfg.hc.Do(req)
if err != nil {
return nil, err
}
Expand Down
49 changes: 49 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package w3s

import (
"fmt"
"io/fs"
"net/http"
"time"

ds "github.com/ipfs/go-datastore"
"github.com/multiformats/go-multiaddr"
)

// Option is an option configuring a web3.storage client.
Expand Down Expand Up @@ -42,6 +45,18 @@ func WithDatastore(ds ds.Batching) Option {
}
}

// WithHTTPClient sets the HTTP client to use when making requests which allows
// timeouts and redirect behaviour to be configured. The default is to use the
// DefaultClient from the Go standard library.
func WithHTTPClient(hc *http.Client) Option {
return func(cfg *clientConfig) error {
if hc != nil {
cfg.hc = hc
}
return nil
}
}

// PutOption is an option configuring a call to Put.
type PutOption func(cfg *putConfig) error

Expand Down Expand Up @@ -85,3 +100,37 @@ func WithMaxResults(maxResults int) ListOption {
return nil
}
}

// PinOption is an option configuring a call to Pin.
type PinOption func(cfg *pinConfig) error

// WithPinName sets the name to use for the pinned data.
func WithPinName(name string) PinOption {
return func(cfg *pinConfig) error {
cfg.name = name
return nil
}
}

// WithPinOrigin adds a multiaddr known to provide the data.
func WithPinOrigin(ma string) PinOption {
return func(cfg *pinConfig) error {
_, err := multiaddr.NewMultiaddr(ma)
if err != nil {
return fmt.Errorf("origin: %w", err)
}
cfg.origins = append(cfg.origins, ma)
return nil
}
}

// WithPinMeta adds metadata about pinned data.
func WithPinMeta(key, value string) PinOption {
return func(cfg *pinConfig) error {
if cfg.meta == nil {
cfg.meta = map[string]string{}
}
cfg.meta[key] = value
return nil
}
}
Loading

0 comments on commit 4828c8b

Please sign in to comment.