--- /dev/null
+package shell
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+
+ files "github.com/ipfs/go-ipfs-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 trailerReader struct {
+ resp *http.Response
+}
+
+func (r *trailerReader) Read(b []byte) (int, error) {
+ n, err := r.resp.Body.Read(b)
+ if err != nil {
+ if e := r.resp.Trailer.Get("X-Stream-Error"); e != "" {
+ err = errors.New(e)
+ }
+ }
+ return n, err
+}
+
+func (r *trailerReader) Close() error {
+ return r.resp.Body.Close()
+}
+
+type Response struct {
+ Output io.ReadCloser
+ Error *Error
+}
+
+func (r *Response) Close() error {
+ if r.Output != nil {
+ // always drain output (response body)
+ _, err1 := io.Copy(ioutil.Discard, r.Output)
+ err2 := r.Output.Close()
+ if err1 != nil {
+ return err1
+ }
+ if err2 != nil {
+ return err2
+ }
+ }
+ 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 = &trailerReader{resp}
+ 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
+ io.Copy(ioutil.Discard, 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())
+}