Initial structure, path stuff
This commit was moved from ipfs/go-ipfs-http-client@93943f7f5671948b0d2aee19c30cf689307faf13
Łukasz Magiera committed
Nov 6, 2018 at 13:13 UTC
6d85aff407d3fb3eb6d2549fbac94615fd147b49
6 files changed
+476
client/httpapi/api.go
new
+127
@@ -0,0 +1,127 @@
1
+package httpapi
2
+
3
+import (
4
+ "github.com/pkg/errors"
5
+ "io/ioutil"
6
+ gohttp "net/http"
7
+ "os"
8
+ "path"
9
+ "strings"
10
+
11
+ "github.com/ipfs/go-ipfs/core/coreapi/interface"
12
+ homedir "github.com/mitchellh/go-homedir"
13
+ ma "github.com/multiformats/go-multiaddr"
14
+ manet "github.com/multiformats/go-multiaddr-net"
15
+ )
16
+
17
+const (
18
+ DefaultPathName = ".ipfs"
19
+ DefaultPathRoot = "~/" + DefaultPathName
20
+ DefaultApiFile = "api"
21
+ EnvDir = "IPFS_PATH"
22
+)
23
+
24
+var ErrNotImplemented = errors.New("not implemented")
25
+
26
+type HttpApi struct {
27
+ url string
28
+ httpcli *gohttp.Client
29
+}
30
+
31
+func NewLocalApi() iface.CoreAPI {
32
+ baseDir := os.Getenv(EnvDir)
33
+ if baseDir == "" {
34
+ baseDir = DefaultPathRoot
35
+ }
36
+
37
+ baseDir, err := homedir.Expand(baseDir)
38
+ if err != nil {
39
+ return nil
40
+ }
41
+
42
+ apiFile := path.Join(baseDir, DefaultApiFile)
43
+
44
+ if _, err := os.Stat(apiFile); err != nil {
45
+ return nil
46
+ }
47
+
48
+ api, err := ioutil.ReadFile(apiFile)
49
+ if err != nil {
50
+ return nil
51
+ }
52
+
53
+ return NewApi(strings.TrimSpace(string(api)))
54
+}
55
+
56
+func NewApi(url string) *HttpApi {
57
+ c := &gohttp.Client{
58
+ Transport: &gohttp.Transport{
59
+ Proxy: gohttp.ProxyFromEnvironment,
60
+ DisableKeepAlives: true,
61
+ },
62
+ }
63
+
64
+ return NewApiWithClient(url, c)
65
+}
66
+
67
+func NewApiWithClient(url string, c *gohttp.Client) *HttpApi {
68
+ if a, err := ma.NewMultiaddr(url); err == nil {
69
+ _, host, err := manet.DialArgs(a)
70
+ if err == nil {
71
+ url = host
72
+ }
73
+ }
74
+
75
+ return &HttpApi{
76
+ url: url,
77
+ httpcli: c,
78
+ }
79
+}
80
+
81
+func (api *HttpApi) request(command string, args ...string) *RequestBuilder {
82
+ return &RequestBuilder{
83
+ command: command,
84
+ args: args,
85
+ shell: api,
86
+ }
87
+}
88
+
89
+func (api *HttpApi) Unixfs() iface.UnixfsAPI {
90
+ return nil
91
+}
92
+
93
+func (api *HttpApi) Block() iface.BlockAPI {
94
+ return nil
95
+}
96
+
97
+func (api *HttpApi) Dag() iface.DagAPI {
98
+ return nil
99
+}
100
+
101
+func (api *HttpApi) Name() iface.NameAPI {
102
+ return (*NameAPI)(api)
103
+}
104
+
105
+func (api *HttpApi) Key() iface.KeyAPI {
106
+ return nil
107
+}
108
+
109
+func (api *HttpApi) Pin() iface.PinAPI {
110
+ return nil
111
+}
112
+
113
+func (api *HttpApi) Object() iface.ObjectAPI {
114
+ return nil
115
+}
116
+
117
+func (api *HttpApi) Dht() iface.DhtAPI {
118
+ return nil
119
+}
120
+
121
+func (api *HttpApi) Swarm() iface.SwarmAPI {
122
+ return nil
123
+}
124
+
125
+func (api *HttpApi) PubSub() iface.PubSubAPI {
126
+ return nil
127
+}
client/httpapi/name.go
new
+35
@@ -0,0 +1,35 @@
1
+package httpapi
2
+
3
+import (
4
+ "context"
5
+ "github.com/ipfs/go-ipfs/core/coreapi/interface"
6
+ "github.com/ipfs/go-ipfs/core/coreapi/interface/options"
7
+)
8
+
9
+type NameAPI HttpApi
10
+
11
+func (api *NameAPI) Publish(ctx context.Context, p iface.Path, opts ...options.NamePublishOption) (iface.IpnsEntry, error) {
12
+ return nil, ErrNotImplemented
13
+}
14
+
15
+func (api *NameAPI) Search(ctx context.Context, name string, opts ...options.NameResolveOption) (<-chan iface.IpnsResult, error) {
16
+ return nil, ErrNotImplemented
17
+}
18
+
19
+func (api *NameAPI) Resolve(ctx context.Context, name string, opts ...options.NameResolveOption) (iface.Path, error) {
20
+ // TODO: options!
21
+
22
+ req := api.core().request("name/resolve")
23
+ req.Arguments(name)
24
+
25
+ var out struct{ Path string }
26
+ if err := req.Exec(ctx, &out); err != nil {
27
+ return nil, err
28
+ }
29
+
30
+ return iface.ParsePath(out.Path)
31
+}
32
+
33
+func (api *NameAPI) core() *HttpApi {
34
+ return (*HttpApi)(api)
35
+}
client/httpapi/path.go
new
+48
@@ -0,0 +1,48 @@
1
+package httpapi
2
+
3
+import (
4
+ "context"
5
+
6
+ "github.com/ipfs/go-ipfs/core/coreapi/interface"
7
+
8
+ cid "gx/ipfs/QmR8BauakNcBa3RbE4nbQu76PDiJgoQgz8AJdhJuiU4TAw/go-cid"
9
+ ipfspath "gx/ipfs/QmRG3XuGwT7GYuAqgWDJBKTzdaHMwAnc1x7J2KHEXNHxzG/go-path"
10
+ ipld "gx/ipfs/QmcKKBwfz6FyQdHR2jsXrrF6XeSBXYL86anmWNewpFpoF5/go-ipld-format"
11
+)
12
+
13
+func (api *HttpApi) ResolvePath(ctx context.Context, path iface.Path) (iface.ResolvedPath, error) {
14
+ var out struct {
15
+ Cid cid.Cid
16
+ RemPath string
17
+ }
18
+
19
+ //TODO: this is hacky, fixing https://github.com/ipfs/go-ipfs/issues/5703 would help
20
+
21
+ var err error
22
+ if path.Namespace() == "ipns" {
23
+ if path, err = api.Name().Resolve(ctx, path.String()); err != nil {
24
+ return nil, err
25
+ }
26
+ }
27
+
28
+ if err := api.request("dag/resolve", path.String()).Exec(ctx, &out); err != nil {
29
+ return nil, err
30
+ }
31
+
32
+ // TODO:
33
+ ipath, err := ipfspath.FromSegments("/" +path.Namespace() + "/", out.Cid.String(), out.RemPath)
34
+ if err != nil {
35
+ return nil, err
36
+ }
37
+
38
+ root, err := cid.Parse(ipfspath.Path(path.String()).Segments()[1])
39
+ if err != nil {
40
+ return nil, err
41
+ }
42
+
43
+ return iface.NewResolvedPath(ipath, out.Cid, root, out.RemPath), nil
44
+}
45
+
46
+func (api *HttpApi) ResolveNode(context.Context, iface.Path) (ipld.Node, error) {
47
+ return nil, ErrNotImplemented
48
+}
client/httpapi/request.go
new
+34
@@ -0,0 +1,34 @@
1
+package httpapi
2
+
3
+import (
4
+ "context"
5
+ "io"
6
+ "strings"
7
+)
8
+
9
+type Request struct {
10
+ ApiBase string
11
+ Command string
12
+ Args []string
13
+ Opts map[string]string
14
+ Body io.Reader
15
+ Headers map[string]string
16
+}
17
+
18
+func NewRequest(ctx context.Context, url, command string, args ...string) *Request {
19
+ if !strings.HasPrefix(url, "http") {
20
+ url = "http://" + url
21
+ }
22
+
23
+ opts := map[string]string{
24
+ "encoding": "json",
25
+ "stream-channels": "true",
26
+ }
27
+ return &Request{
28
+ ApiBase: url + "/api/v0",
29
+ Command: command,
30
+ Args: args,
31
+ Opts: opts,
32
+ Headers: make(map[string]string),
33
+ }
34
+}
client/httpapi/requestbuilder.go
new
+100
@@ -0,0 +1,100 @@
1
+package httpapi
2
+
3
+import (
4
+ "bytes"
5
+ "context"
6
+ "fmt"
7
+ "io"
8
+ "strconv"
9
+ "strings"
10
+)
11
+
12
+// RequestBuilder is an IPFS commands request builder.
13
+type RequestBuilder struct {
14
+ command string
15
+ args []string
16
+ opts map[string]string
17
+ headers map[string]string
18
+ body io.Reader
19
+
20
+ shell *HttpApi
21
+}
22
+
23
+// Arguments adds the arguments to the args.
24
+func (r *RequestBuilder) Arguments(args ...string) *RequestBuilder {
25
+ r.args = append(r.args, args...)
26
+ return r
27
+}
28
+
29
+// BodyString sets the request body to the given string.
30
+func (r *RequestBuilder) BodyString(body string) *RequestBuilder {
31
+ return r.Body(strings.NewReader(body))
32
+}
33
+
34
+// BodyBytes sets the request body to the given buffer.
35
+func (r *RequestBuilder) BodyBytes(body []byte) *RequestBuilder {
36
+ return r.Body(bytes.NewReader(body))
37
+}
38
+
39
+// Body sets the request body to the given reader.
40
+func (r *RequestBuilder) Body(body io.Reader) *RequestBuilder {
41
+ r.body = body
42
+ return r
43
+}
44
+
45
+// Option sets the given option.
46
+func (r *RequestBuilder) Option(key string, value interface{}) *RequestBuilder {
47
+ var s string
48
+ switch v := value.(type) {
49
+ case bool:
50
+ s = strconv.FormatBool(v)
51
+ case string:
52
+ s = v
53
+ case []byte:
54
+ s = string(v)
55
+ default:
56
+ // slow case.
57
+ s = fmt.Sprint(value)
58
+ }
59
+ if r.opts == nil {
60
+ r.opts = make(map[string]string, 1)
61
+ }
62
+ r.opts[key] = s
63
+ return r
64
+}
65
+
66
+// Header sets the given header.
67
+func (r *RequestBuilder) Header(name, value string) *RequestBuilder {
68
+ if r.headers == nil {
69
+ r.headers = make(map[string]string, 1)
70
+ }
71
+ r.headers[name] = value
72
+ return r
73
+}
74
+
75
+// Send sends the request and return the response.
76
+func (r *RequestBuilder) Send(ctx context.Context) (*Response, error) {
77
+ req := NewRequest(ctx, r.shell.url, r.command, r.args...)
78
+ req.Opts = r.opts
79
+ req.Headers = r.headers
80
+ req.Body = r.body
81
+ return req.Send(r.shell.httpcli)
82
+}
83
+
84
+// Exec sends the request a request and decodes the response.
85
+func (r *RequestBuilder) Exec(ctx context.Context, res interface{}) error {
86
+ httpRes, err := r.Send(ctx)
87
+ if err != nil {
88
+ return err
89
+ }
90
+
91
+ if res == nil {
92
+ httpRes.Close()
93
+ if httpRes.Error != nil {
94
+ return httpRes.Error
95
+ }
96
+ return nil
97
+ }
98
+
99
+ return httpRes.Decode(res)
100
+}
client/httpapi/response.go
new
+132
@@ -0,0 +1,132 @@
1
+package httpapi
2
+
3
+import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+ "io/ioutil"
8
+ "net/http"
9
+ "net/url"
10
+ "os"
11
+ "strings"
12
+
13
+ files "github.com/ipfs/go-ipfs-files"
14
+)
15
+
16
+type Response struct {
17
+ Output io.ReadCloser
18
+ Error *Error
19
+}
20
+
21
+func (r *Response) Close() error {
22
+ if r.Output != nil {
23
+ // always drain output (response body)
24
+ ioutil.ReadAll(r.Output)
25
+ return r.Output.Close()
26
+ }
27
+ return nil
28
+}
29
+
30
+func (r *Response) Decode(dec interface{}) error {
31
+ defer r.Close()
32
+ if r.Error != nil {
33
+ return r.Error
34
+ }
35
+
36
+ return json.NewDecoder(r.Output).Decode(dec)
37
+}
38
+
39
+type Error struct {
40
+ Command string
41
+ Message string
42
+ Code int
43
+}
44
+
45
+func (e *Error) Error() string {
46
+ var out string
47
+ if e.Command != "" {
48
+ out = e.Command + ": "
49
+ }
50
+ if e.Code != 0 {
51
+ out = fmt.Sprintf("%s%d: ", out, e.Code)
52
+ }
53
+ return out + e.Message
54
+}
55
+
56
+func (r *Request) Send(c *http.Client) (*Response, error) {
57
+ url := r.getURL()
58
+ req, err := http.NewRequest("POST", url, r.Body)
59
+ if err != nil {
60
+ return nil, err
61
+ }
62
+
63
+ // Add any headers that were supplied via the RequestBuilder.
64
+ for k, v := range r.Headers {
65
+ req.Header.Add(k, v)
66
+ }
67
+
68
+ if fr, ok := r.Body.(*files.MultiFileReader); ok {
69
+ req.Header.Set("Content-Type", "multipart/form-data; boundary="+fr.Boundary())
70
+ req.Header.Set("Content-Disposition", "form-data: name=\"files\"")
71
+ }
72
+
73
+ resp, err := c.Do(req)
74
+ if err != nil {
75
+ return nil, err
76
+ }
77
+
78
+ contentType := resp.Header.Get("Content-Type")
79
+ parts := strings.Split(contentType, ";")
80
+ contentType = parts[0]
81
+
82
+ nresp := new(Response)
83
+
84
+ nresp.Output = resp.Body
85
+ if resp.StatusCode >= http.StatusBadRequest {
86
+ e := &Error{
87
+ Command: r.Command,
88
+ }
89
+ switch {
90
+ case resp.StatusCode == http.StatusNotFound:
91
+ e.Message = "command not found"
92
+ case contentType == "text/plain":
93
+ out, err := ioutil.ReadAll(resp.Body)
94
+ if err != nil {
95
+ fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) read error: %s\n", resp.StatusCode, err)
96
+ }
97
+ e.Message = string(out)
98
+ case contentType == "application/json":
99
+ if err = json.NewDecoder(resp.Body).Decode(e); err != nil {
100
+ fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) unmarshall error: %s\n", resp.StatusCode, err)
101
+ }
102
+ default:
103
+ fmt.Fprintf(os.Stderr, "ipfs-shell: warning! unhandled response (%d) encoding: %s", resp.StatusCode, contentType)
104
+ out, err := ioutil.ReadAll(resp.Body)
105
+ if err != nil {
106
+ fmt.Fprintf(os.Stderr, "ipfs-shell: response (%d) read error: %s\n", resp.StatusCode, err)
107
+ }
108
+ e.Message = fmt.Sprintf("unknown ipfs-shell error encoding: %q - %q", contentType, out)
109
+ }
110
+ nresp.Error = e
111
+ nresp.Output = nil
112
+
113
+ // drain body and close
114
+ ioutil.ReadAll(resp.Body)
115
+ resp.Body.Close()
116
+ }
117
+
118
+ return nresp, nil
119
+}
120
+
121
+func (r *Request) getURL() string {
122
+
123
+ values := make(url.Values)
124
+ for _, arg := range r.Args {
125
+ values.Add("arg", arg)
126
+ }
127
+ for k, v := range r.Opts {
128
+ values.Add(k, v)
129
+ }
130
+
131
+ return fmt.Sprintf("%s/%s?%s", r.ApiBase, r.Command, values.Encode())
132
+}