github.com/anthdm/go-ethereum@v1.8.4-0.20180412101906-60516c83b011/swarm/api/client/client.go (about)

     1  // Copyright 2017 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package client
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"mime"
    28  	"mime/multipart"
    29  	"net/http"
    30  	"net/textproto"
    31  	"os"
    32  	"path/filepath"
    33  	"strconv"
    34  	"strings"
    35  
    36  	"github.com/ethereum/go-ethereum/swarm/api"
    37  )
    38  
    39  var (
    40  	DefaultGateway = "http://localhost:8500"
    41  	DefaultClient  = NewClient(DefaultGateway)
    42  )
    43  
    44  func NewClient(gateway string) *Client {
    45  	return &Client{
    46  		Gateway: gateway,
    47  	}
    48  }
    49  
    50  // Client wraps interaction with a swarm HTTP gateway.
    51  type Client struct {
    52  	Gateway string
    53  }
    54  
    55  // UploadRaw uploads raw data to swarm and returns the resulting hash
    56  func (c *Client) UploadRaw(r io.Reader, size int64) (string, error) {
    57  	if size <= 0 {
    58  		return "", errors.New("data size must be greater than zero")
    59  	}
    60  	req, err := http.NewRequest("POST", c.Gateway+"/bzz-raw:/", r)
    61  	if err != nil {
    62  		return "", err
    63  	}
    64  	req.ContentLength = size
    65  	res, err := http.DefaultClient.Do(req)
    66  	if err != nil {
    67  		return "", err
    68  	}
    69  	defer res.Body.Close()
    70  	if res.StatusCode != http.StatusOK {
    71  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
    72  	}
    73  	data, err := ioutil.ReadAll(res.Body)
    74  	if err != nil {
    75  		return "", err
    76  	}
    77  	return string(data), nil
    78  }
    79  
    80  // DownloadRaw downloads raw data from swarm
    81  func (c *Client) DownloadRaw(hash string) (io.ReadCloser, error) {
    82  	uri := c.Gateway + "/bzz-raw:/" + hash
    83  	res, err := http.DefaultClient.Get(uri)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	if res.StatusCode != http.StatusOK {
    88  		res.Body.Close()
    89  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
    90  	}
    91  	return res.Body, nil
    92  }
    93  
    94  // File represents a file in a swarm manifest and is used for uploading and
    95  // downloading content to and from swarm
    96  type File struct {
    97  	io.ReadCloser
    98  	api.ManifestEntry
    99  }
   100  
   101  // Open opens a local file which can then be passed to client.Upload to upload
   102  // it to swarm
   103  func Open(path string) (*File, error) {
   104  	f, err := os.Open(path)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	stat, err := f.Stat()
   109  	if err != nil {
   110  		f.Close()
   111  		return nil, err
   112  	}
   113  	return &File{
   114  		ReadCloser: f,
   115  		ManifestEntry: api.ManifestEntry{
   116  			ContentType: mime.TypeByExtension(filepath.Ext(path)),
   117  			Mode:        int64(stat.Mode()),
   118  			Size:        stat.Size(),
   119  			ModTime:     stat.ModTime(),
   120  		},
   121  	}, nil
   122  }
   123  
   124  // Upload uploads a file to swarm and either adds it to an existing manifest
   125  // (if the manifest argument is non-empty) or creates a new manifest containing
   126  // the file, returning the resulting manifest hash (the file will then be
   127  // available at bzz:/<hash>/<path>)
   128  func (c *Client) Upload(file *File, manifest string) (string, error) {
   129  	if file.Size <= 0 {
   130  		return "", errors.New("file size must be greater than zero")
   131  	}
   132  	return c.TarUpload(manifest, &FileUploader{file})
   133  }
   134  
   135  // Download downloads a file with the given path from the swarm manifest with
   136  // the given hash (i.e. it gets bzz:/<hash>/<path>)
   137  func (c *Client) Download(hash, path string) (*File, error) {
   138  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   139  	res, err := http.DefaultClient.Get(uri)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	if res.StatusCode != http.StatusOK {
   144  		res.Body.Close()
   145  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   146  	}
   147  	return &File{
   148  		ReadCloser: res.Body,
   149  		ManifestEntry: api.ManifestEntry{
   150  			ContentType: res.Header.Get("Content-Type"),
   151  			Size:        res.ContentLength,
   152  		},
   153  	}, nil
   154  }
   155  
   156  // UploadDirectory uploads a directory tree to swarm and either adds the files
   157  // to an existing manifest (if the manifest argument is non-empty) or creates a
   158  // new manifest, returning the resulting manifest hash (files from the
   159  // directory will then be available at bzz:/<hash>/path/to/file), with
   160  // the file specified in defaultPath being uploaded to the root of the manifest
   161  // (i.e. bzz:/<hash>/)
   162  func (c *Client) UploadDirectory(dir, defaultPath, manifest string) (string, error) {
   163  	stat, err := os.Stat(dir)
   164  	if err != nil {
   165  		return "", err
   166  	} else if !stat.IsDir() {
   167  		return "", fmt.Errorf("not a directory: %s", dir)
   168  	}
   169  	return c.TarUpload(manifest, &DirectoryUploader{dir, defaultPath})
   170  }
   171  
   172  // DownloadDirectory downloads the files contained in a swarm manifest under
   173  // the given path into a local directory (existing files will be overwritten)
   174  func (c *Client) DownloadDirectory(hash, path, destDir string) error {
   175  	stat, err := os.Stat(destDir)
   176  	if err != nil {
   177  		return err
   178  	} else if !stat.IsDir() {
   179  		return fmt.Errorf("not a directory: %s", destDir)
   180  	}
   181  
   182  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   183  	req, err := http.NewRequest("GET", uri, nil)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	req.Header.Set("Accept", "application/x-tar")
   188  	res, err := http.DefaultClient.Do(req)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	defer res.Body.Close()
   193  	if res.StatusCode != http.StatusOK {
   194  		return fmt.Errorf("unexpected HTTP status: %s", res.Status)
   195  	}
   196  	tr := tar.NewReader(res.Body)
   197  	for {
   198  		hdr, err := tr.Next()
   199  		if err == io.EOF {
   200  			return nil
   201  		} else if err != nil {
   202  			return err
   203  		}
   204  		// ignore the default path file
   205  		if hdr.Name == "" {
   206  			continue
   207  		}
   208  
   209  		dstPath := filepath.Join(destDir, filepath.Clean(strings.TrimPrefix(hdr.Name, path)))
   210  		if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
   211  			return err
   212  		}
   213  		var mode os.FileMode = 0644
   214  		if hdr.Mode > 0 {
   215  			mode = os.FileMode(hdr.Mode)
   216  		}
   217  		dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   218  		if err != nil {
   219  			return err
   220  		}
   221  		n, err := io.Copy(dst, tr)
   222  		dst.Close()
   223  		if err != nil {
   224  			return err
   225  		} else if n != hdr.Size {
   226  			return fmt.Errorf("expected %s to be %d bytes but got %d", hdr.Name, hdr.Size, n)
   227  		}
   228  	}
   229  }
   230  
   231  // UploadManifest uploads the given manifest to swarm
   232  func (c *Client) UploadManifest(m *api.Manifest) (string, error) {
   233  	data, err := json.Marshal(m)
   234  	if err != nil {
   235  		return "", err
   236  	}
   237  	return c.UploadRaw(bytes.NewReader(data), int64(len(data)))
   238  }
   239  
   240  // DownloadManifest downloads a swarm manifest
   241  func (c *Client) DownloadManifest(hash string) (*api.Manifest, error) {
   242  	res, err := c.DownloadRaw(hash)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	defer res.Close()
   247  	var manifest api.Manifest
   248  	if err := json.NewDecoder(res).Decode(&manifest); err != nil {
   249  		return nil, err
   250  	}
   251  	return &manifest, nil
   252  }
   253  
   254  // List list files in a swarm manifest which have the given prefix, grouping
   255  // common prefixes using "/" as a delimiter.
   256  //
   257  // For example, if the manifest represents the following directory structure:
   258  //
   259  // file1.txt
   260  // file2.txt
   261  // dir1/file3.txt
   262  // dir1/dir2/file4.txt
   263  //
   264  // Then:
   265  //
   266  // - a prefix of ""      would return [dir1/, file1.txt, file2.txt]
   267  // - a prefix of "file"  would return [file1.txt, file2.txt]
   268  // - a prefix of "dir1/" would return [dir1/dir2/, dir1/file3.txt]
   269  //
   270  // where entries ending with "/" are common prefixes.
   271  func (c *Client) List(hash, prefix string) (*api.ManifestList, error) {
   272  	res, err := http.DefaultClient.Get(c.Gateway + "/bzz-list:/" + hash + "/" + prefix)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	defer res.Body.Close()
   277  	if res.StatusCode != http.StatusOK {
   278  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   279  	}
   280  	var list api.ManifestList
   281  	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
   282  		return nil, err
   283  	}
   284  	return &list, nil
   285  }
   286  
   287  // Uploader uploads files to swarm using a provided UploadFn
   288  type Uploader interface {
   289  	Upload(UploadFn) error
   290  }
   291  
   292  type UploaderFunc func(UploadFn) error
   293  
   294  func (u UploaderFunc) Upload(upload UploadFn) error {
   295  	return u(upload)
   296  }
   297  
   298  // DirectoryUploader uploads all files in a directory, optionally uploading
   299  // a file to the default path
   300  type DirectoryUploader struct {
   301  	Dir         string
   302  	DefaultPath string
   303  }
   304  
   305  // Upload performs the upload of the directory and default path
   306  func (d *DirectoryUploader) Upload(upload UploadFn) error {
   307  	if d.DefaultPath != "" {
   308  		file, err := Open(d.DefaultPath)
   309  		if err != nil {
   310  			return err
   311  		}
   312  		if err := upload(file); err != nil {
   313  			return err
   314  		}
   315  	}
   316  	return filepath.Walk(d.Dir, func(path string, f os.FileInfo, err error) error {
   317  		if err != nil {
   318  			return err
   319  		}
   320  		if f.IsDir() {
   321  			return nil
   322  		}
   323  		file, err := Open(path)
   324  		if err != nil {
   325  			return err
   326  		}
   327  		relPath, err := filepath.Rel(d.Dir, path)
   328  		if err != nil {
   329  			return err
   330  		}
   331  		file.Path = filepath.ToSlash(relPath)
   332  		return upload(file)
   333  	})
   334  }
   335  
   336  // FileUploader uploads a single file
   337  type FileUploader struct {
   338  	File *File
   339  }
   340  
   341  // Upload performs the upload of the file
   342  func (f *FileUploader) Upload(upload UploadFn) error {
   343  	return upload(f.File)
   344  }
   345  
   346  // UploadFn is the type of function passed to an Uploader to perform the upload
   347  // of a single file (for example, a directory uploader would call a provided
   348  // UploadFn for each file in the directory tree)
   349  type UploadFn func(file *File) error
   350  
   351  // TarUpload uses the given Uploader to upload files to swarm as a tar stream,
   352  // returning the resulting manifest hash
   353  func (c *Client) TarUpload(hash string, uploader Uploader) (string, error) {
   354  	reqR, reqW := io.Pipe()
   355  	defer reqR.Close()
   356  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
   357  	if err != nil {
   358  		return "", err
   359  	}
   360  	req.Header.Set("Content-Type", "application/x-tar")
   361  
   362  	// use 'Expect: 100-continue' so we don't send the request body if
   363  	// the server refuses the request
   364  	req.Header.Set("Expect", "100-continue")
   365  
   366  	tw := tar.NewWriter(reqW)
   367  
   368  	// define an UploadFn which adds files to the tar stream
   369  	uploadFn := func(file *File) error {
   370  		hdr := &tar.Header{
   371  			Name:    file.Path,
   372  			Mode:    file.Mode,
   373  			Size:    file.Size,
   374  			ModTime: file.ModTime,
   375  			Xattrs: map[string]string{
   376  				"user.swarm.content-type": file.ContentType,
   377  			},
   378  		}
   379  		if err := tw.WriteHeader(hdr); err != nil {
   380  			return err
   381  		}
   382  		_, err = io.Copy(tw, file)
   383  		return err
   384  	}
   385  
   386  	// run the upload in a goroutine so we can send the request headers and
   387  	// wait for a '100 Continue' response before sending the tar stream
   388  	go func() {
   389  		err := uploader.Upload(uploadFn)
   390  		if err == nil {
   391  			err = tw.Close()
   392  		}
   393  		reqW.CloseWithError(err)
   394  	}()
   395  
   396  	res, err := http.DefaultClient.Do(req)
   397  	if err != nil {
   398  		return "", err
   399  	}
   400  	defer res.Body.Close()
   401  	if res.StatusCode != http.StatusOK {
   402  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   403  	}
   404  	data, err := ioutil.ReadAll(res.Body)
   405  	if err != nil {
   406  		return "", err
   407  	}
   408  	return string(data), nil
   409  }
   410  
   411  // MultipartUpload uses the given Uploader to upload files to swarm as a
   412  // multipart form, returning the resulting manifest hash
   413  func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) {
   414  	reqR, reqW := io.Pipe()
   415  	defer reqR.Close()
   416  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
   417  	if err != nil {
   418  		return "", err
   419  	}
   420  
   421  	// use 'Expect: 100-continue' so we don't send the request body if
   422  	// the server refuses the request
   423  	req.Header.Set("Expect", "100-continue")
   424  
   425  	mw := multipart.NewWriter(reqW)
   426  	req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))
   427  
   428  	// define an UploadFn which adds files to the multipart form
   429  	uploadFn := func(file *File) error {
   430  		hdr := make(textproto.MIMEHeader)
   431  		hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", file.Path))
   432  		hdr.Set("Content-Type", file.ContentType)
   433  		hdr.Set("Content-Length", strconv.FormatInt(file.Size, 10))
   434  		w, err := mw.CreatePart(hdr)
   435  		if err != nil {
   436  			return err
   437  		}
   438  		_, err = io.Copy(w, file)
   439  		return err
   440  	}
   441  
   442  	// run the upload in a goroutine so we can send the request headers and
   443  	// wait for a '100 Continue' response before sending the multipart form
   444  	go func() {
   445  		err := uploader.Upload(uploadFn)
   446  		if err == nil {
   447  			err = mw.Close()
   448  		}
   449  		reqW.CloseWithError(err)
   450  	}()
   451  
   452  	res, err := http.DefaultClient.Do(req)
   453  	if err != nil {
   454  		return "", err
   455  	}
   456  	defer res.Body.Close()
   457  	if res.StatusCode != http.StatusOK {
   458  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   459  	}
   460  	data, err := ioutil.ReadAll(res.Body)
   461  	if err != nil {
   462  		return "", err
   463  	}
   464  	return string(data), nil
   465  }