Skip to content

Commit

Permalink
Merge pull request ipfs/go-ipfs-http-client#1 from ipfs/feat/implement
Browse files Browse the repository at this point in the history
Initial implementation

This commit was moved from ipfs/go-ipfs-http-client@449b614
  • Loading branch information
magik6k authored Feb 23, 2019
2 parents 35c271e + 47b8201 commit 4b30924
Show file tree
Hide file tree
Showing 17 changed files with 2,673 additions and 0 deletions.
183 changes: 183 additions & 0 deletions client/httpapi/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package httpapi

import (
"fmt"
"io/ioutil"
gohttp "net/http"
"os"
"path"
"strings"

iface "github.com/ipfs/interface-go-ipfs-core"
caopts "github.com/ipfs/interface-go-ipfs-core/options"
homedir "github.com/mitchellh/go-homedir"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr-net"
)

const (
DefaultPathName = ".ipfs"
DefaultPathRoot = "~/" + DefaultPathName
DefaultApiFile = "api"
EnvDir = "IPFS_PATH"
)

// HttpApi implements github.com/ipfs/interface-go-ipfs-core/CoreAPI using
// IPFS HTTP API.
//
// For interface docs see
// https://godoc.org/github.com/ipfs/interface-go-ipfs-core#CoreAPI
type HttpApi struct {
url string
httpcli gohttp.Client

applyGlobal func(*RequestBuilder)
}

// NewLocalApi tries to construct new HttpApi instance communicating with local
// IPFS daemon
//
// Daemon api address is pulled from the $IPFS_PATH/api file.
// If $IPFS_PATH env var is not present, it defaults to ~/.ipfs
func NewLocalApi() (iface.CoreAPI, error) {
baseDir := os.Getenv(EnvDir)
if baseDir == "" {
baseDir = DefaultPathRoot
}

return NewPathApi(baseDir)
}

// NewPathApi constructs new HttpApi by pulling api address from specified
// ipfspath. Api file should be located at $ipfspath/api
func NewPathApi(ipfspath string) (iface.CoreAPI, error) {
a, err := ApiAddr(ipfspath)
if err != nil {
if os.IsNotExist(err) {
err = nil
}
return nil, err
}
return NewApi(a)
}

// ApiAddr reads api file in specified ipfs path
func ApiAddr(ipfspath string) (ma.Multiaddr, error) {
baseDir, err := homedir.Expand(ipfspath)
if err != nil {
return nil, err
}

apiFile := path.Join(baseDir, DefaultApiFile)

api, err := ioutil.ReadFile(apiFile)
if err != nil {
return nil, err
}

return ma.NewMultiaddr(strings.TrimSpace(string(api)))
}

// NewApi constructs HttpApi with specified endpoint
func NewApi(a ma.Multiaddr) (*HttpApi, error) {
c := &gohttp.Client{
Transport: &gohttp.Transport{
Proxy: gohttp.ProxyFromEnvironment,
DisableKeepAlives: true,
},
}

return NewApiWithClient(a, c)
}

// NewApiWithClient constructs HttpApi with specified endpoint and custom http client
func NewApiWithClient(a ma.Multiaddr, c *gohttp.Client) (*HttpApi, error) {
_, url, err := manet.DialArgs(a)
if err != nil {
return nil, err
}

if a, err := ma.NewMultiaddr(url); err == nil {
_, host, err := manet.DialArgs(a)
if err == nil {
url = host
}
}

api := &HttpApi{
url: url,
httpcli: *c,
applyGlobal: func(*RequestBuilder) {},
}

// We don't support redirects.
api.httpcli.CheckRedirect = func(_ *gohttp.Request, _ []*gohttp.Request) error {
return fmt.Errorf("unexpected redirect")
}

return api, nil
}

func (api *HttpApi) WithOptions(opts ...caopts.ApiOption) (iface.CoreAPI, error) {
options, err := caopts.ApiOptions(opts...)
if err != nil {
return nil, err
}

subApi := *api
subApi.applyGlobal = func(req *RequestBuilder) {
if options.Offline {
req.Option("offline", options.Offline)
}
}

return &subApi, nil
}

func (api *HttpApi) request(command string, args ...string) *RequestBuilder {
return &RequestBuilder{
command: command,
args: args,
shell: api,
}
}

func (api *HttpApi) Unixfs() iface.UnixfsAPI {
return (*UnixfsAPI)(api)
}

func (api *HttpApi) Block() iface.BlockAPI {
return (*BlockAPI)(api)
}

func (api *HttpApi) Dag() iface.APIDagService {
return (*HttpDagServ)(api)
}

func (api *HttpApi) Name() iface.NameAPI {
return (*NameAPI)(api)
}

func (api *HttpApi) Key() iface.KeyAPI {
return (*KeyAPI)(api)
}

func (api *HttpApi) Pin() iface.PinAPI {
return (*PinAPI)(api)
}

func (api *HttpApi) Object() iface.ObjectAPI {
return (*ObjectAPI)(api)
}

func (api *HttpApi) Dht() iface.DhtAPI {
return (*DhtAPI)(api)
}

func (api *HttpApi) Swarm() iface.SwarmAPI {
return (*SwarmAPI)(api)
}

func (api *HttpApi) PubSub() iface.PubSubAPI {
return (*PubsubAPI)(api)
}
213 changes: 213 additions & 0 deletions client/httpapi/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package httpapi

import (
"context"
"io/ioutil"
gohttp "net/http"
"os"
"strconv"
"sync"
"testing"

"github.com/ipfs/interface-go-ipfs-core"
"github.com/ipfs/interface-go-ipfs-core/tests"
local "github.com/ipfs/iptb-plugins/local"
"github.com/ipfs/iptb/testbed"
"github.com/ipfs/iptb/testbed/interfaces"
ma "github.com/multiformats/go-multiaddr"
)

const parallelSpeculativeNodes = 15 // 15 seems to work best

func init() {
_, err := testbed.RegisterPlugin(testbed.IptbPlugin{
From: "<builtin>",
NewNode: local.NewNode,
GetAttrList: local.GetAttrList,
GetAttrDesc: local.GetAttrDesc,
PluginName: local.PluginName,
BuiltIn: true,
}, false)
if err != nil {
panic(err)
}
}

type NodeProvider struct {
simple <-chan func(context.Context) ([]iface.CoreAPI, error)
}

func newNodeProvider(ctx context.Context) *NodeProvider {
simpleNodes := make(chan func(context.Context) ([]iface.CoreAPI, error), parallelSpeculativeNodes)

np := &NodeProvider{
simple: simpleNodes,
}

// start basic nodes speculatively in parallel
for i := 0; i < parallelSpeculativeNodes; i++ {
go func() {
for {
ctx, cancel := context.WithCancel(ctx)

snd, err := np.makeAPISwarm(ctx, false, 1)

res := func(ctx context.Context) ([]iface.CoreAPI, error) {
if err != nil {
return nil, err
}

go func() {
<-ctx.Done()
cancel()
}()

return snd, nil
}

select {
case simpleNodes <- res:
case <-ctx.Done():
return
}
}
}()
}

return np
}

func (np *NodeProvider) MakeAPISwarm(ctx context.Context, fullIdentity bool, n int) ([]iface.CoreAPI, error) {
if !fullIdentity && n == 1 {
return (<-np.simple)(ctx)
}
return np.makeAPISwarm(ctx, fullIdentity, n)
}

func (NodeProvider) makeAPISwarm(ctx context.Context, fullIdentity bool, n int) ([]iface.CoreAPI, error) {

dir, err := ioutil.TempDir("", "httpapi-tb-")
if err != nil {
return nil, err
}

tb := testbed.NewTestbed(dir)

specs, err := testbed.BuildSpecs(tb.Dir(), n, "localipfs", nil)
if err != nil {
return nil, err
}

if err := testbed.WriteNodeSpecs(tb.Dir(), specs); err != nil {
return nil, err
}

nodes, err := tb.Nodes()
if err != nil {
return nil, err
}

apis := make([]iface.CoreAPI, n)

wg := sync.WaitGroup{}
zero := sync.WaitGroup{}

wg.Add(len(nodes))
zero.Add(1)
errs := make(chan error, len(nodes))

for i, nd := range nodes {
go func(i int, nd testbedi.Core) {
defer wg.Done()

if _, err := nd.Init(ctx, "--empty-repo"); err != nil {
errs <- err
return
}

if _, err := nd.RunCmd(ctx, nil, "ipfs", "config", "--json", "Experimental.FilestoreEnabled", "true"); err != nil {
errs <- err
return
}

if _, err := nd.Start(ctx, true, "--enable-pubsub-experiment", "--offline="+strconv.FormatBool(n == 1)); err != nil {
errs <- err
return
}

if i > 0 {
zero.Wait()
if err := nd.Connect(ctx, nodes[0]); err != nil {
errs <- err
return
}
} else {
zero.Done()
}

addr, err := nd.APIAddr()
if err != nil {
errs <- err
return
}

maddr, err := ma.NewMultiaddr(addr)
if err != nil {
errs <- err
return
}

c := &gohttp.Client{
Transport: &gohttp.Transport{
Proxy: gohttp.ProxyFromEnvironment,
DisableKeepAlives: true,
DisableCompression: true,
},
}
apis[i], err = NewApiWithClient(maddr, c)
if err != nil {
errs <- err
return
}

// empty node is pinned even with --empty-repo, we don't want that
emptyNode, err := iface.ParsePath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
if err != nil {
errs <- err
return
}
if err := apis[i].Pin().Rm(ctx, emptyNode); err != nil {
errs <- err
return
}
}(i, nd)
}

wg.Wait()

go func() {
<-ctx.Done()

defer os.Remove(dir)

defer func() {
for _, nd := range nodes {
_ = nd.Stop(context.Background())
}
}()
}()

select {
case err = <-errs:
default:
}

return apis, err
}

func TestHttpApi(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tests.TestApi(newNodeProvider(ctx))(t)
}
Loading

0 comments on commit 4b30924

Please sign in to comment.