Skip to content

Commit 4b30924

Browse files
authored
Merge pull request ipfs/go-ipfs-http-client#1 from ipfs/feat/implement
Initial implementation This commit was moved from ipfs/go-ipfs-http-client@449b614
2 parents 35c271e + 47b8201 commit 4b30924

File tree

17 files changed

+2673
-0
lines changed

17 files changed

+2673
-0
lines changed

client/httpapi/api.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package httpapi
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
gohttp "net/http"
7+
"os"
8+
"path"
9+
"strings"
10+
11+
iface "github.com/ipfs/interface-go-ipfs-core"
12+
caopts "github.com/ipfs/interface-go-ipfs-core/options"
13+
homedir "github.com/mitchellh/go-homedir"
14+
ma "github.com/multiformats/go-multiaddr"
15+
manet "github.com/multiformats/go-multiaddr-net"
16+
)
17+
18+
const (
19+
DefaultPathName = ".ipfs"
20+
DefaultPathRoot = "~/" + DefaultPathName
21+
DefaultApiFile = "api"
22+
EnvDir = "IPFS_PATH"
23+
)
24+
25+
// HttpApi implements github.com/ipfs/interface-go-ipfs-core/CoreAPI using
26+
// IPFS HTTP API.
27+
//
28+
// For interface docs see
29+
// https://godoc.org/github.com/ipfs/interface-go-ipfs-core#CoreAPI
30+
type HttpApi struct {
31+
url string
32+
httpcli gohttp.Client
33+
34+
applyGlobal func(*RequestBuilder)
35+
}
36+
37+
// NewLocalApi tries to construct new HttpApi instance communicating with local
38+
// IPFS daemon
39+
//
40+
// Daemon api address is pulled from the $IPFS_PATH/api file.
41+
// If $IPFS_PATH env var is not present, it defaults to ~/.ipfs
42+
func NewLocalApi() (iface.CoreAPI, error) {
43+
baseDir := os.Getenv(EnvDir)
44+
if baseDir == "" {
45+
baseDir = DefaultPathRoot
46+
}
47+
48+
return NewPathApi(baseDir)
49+
}
50+
51+
// NewPathApi constructs new HttpApi by pulling api address from specified
52+
// ipfspath. Api file should be located at $ipfspath/api
53+
func NewPathApi(ipfspath string) (iface.CoreAPI, error) {
54+
a, err := ApiAddr(ipfspath)
55+
if err != nil {
56+
if os.IsNotExist(err) {
57+
err = nil
58+
}
59+
return nil, err
60+
}
61+
return NewApi(a)
62+
}
63+
64+
// ApiAddr reads api file in specified ipfs path
65+
func ApiAddr(ipfspath string) (ma.Multiaddr, error) {
66+
baseDir, err := homedir.Expand(ipfspath)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
apiFile := path.Join(baseDir, DefaultApiFile)
72+
73+
api, err := ioutil.ReadFile(apiFile)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return ma.NewMultiaddr(strings.TrimSpace(string(api)))
79+
}
80+
81+
// NewApi constructs HttpApi with specified endpoint
82+
func NewApi(a ma.Multiaddr) (*HttpApi, error) {
83+
c := &gohttp.Client{
84+
Transport: &gohttp.Transport{
85+
Proxy: gohttp.ProxyFromEnvironment,
86+
DisableKeepAlives: true,
87+
},
88+
}
89+
90+
return NewApiWithClient(a, c)
91+
}
92+
93+
// NewApiWithClient constructs HttpApi with specified endpoint and custom http client
94+
func NewApiWithClient(a ma.Multiaddr, c *gohttp.Client) (*HttpApi, error) {
95+
_, url, err := manet.DialArgs(a)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
if a, err := ma.NewMultiaddr(url); err == nil {
101+
_, host, err := manet.DialArgs(a)
102+
if err == nil {
103+
url = host
104+
}
105+
}
106+
107+
api := &HttpApi{
108+
url: url,
109+
httpcli: *c,
110+
applyGlobal: func(*RequestBuilder) {},
111+
}
112+
113+
// We don't support redirects.
114+
api.httpcli.CheckRedirect = func(_ *gohttp.Request, _ []*gohttp.Request) error {
115+
return fmt.Errorf("unexpected redirect")
116+
}
117+
118+
return api, nil
119+
}
120+
121+
func (api *HttpApi) WithOptions(opts ...caopts.ApiOption) (iface.CoreAPI, error) {
122+
options, err := caopts.ApiOptions(opts...)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
subApi := *api
128+
subApi.applyGlobal = func(req *RequestBuilder) {
129+
if options.Offline {
130+
req.Option("offline", options.Offline)
131+
}
132+
}
133+
134+
return &subApi, nil
135+
}
136+
137+
func (api *HttpApi) request(command string, args ...string) *RequestBuilder {
138+
return &RequestBuilder{
139+
command: command,
140+
args: args,
141+
shell: api,
142+
}
143+
}
144+
145+
func (api *HttpApi) Unixfs() iface.UnixfsAPI {
146+
return (*UnixfsAPI)(api)
147+
}
148+
149+
func (api *HttpApi) Block() iface.BlockAPI {
150+
return (*BlockAPI)(api)
151+
}
152+
153+
func (api *HttpApi) Dag() iface.APIDagService {
154+
return (*HttpDagServ)(api)
155+
}
156+
157+
func (api *HttpApi) Name() iface.NameAPI {
158+
return (*NameAPI)(api)
159+
}
160+
161+
func (api *HttpApi) Key() iface.KeyAPI {
162+
return (*KeyAPI)(api)
163+
}
164+
165+
func (api *HttpApi) Pin() iface.PinAPI {
166+
return (*PinAPI)(api)
167+
}
168+
169+
func (api *HttpApi) Object() iface.ObjectAPI {
170+
return (*ObjectAPI)(api)
171+
}
172+
173+
func (api *HttpApi) Dht() iface.DhtAPI {
174+
return (*DhtAPI)(api)
175+
}
176+
177+
func (api *HttpApi) Swarm() iface.SwarmAPI {
178+
return (*SwarmAPI)(api)
179+
}
180+
181+
func (api *HttpApi) PubSub() iface.PubSubAPI {
182+
return (*PubsubAPI)(api)
183+
}

client/httpapi/api_test.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package httpapi
2+
3+
import (
4+
"context"
5+
"io/ioutil"
6+
gohttp "net/http"
7+
"os"
8+
"strconv"
9+
"sync"
10+
"testing"
11+
12+
"github.com/ipfs/interface-go-ipfs-core"
13+
"github.com/ipfs/interface-go-ipfs-core/tests"
14+
local "github.com/ipfs/iptb-plugins/local"
15+
"github.com/ipfs/iptb/testbed"
16+
"github.com/ipfs/iptb/testbed/interfaces"
17+
ma "github.com/multiformats/go-multiaddr"
18+
)
19+
20+
const parallelSpeculativeNodes = 15 // 15 seems to work best
21+
22+
func init() {
23+
_, err := testbed.RegisterPlugin(testbed.IptbPlugin{
24+
From: "<builtin>",
25+
NewNode: local.NewNode,
26+
GetAttrList: local.GetAttrList,
27+
GetAttrDesc: local.GetAttrDesc,
28+
PluginName: local.PluginName,
29+
BuiltIn: true,
30+
}, false)
31+
if err != nil {
32+
panic(err)
33+
}
34+
}
35+
36+
type NodeProvider struct {
37+
simple <-chan func(context.Context) ([]iface.CoreAPI, error)
38+
}
39+
40+
func newNodeProvider(ctx context.Context) *NodeProvider {
41+
simpleNodes := make(chan func(context.Context) ([]iface.CoreAPI, error), parallelSpeculativeNodes)
42+
43+
np := &NodeProvider{
44+
simple: simpleNodes,
45+
}
46+
47+
// start basic nodes speculatively in parallel
48+
for i := 0; i < parallelSpeculativeNodes; i++ {
49+
go func() {
50+
for {
51+
ctx, cancel := context.WithCancel(ctx)
52+
53+
snd, err := np.makeAPISwarm(ctx, false, 1)
54+
55+
res := func(ctx context.Context) ([]iface.CoreAPI, error) {
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
go func() {
61+
<-ctx.Done()
62+
cancel()
63+
}()
64+
65+
return snd, nil
66+
}
67+
68+
select {
69+
case simpleNodes <- res:
70+
case <-ctx.Done():
71+
return
72+
}
73+
}
74+
}()
75+
}
76+
77+
return np
78+
}
79+
80+
func (np *NodeProvider) MakeAPISwarm(ctx context.Context, fullIdentity bool, n int) ([]iface.CoreAPI, error) {
81+
if !fullIdentity && n == 1 {
82+
return (<-np.simple)(ctx)
83+
}
84+
return np.makeAPISwarm(ctx, fullIdentity, n)
85+
}
86+
87+
func (NodeProvider) makeAPISwarm(ctx context.Context, fullIdentity bool, n int) ([]iface.CoreAPI, error) {
88+
89+
dir, err := ioutil.TempDir("", "httpapi-tb-")
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
tb := testbed.NewTestbed(dir)
95+
96+
specs, err := testbed.BuildSpecs(tb.Dir(), n, "localipfs", nil)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil {
102+
return nil, err
103+
}
104+
105+
nodes, err := tb.Nodes()
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
apis := make([]iface.CoreAPI, n)
111+
112+
wg := sync.WaitGroup{}
113+
zero := sync.WaitGroup{}
114+
115+
wg.Add(len(nodes))
116+
zero.Add(1)
117+
errs := make(chan error, len(nodes))
118+
119+
for i, nd := range nodes {
120+
go func(i int, nd testbedi.Core) {
121+
defer wg.Done()
122+
123+
if _, err := nd.Init(ctx, "--empty-repo"); err != nil {
124+
errs <- err
125+
return
126+
}
127+
128+
if _, err := nd.RunCmd(ctx, nil, "ipfs", "config", "--json", "Experimental.FilestoreEnabled", "true"); err != nil {
129+
errs <- err
130+
return
131+
}
132+
133+
if _, err := nd.Start(ctx, true, "--enable-pubsub-experiment", "--offline="+strconv.FormatBool(n == 1)); err != nil {
134+
errs <- err
135+
return
136+
}
137+
138+
if i > 0 {
139+
zero.Wait()
140+
if err := nd.Connect(ctx, nodes[0]); err != nil {
141+
errs <- err
142+
return
143+
}
144+
} else {
145+
zero.Done()
146+
}
147+
148+
addr, err := nd.APIAddr()
149+
if err != nil {
150+
errs <- err
151+
return
152+
}
153+
154+
maddr, err := ma.NewMultiaddr(addr)
155+
if err != nil {
156+
errs <- err
157+
return
158+
}
159+
160+
c := &gohttp.Client{
161+
Transport: &gohttp.Transport{
162+
Proxy: gohttp.ProxyFromEnvironment,
163+
DisableKeepAlives: true,
164+
DisableCompression: true,
165+
},
166+
}
167+
apis[i], err = NewApiWithClient(maddr, c)
168+
if err != nil {
169+
errs <- err
170+
return
171+
}
172+
173+
// empty node is pinned even with --empty-repo, we don't want that
174+
emptyNode, err := iface.ParsePath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
175+
if err != nil {
176+
errs <- err
177+
return
178+
}
179+
if err := apis[i].Pin().Rm(ctx, emptyNode); err != nil {
180+
errs <- err
181+
return
182+
}
183+
}(i, nd)
184+
}
185+
186+
wg.Wait()
187+
188+
go func() {
189+
<-ctx.Done()
190+
191+
defer os.Remove(dir)
192+
193+
defer func() {
194+
for _, nd := range nodes {
195+
_ = nd.Stop(context.Background())
196+
}
197+
}()
198+
}()
199+
200+
select {
201+
case err = <-errs:
202+
default:
203+
}
204+
205+
return apis, err
206+
}
207+
208+
func TestHttpApi(t *testing.T) {
209+
ctx, cancel := context.WithCancel(context.Background())
210+
defer cancel()
211+
212+
tests.TestApi(newNodeProvider(ctx))(t)
213+
}

0 commit comments

Comments
 (0)