diff --git a/api.go b/api.go new file mode 100644 index 000000000..d4e5a64d7 --- /dev/null +++ b/api.go @@ -0,0 +1,116 @@ +package shell + +import ( + "context" + "errors" + "os" + "strings" + "path/filepath" + "io/ioutil" + "net/http" + + homedir "github.com/mitchellh/go-homedir" + iface "github.com/ipfs/go-ipfs/core/coreapi/interface" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" +) + +const ( + DefaultPathName = ".ipfs" + DefaultPathRoot = "~/" + DefaultPathName + DefaultApiFile = "api" + EnvDir = "IPFS_PATH" +) + +type httpApi struct{ + url string + client *http.Client +} + +func NewLocalApi() (iface.CoreAPI, error) { + baseDir := os.Getenv(EnvDir) + if baseDir == "" { + baseDir = DefaultPathRoot + } + + baseDir, err := homedir.Expand(baseDir) + if err != nil { + return nil, err + } + + apiFile := filepath.Join(baseDir, DefaultApiFile) + + if _, err := os.Stat(apiFile); err != nil { + return nil, err + } + + api, err := ioutil.ReadFile(apiFile) + if err != nil { + return nil, err + } + + return NewApi(strings.TrimSpace(string(api))) +} + +func NewApi(url string) (iface.CoreAPI, error) { + if a, err := ma.NewMultiaddr(url); err == nil { + _, host, err := manet.DialArgs(a) + if err == nil { + url = host + } + } + + return &httpApi{ + url: url, + client: &http.Client{ + Transport: &http.Transport{ + DisableKeepAlives: true, + }, + }, + }, nil +} + +// Unixfs returns the UnixfsAPI interface backed by the go-ipfs node +func (api *httpApi) Unixfs() iface.UnixfsAPI { + return nil +} + +func (api *httpApi) Block() iface.BlockAPI { + return nil +} + +// Dag returns the DagAPI interface backed by the go-ipfs node +func (api *httpApi) Dag() iface.DagAPI { + return nil +} + +// Name returns the NameAPI interface backed by the go-ipfs node +func (api *httpApi) Name() iface.NameAPI { + return nil +} + +// Key returns the KeyAPI interface backed by the go-ipfs node +func (api *httpApi) Key() iface.KeyAPI { + return (*httpKeyApi)(api) +} + +//Object returns the ObjectAPI interface backed by the go-ipfs node +func (api *httpApi) Object() iface.ObjectAPI { + return nil +} + +func (api *httpApi) Pin() iface.PinAPI { + return nil +} + +// ResolveNode resolves the path `p` using Unixfs resolver, gets and returns the +// resolved Node. +func (api *httpApi) ResolveNode(ctx context.Context, p iface.Path) (iface.Node, error) { + return nil, errors.New("TODO") +} + +// ResolvePath resolves the path `p` using Unixfs resolver, returns the +// resolved path. +func (api *httpApi) ResolvePath(ctx context.Context, p iface.Path) (iface.Path, error) { + return nil, errors.New("TODO") +} diff --git a/key.go b/key.go new file mode 100644 index 000000000..68f94edb3 --- /dev/null +++ b/key.go @@ -0,0 +1,76 @@ +package shell + +import ( + "context" + "errors" + "encoding/json" + + "github.com/ipfs/go-ipfs/core/coreapi/interface" + "github.com/ipfs/go-ipfs/core/coreapi/interface/options" +) + +type httpKeyApi httpApi + +type key struct { + KeyName string `json:"Name"` + Id string `json:"Id"` +} + +func (k *key) Name() string { + return k.KeyName +} + +func (k *key) Path() iface.Path { + return nil //TODO +} + + +func (api *httpKeyApi) Generate(ctx context.Context, name string, opts ...options.KeyGenerateOption) (iface.Key, error) { + return nil, errors.New("TODO") +} + +func (api *httpKeyApi) WithType(algorithm string) options.KeyGenerateOption { + return nil +} + +func (api *httpKeyApi) WithSize(size int) options.KeyGenerateOption { + return nil +} + +func (api *httpKeyApi) Rename(ctx context.Context, oldName string, newName string, opts ...options.KeyRenameOption) (iface.Key, bool, error) { + return nil, false, errors.New("TODO") +} + +func (api *httpKeyApi) WithForce(force bool) options.KeyRenameOption { + return nil +} + +func (api *httpKeyApi) List(ctx context.Context) ([]iface.Key, error) { + resp, err := api.core().newRequest(ctx, "key/list").Send(api.client) + if err != nil { + return nil, err + } + + var res = struct{ + Keys []*key + }{} + + if err := json.NewDecoder(resp.Output).Decode(&res); err != nil { + return nil, err + } + + out := make([]iface.Key, len(res.Keys)) + for i, e := range res.Keys { + out[i] = e + } + + return out, nil +} + +func (api *httpKeyApi) Remove(ctx context.Context, name string) (iface.Path, error) { + return nil, errors.New("TODO") +} + +func (api *httpKeyApi) core() *httpApi { + return (*httpApi)(api) +} diff --git a/key_test.go b/key_test.go new file mode 100644 index 000000000..1ab560266 --- /dev/null +++ b/key_test.go @@ -0,0 +1,24 @@ +package shell + +import ( + "testing" + "context" +) + +//TODO: create utils to run tests on a proper test node +func TestListSelf(t *testing.T) { + ctx := context.Background() + api, err := NewLocalApi() + if err != nil { + t.Fatal(err) + } + + keys, err := api.Key().List(ctx) + if err != nil { + t.Fatalf("failed to list keys: %s", err) + } + + if keys[0].Name() != "self" { + t.Errorf("expected the key to be called 'self', got '%s'", keys[0].Name()) + } +} diff --git a/legacy.go b/legacy.go new file mode 100644 index 000000000..1c9bcdcf9 --- /dev/null +++ b/legacy.go @@ -0,0 +1,19 @@ +package shell + +import ( + gohttp "net/http" + + "github.com/ipfs/go-ipfs-api/legacy" +) + +func NewLocalShell() *legacy.Shell { + return legacy.NewLocalShell() +} + +func NewShell(url string) *legacy.Shell { + return legacy.NewShell(url) +} + +func NewShellWithClient(url string, c *gohttp.Client) *legacy.Shell { + return legacy.NewShellWithClient(url, c) +} diff --git a/dag.go b/legacy/dag.go similarity index 98% rename from dag.go rename to legacy/dag.go index 27899af58..9f0d57175 100644 --- a/dag.go +++ b/legacy/dag.go @@ -1,4 +1,4 @@ -package shell +package legacy import ( "bytes" diff --git a/ipns.go b/legacy/ipns.go similarity index 98% rename from ipns.go rename to legacy/ipns.go index 90715b6cf..9e3d96397 100644 --- a/ipns.go +++ b/legacy/ipns.go @@ -1,4 +1,4 @@ -package shell +package legacy import ( "context" diff --git a/pubsub.go b/legacy/pubsub.go similarity index 99% rename from pubsub.go rename to legacy/pubsub.go index 8d8e9983f..9f0ef8743 100644 --- a/pubsub.go +++ b/legacy/pubsub.go @@ -1,4 +1,4 @@ -package shell +package legacy import ( "encoding/binary" diff --git a/legacy/request.go b/legacy/request.go new file mode 100644 index 000000000..a9a7d46ac --- /dev/null +++ b/legacy/request.go @@ -0,0 +1,147 @@ +package legacy + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + files "github.com/ipfs/go-ipfs-cmdkit/files" +) + +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), + } +} + +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 +} + +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 + } + + 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 read error: %s\n", 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 unmarshall error: %s\n", err) + } + default: + fmt.Fprintf(os.Stderr, "ipfs-shell: warning! unhandled response encoding: %s", contentType) + out, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Fprintf(os.Stderr, "ipfs-shell: response read error: %s\n", 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()) +} diff --git a/shell.go b/legacy/shell.go similarity index 99% rename from shell.go rename to legacy/shell.go index 16c56cb77..0c566de6d 100644 --- a/shell.go +++ b/legacy/shell.go @@ -1,5 +1,5 @@ // package shell implements a remote API interface for a running ipfs daemon -package shell +package legacy import ( "bytes" diff --git a/shell_test.go b/legacy/shell_test.go similarity index 99% rename from shell_test.go rename to legacy/shell_test.go index 26b43eaa1..3fd1325c9 100644 --- a/shell_test.go +++ b/legacy/shell_test.go @@ -1,4 +1,4 @@ -package shell +package legacy import ( "bytes" diff --git a/unixfs.go b/legacy/unixfs.go similarity index 98% rename from unixfs.go rename to legacy/unixfs.go index 11df763b0..96621c74e 100644 --- a/unixfs.go +++ b/legacy/unixfs.go @@ -1,4 +1,4 @@ -package shell +package legacy import ( "context" diff --git a/request.go b/request.go index 8b6d4e0df..88d1c1b22 100644 --- a/request.go +++ b/request.go @@ -23,7 +23,10 @@ type Request struct { Headers map[string]string } -func NewRequest(ctx context.Context, url, command string, args ...string) *Request { +//TODO: consider refactoring / cleaning up all this +func (api *httpApi) newRequest(ctx context.Context, command string, args ...string) *Request { + url := api.url + if !strings.HasPrefix(url, "http") { url = "http://" + url } @@ -73,9 +76,7 @@ func (e *Error) Error() string { } func (r *Request) Send(c *http.Client) (*Response, error) { - url := r.getURL() - - req, err := http.NewRequest("POST", url, r.Body) + req, err := http.NewRequest("POST", r.getURL(), r.Body) if err != nil { return nil, err } @@ -134,7 +135,6 @@ func (r *Request) Send(c *http.Client) (*Response, error) { } func (r *Request) getURL() string { - values := make(url.Values) for _, arg := range r.Args { values.Add("arg", arg)