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

Commit 4828c8b

Browse files
authored
feat: add pinning service support (#17)
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.
1 parent 386d654 commit 4828c8b

12 files changed

+601
-8
lines changed

Diff for: client.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,26 @@ type Client interface {
2424
PutCar(context.Context, io.Reader) (cid.Cid, error)
2525
Status(context.Context, cid.Cid) (*Status, error)
2626
List(context.Context, ...ListOption) (*UploadIterator, error)
27+
Pin(context.Context, cid.Cid, ...PinOption) (*PinResponse, error)
2728
}
2829

2930
type clientConfig struct {
3031
token string
3132
endpoint string
3233
ds ds.Batching
34+
hc *http.Client
3335
}
3436

3537
type client struct {
3638
cfg *clientConfig
3739
bsvc bserv.BlockService
38-
hc *http.Client
3940
}
4041

4142
// NewClient creates a new web3.storage API client.
4243
func NewClient(options ...Option) (Client, error) {
4344
cfg := clientConfig{
4445
endpoint: "https://api.web3.storage",
46+
hc: &http.Client{},
4547
}
4648
for _, opt := range options {
4749
if err := opt(&cfg); err != nil {
@@ -51,7 +53,7 @@ func NewClient(options ...Option) (Client, error) {
5153
if cfg.token == "" {
5254
return nil, fmt.Errorf("missing auth token")
5355
}
54-
c := client{cfg: &cfg, hc: &http.Client{}}
56+
c := client{cfg: &cfg}
5557
if cfg.ds != nil {
5658
c.bsvc = bserv.New(blockstore.NewBlockstore(cfg.ds), nil)
5759
} else {

Diff for: get.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ func (c *client) Get(ctx context.Context, cid cid.Cid) (*w3http.Web3Response, er
1616
}
1717
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.cfg.token))
1818
req.Header.Add("X-Client", clientName)
19-
res, err := c.hc.Do(req)
19+
res, err := c.cfg.hc.Do(req)
2020
return w3http.NewWeb3Response(res, c.bsvc), err
2121
}

Diff for: get_test.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package w3s
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
"io/fs"
8+
"net/http"
9+
"net/http/httptest"
10+
"net/url"
11+
"path"
12+
"testing"
13+
14+
"github.com/ipfs/go-cid"
15+
)
16+
17+
const (
18+
validToken = "validtoken"
19+
20+
// a car containing a single file called helloword.txt
21+
helloRoot = "bafybeicymili4gmgoa4xpx5jfghi7leffvai4fd47f6nxgrhq4ug6ekiga"
22+
helloCarHex = "3aa265726f6f747381d82a582500017012205862168e1986703977dfa9298e8fac852d408e147cf97cdb9a2787286f1148306776657273696f6e0162017012205862168e1986703977dfa9298e8fac852d408e147cf97cdb9a2787286f11483012380a2401551220315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3120e68656c6c6f776f726c642e747874180d0a0208013101551220315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd348656c6c6f2c20776f726c6421"
23+
24+
// a car containing a single file called thanks.txt
25+
thanksRoot = "bafybeid7orcaehmy2lzlkr4wnfgexmm2xoonmamaimdsjycex7wu4pjip4"
26+
thanksCarHex = "3aa265726f6f747381d82a582500017012207f7444021d98d2f2b54796694c4bb19abb9cd60180430724e044bfed4e3d287f6776657273696f6e015e017012207f7444021d98d2f2b54796694c4bb19abb9cd60180430724e044bfed4e3d287f12340a24015512200386a02a5f79b12d40569f36f0e3623d71f6655d00c5c0fc3826b4a945670685120a7468616e6b732e74787418170a0208013b015512200386a02a5f79b12d40569f36f0e3623d71f6655d00c5c0fc3826b4a9456706855468616e6b7320666f7220616c6c207468652066697368"
27+
)
28+
29+
type routeMap map[string]map[string]http.HandlerFunc
30+
31+
func startTestServer(t *testing.T, routes routeMap) (*http.Client, func()) {
32+
mux := http.NewServeMux()
33+
for path, methodHandlers := range routes {
34+
for method, handler := range methodHandlers {
35+
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
36+
if r.Method != method {
37+
w.WriteHeader(http.StatusNotFound)
38+
return
39+
}
40+
handler(w, r)
41+
})
42+
}
43+
}
44+
45+
ts := httptest.NewServer(mux)
46+
47+
u, err := url.Parse(ts.URL)
48+
if err != nil {
49+
t.Fatalf("failed to parse httptest.Server URL: %v", err)
50+
}
51+
52+
hc := &http.Client{
53+
Transport: urlRewriteTransport{URL: u},
54+
}
55+
56+
return hc, func() {
57+
ts.Close()
58+
}
59+
}
60+
61+
type urlRewriteTransport struct {
62+
Transport http.RoundTripper
63+
URL *url.URL
64+
}
65+
66+
func (t urlRewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
67+
req.URL.Scheme = t.URL.Scheme
68+
req.URL.Host = t.URL.Host
69+
req.URL.Path = path.Join(t.URL.Path, req.URL.Path)
70+
rt := t.Transport
71+
if rt == nil {
72+
rt = http.DefaultTransport
73+
}
74+
return rt.RoundTrip(req)
75+
}
76+
77+
var getHelloCarHandler = func(w http.ResponseWriter, r *http.Request) {
78+
carbytes, err := hex.DecodeString(helloCarHex)
79+
if err != nil {
80+
fmt.Printf("DecodeString: %v\n", err)
81+
w.WriteHeader(http.StatusInternalServerError)
82+
return
83+
}
84+
85+
w.Header().Set("Content-Type", "application/car")
86+
w.Header().Set("Content-Disposition", `attachment; filename="`+helloRoot+`.car"`)
87+
88+
w.WriteHeader(http.StatusOK)
89+
w.Write(carbytes)
90+
}
91+
92+
func TestGetHappyPath(t *testing.T) {
93+
routes := routeMap{
94+
"/car/" + helloRoot: {
95+
http.MethodGet: getHelloCarHandler,
96+
},
97+
}
98+
99+
hc, cleanup := startTestServer(t, routes)
100+
defer cleanup()
101+
102+
client, err := NewClient(WithHTTPClient(hc), WithToken("validtoken"))
103+
if err != nil {
104+
t.Fatalf("failed to create client: %v", err)
105+
}
106+
107+
c, _ := cid.Parse(helloRoot)
108+
resp, err := client.Get(context.Background(), c)
109+
if err != nil {
110+
t.Fatalf("failed to send request: %v", err)
111+
}
112+
113+
if resp.StatusCode != 200 {
114+
t.Fatalf("got status %d, wanted %d", resp.StatusCode, 200)
115+
}
116+
117+
f, fsys, err := resp.Files()
118+
if err != nil {
119+
t.Fatalf("failed to read files: %v", err)
120+
}
121+
122+
info, err := f.Stat()
123+
if err != nil {
124+
t.Fatalf("failed to send stat car: %v", err)
125+
}
126+
127+
if !info.IsDir() {
128+
t.Fatalf("expected a car containing a directory of files")
129+
}
130+
err = fs.WalkDir(fsys, "/", func(path string, d fs.DirEntry, werr error) error {
131+
_, err := d.Info()
132+
return err
133+
})
134+
if err != nil {
135+
t.Fatalf("failed to send walk car: %v", err)
136+
}
137+
}

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ require (
2424
github.com/ipld/go-codec-dagpb v1.3.0
2525
github.com/ipld/go-ipld-prime v0.14.3
2626
github.com/libp2p/go-libp2p-core v0.11.0
27+
github.com/multiformats/go-multiaddr v0.4.1
2728
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
2829
)

Diff for: list.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (c *client) List(ctx context.Context, options ...ListOption) (*UploadIterat
7373
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.cfg.token))
7474
req.Header.Add("Access-Control-Request-Headers", "Link")
7575
req.Header.Add("X-Client", clientName)
76-
res, err := c.hc.Do(req)
76+
res, err := c.cfg.hc.Do(req)
7777
if err != nil {
7878
return nil, err
7979
}

Diff for: opts.go

+49
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package w3s
22

33
import (
4+
"fmt"
45
"io/fs"
6+
"net/http"
57
"time"
68

79
ds "github.com/ipfs/go-datastore"
10+
"github.com/multiformats/go-multiaddr"
811
)
912

1013
// Option is an option configuring a web3.storage client.
@@ -42,6 +45,18 @@ func WithDatastore(ds ds.Batching) Option {
4245
}
4346
}
4447

48+
// WithHTTPClient sets the HTTP client to use when making requests which allows
49+
// timeouts and redirect behaviour to be configured. The default is to use the
50+
// DefaultClient from the Go standard library.
51+
func WithHTTPClient(hc *http.Client) Option {
52+
return func(cfg *clientConfig) error {
53+
if hc != nil {
54+
cfg.hc = hc
55+
}
56+
return nil
57+
}
58+
}
59+
4560
// PutOption is an option configuring a call to Put.
4661
type PutOption func(cfg *putConfig) error
4762

@@ -85,3 +100,37 @@ func WithMaxResults(maxResults int) ListOption {
85100
return nil
86101
}
87102
}
103+
104+
// PinOption is an option configuring a call to Pin.
105+
type PinOption func(cfg *pinConfig) error
106+
107+
// WithPinName sets the name to use for the pinned data.
108+
func WithPinName(name string) PinOption {
109+
return func(cfg *pinConfig) error {
110+
cfg.name = name
111+
return nil
112+
}
113+
}
114+
115+
// WithPinOrigin adds a multiaddr known to provide the data.
116+
func WithPinOrigin(ma string) PinOption {
117+
return func(cfg *pinConfig) error {
118+
_, err := multiaddr.NewMultiaddr(ma)
119+
if err != nil {
120+
return fmt.Errorf("origin: %w", err)
121+
}
122+
cfg.origins = append(cfg.origins, ma)
123+
return nil
124+
}
125+
}
126+
127+
// WithPinMeta adds metadata about pinned data.
128+
func WithPinMeta(key, value string) PinOption {
129+
return func(cfg *pinConfig) error {
130+
if cfg.meta == nil {
131+
cfg.meta = map[string]string{}
132+
}
133+
cfg.meta[key] = value
134+
return nil
135+
}
136+
}

0 commit comments

Comments
 (0)