-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
17 changed files
with
2,673 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.