Initial structure, path stuff

This commit was moved from ipfs/go-ipfs-http-client@93943f7f56
This commit is contained in:
Łukasz Magiera 2018-11-06 13:13:15 +01:00
parent 202081523f
commit 6d85aff407
6 changed files with 476 additions and 0 deletions

127
client/httpapi/api.go Normal file
View File

@ -0,0 +1,127 @@
package httpapi
import (
"github.com/pkg/errors"
"io/ioutil"
gohttp "net/http"
"os"
"path"
"strings"
"github.com/ipfs/go-ipfs/core/coreapi/interface"
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"
)
var ErrNotImplemented = errors.New("not implemented")
type HttpApi struct {
url string
httpcli *gohttp.Client
}
func NewLocalApi() iface.CoreAPI {
baseDir := os.Getenv(EnvDir)
if baseDir == "" {
baseDir = DefaultPathRoot
}
baseDir, err := homedir.Expand(baseDir)
if err != nil {
return nil
}
apiFile := path.Join(baseDir, DefaultApiFile)
if _, err := os.Stat(apiFile); err != nil {
return nil
}
api, err := ioutil.ReadFile(apiFile)
if err != nil {
return nil
}
return NewApi(strings.TrimSpace(string(api)))
}
func NewApi(url string) *HttpApi {
c := &gohttp.Client{
Transport: &gohttp.Transport{
Proxy: gohttp.ProxyFromEnvironment,
DisableKeepAlives: true,
},
}
return NewApiWithClient(url, c)
}
func NewApiWithClient(url string, c *gohttp.Client) *HttpApi {
if a, err := ma.NewMultiaddr(url); err == nil {
_, host, err := manet.DialArgs(a)
if err == nil {
url = host
}
}
return &HttpApi{
url: url,
httpcli: c,
}
}
func (api *HttpApi) request(command string, args ...string) *RequestBuilder {
return &RequestBuilder{
command: command,
args: args,
shell: api,
}
}
func (api *HttpApi) Unixfs() iface.UnixfsAPI {
return nil
}
func (api *HttpApi) Block() iface.BlockAPI {
return nil
}
func (api *HttpApi) Dag() iface.DagAPI {
return nil
}
func (api *HttpApi) Name() iface.NameAPI {
return (*NameAPI)(api)
}
func (api *HttpApi) Key() iface.KeyAPI {
return nil
}
func (api *HttpApi) Pin() iface.PinAPI {
return nil
}
func (api *HttpApi) Object() iface.ObjectAPI {
return nil
}
func (api *HttpApi) Dht() iface.DhtAPI {
return nil
}
func (api *HttpApi) Swarm() iface.SwarmAPI {
return nil
}
func (api *HttpApi) PubSub() iface.PubSubAPI {
return nil
}

35
client/httpapi/name.go Normal file
View File

@ -0,0 +1,35 @@
package httpapi
import (
"context"
"github.com/ipfs/go-ipfs/core/coreapi/interface"
"github.com/ipfs/go-ipfs/core/coreapi/interface/options"
)
type NameAPI HttpApi
func (api *NameAPI) Publish(ctx context.Context, p iface.Path, opts ...options.NamePublishOption) (iface.IpnsEntry, error) {
return nil, ErrNotImplemented
}
func (api *NameAPI) Search(ctx context.Context, name string, opts ...options.NameResolveOption) (<-chan iface.IpnsResult, error) {
return nil, ErrNotImplemented
}
func (api *NameAPI) Resolve(ctx context.Context, name string, opts ...options.NameResolveOption) (iface.Path, error) {
// TODO: options!
req := api.core().request("name/resolve")
req.Arguments(name)
var out struct{ Path string }
if err := req.Exec(ctx, &out); err != nil {
return nil, err
}
return iface.ParsePath(out.Path)
}
func (api *NameAPI) core() *HttpApi {
return (*HttpApi)(api)
}

48
client/httpapi/path.go Normal file
View File

@ -0,0 +1,48 @@
package httpapi
import (
"context"
"github.com/ipfs/go-ipfs/core/coreapi/interface"
cid "gx/ipfs/QmR8BauakNcBa3RbE4nbQu76PDiJgoQgz8AJdhJuiU4TAw/go-cid"
ipfspath "gx/ipfs/QmRG3XuGwT7GYuAqgWDJBKTzdaHMwAnc1x7J2KHEXNHxzG/go-path"
ipld "gx/ipfs/QmcKKBwfz6FyQdHR2jsXrrF6XeSBXYL86anmWNewpFpoF5/go-ipld-format"
)
func (api *HttpApi) ResolvePath(ctx context.Context, path iface.Path) (iface.ResolvedPath, error) {
var out struct {
Cid cid.Cid
RemPath string
}
//TODO: this is hacky, fixing https://github.com/ipfs/go-ipfs/issues/5703 would help
var err error
if path.Namespace() == "ipns" {
if path, err = api.Name().Resolve(ctx, path.String()); err != nil {
return nil, err
}
}
if err := api.request("dag/resolve", path.String()).Exec(ctx, &out); err != nil {
return nil, err
}
// TODO:
ipath, err := ipfspath.FromSegments("/" +path.Namespace() + "/", out.Cid.String(), out.RemPath)
if err != nil {
return nil, err
}
root, err := cid.Parse(ipfspath.Path(path.String()).Segments()[1])
if err != nil {
return nil, err
}
return iface.NewResolvedPath(ipath, out.Cid, root, out.RemPath), nil
}
func (api *HttpApi) ResolveNode(context.Context, iface.Path) (ipld.Node, error) {
return nil, ErrNotImplemented
}

34
client/httpapi/request.go Normal file
View File

@ -0,0 +1,34 @@
package httpapi
import (
"context"
"io"
"strings"
)
type Request struct {
ApiBase string
Command string
Args []string
Opts map[string]string
Body io.Reader
Headers map[string]string
}
func NewRequest(ctx context.Context, url, command string, args ...string) *Request {
if !strings.HasPrefix(url, "http") {
url = "http://" + url
}
opts := map[string]string{
"encoding": "json",
"stream-channels": "true",
}
return &Request{
ApiBase: url + "/api/v0",
Command: command,
Args: args,
Opts: opts,
Headers: make(map[string]string),
}
}

View File

@ -0,0 +1,100 @@
package httpapi
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"strings"
)
// RequestBuilder is an IPFS commands request builder.
type RequestBuilder struct {
command string
args []string
opts map[string]string
headers map[string]string
body io.Reader
shell *HttpApi
}
// Arguments adds the arguments to the args.
func (r *RequestBuilder) Arguments(args ...string) *RequestBuilder {
r.args = append(r.args, args...)
return r
}
// BodyString sets the request body to the given string.
func (r *RequestBuilder) BodyString(body string) *RequestBuilder {
return r.Body(strings.NewReader(body))
}
// BodyBytes sets the request body to the given buffer.
func (r *RequestBuilder) BodyBytes(body []byte) *RequestBuilder {
return r.Body(bytes.NewReader(body))
}
// Body sets the request body to the given reader.
func (r *RequestBuilder) Body(body io.Reader) *RequestBuilder {
r.body = body
return r
}
// Option sets the given option.
func (r *RequestBuilder) Option(key string, value interface{}) *RequestBuilder {
var s string
switch v := value.(type) {
case bool:
s = strconv.FormatBool(v)
case string:
s = v
case []byte:
s = string(v)
default:
// slow case.
s = fmt.Sprint(value)
}
if r.opts == nil {
r.opts = make(map[string]string, 1)
}
r.opts[key] = s
return r
}
// Header sets the given header.
func (r *RequestBuilder) Header(name, value string) *RequestBuilder {
if r.headers == nil {
r.headers = make(map[string]string, 1)
}
r.headers[name] = value
return r
}
// Send sends the request and return the response.
func (r *RequestBuilder) Send(ctx context.Context) (*Response, error) {
req := NewRequest(ctx, r.shell.url, r.command, r.args...)
req.Opts = r.opts
req.Headers = r.headers
req.Body = r.body
return req.Send(r.shell.httpcli)
}
// Exec sends the request a request and decodes the response.
func (r *RequestBuilder) Exec(ctx context.Context, res interface{}) error {
httpRes, err := r.Send(ctx)
if err != nil {
return err
}
if res == nil {
httpRes.Close()
if httpRes.Error != nil {
return httpRes.Error
}
return nil
}
return httpRes.Decode(res)
}

132
client/httpapi/response.go Normal file
View File

@ -0,0 +1,132 @@
package httpapi
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
files "github.com/ipfs/go-ipfs-files"
)
type Response struct {
Output io.ReadCloser
Error *Error
}
func (r *Response) Close() error {
if r.Output != nil {
// always drain output (response body)
ioutil.ReadAll(r.Output)
return r.Output.Close()
}
return nil
}
func (r *Response) Decode(dec interface{}) error {
defer r.Close()
if r.Error != nil {
return r.Error
}
return json.NewDecoder(r.Output).Decode(dec)
}
type Error struct {
Command string
Message string
Code int
}
func (e *Error) Error() string {
var out string
if e.Command != "" {
out = e.Command + ": "
}
if e.Code != 0 {
out = fmt.Sprintf("%s%d: ", out, e.Code)
}
return out + e.Message
}
func (r *Request) Send(c *http.Client) (*Response, error) {
url := r.getURL()
req, err := http.NewRequest("POST", url, r.Body)
if err != nil {
return nil, err
}
// Add any headers that were supplied via the RequestBuilder.
for k, v := range r.Headers {
req.Header.Add(k, v)
}
if fr, ok := r.Body.(*files.MultiFileReader); ok {
req.Header.Set("Content-Type", "multipart/form-data; boundary="+fr.Boundary())
req.Header.Set("Content-Disposition", "form-data: name=\"files\"")
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
contentType := resp.Header.Get("Content-Type")
parts := strings.Split(contentType, ";")
contentType = parts[0]
nresp := new(Response)
nresp.Output = resp.Body
if resp.StatusCode >= http.StatusBadRequest {
e := &Error{
Command: r.Command,
}
switch {
case resp.StatusCode == http.StatusNotFound:
e.Message = "command not found"
case contentType == "text/plain":
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) read error: %s\n", resp.StatusCode, err)
}
e.Message = string(out)
case contentType == "application/json":
if err = json.NewDecoder(resp.Body).Decode(e); err != nil {
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! response (%d) unmarshall error: %s\n", resp.StatusCode, err)
}
default:
fmt.Fprintf(os.Stderr, "ipfs-shell: warning! unhandled response (%d) encoding: %s", resp.StatusCode, contentType)
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "ipfs-shell: response (%d) read error: %s\n", resp.StatusCode, err)
}
e.Message = fmt.Sprintf("unknown ipfs-shell error encoding: %q - %q", contentType, out)
}
nresp.Error = e
nresp.Output = nil
// drain body and close
ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
return nresp, nil
}
func (r *Request) getURL() string {
values := make(url.Values)
for _, arg := range r.Args {
values.Add("arg", arg)
}
for k, v := range r.Opts {
values.Add(k, v)
}
return fmt.Sprintf("%s/%s?%s", r.ApiBase, r.Command, values.Encode())
}