github.com/daragao/go-ethereum@v1.8.14-0.20180809141559-45eaef243198/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  	"regexp"
    34  	"strconv"
    35  	"strings"
    36  
    37  	"github.com/ethereum/go-ethereum/swarm/api"
    38  	"github.com/ethereum/go-ethereum/swarm/storage/mru"
    39  )
    40  
    41  var (
    42  	DefaultGateway = "http://localhost:8500"
    43  	DefaultClient  = NewClient(DefaultGateway)
    44  )
    45  
    46  func NewClient(gateway string) *Client {
    47  	return &Client{
    48  		Gateway: gateway,
    49  	}
    50  }
    51  
    52  // Client wraps interaction with a swarm HTTP gateway.
    53  type Client struct {
    54  	Gateway string
    55  }
    56  
    57  // UploadRaw uploads raw data to swarm and returns the resulting hash. If toEncrypt is true it
    58  // uploads encrypted data
    59  func (c *Client) UploadRaw(r io.Reader, size int64, toEncrypt bool) (string, error) {
    60  	if size <= 0 {
    61  		return "", errors.New("data size must be greater than zero")
    62  	}
    63  	addr := ""
    64  	if toEncrypt {
    65  		addr = "encrypt"
    66  	}
    67  	req, err := http.NewRequest("POST", c.Gateway+"/bzz-raw:/"+addr, r)
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	req.ContentLength = size
    72  	res, err := http.DefaultClient.Do(req)
    73  	if err != nil {
    74  		return "", err
    75  	}
    76  	defer res.Body.Close()
    77  	if res.StatusCode != http.StatusOK {
    78  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
    79  	}
    80  	data, err := ioutil.ReadAll(res.Body)
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  	return string(data), nil
    85  }
    86  
    87  // DownloadRaw downloads raw data from swarm and it returns a ReadCloser and a bool whether the
    88  // content was encrypted
    89  func (c *Client) DownloadRaw(hash string) (io.ReadCloser, bool, error) {
    90  	uri := c.Gateway + "/bzz-raw:/" + hash
    91  	res, err := http.DefaultClient.Get(uri)
    92  	if err != nil {
    93  		return nil, false, err
    94  	}
    95  	if res.StatusCode != http.StatusOK {
    96  		res.Body.Close()
    97  		return nil, false, fmt.Errorf("unexpected HTTP status: %s", res.Status)
    98  	}
    99  	isEncrypted := (res.Header.Get("X-Decrypted") == "true")
   100  	return res.Body, isEncrypted, nil
   101  }
   102  
   103  // File represents a file in a swarm manifest and is used for uploading and
   104  // downloading content to and from swarm
   105  type File struct {
   106  	io.ReadCloser
   107  	api.ManifestEntry
   108  }
   109  
   110  // Open opens a local file which can then be passed to client.Upload to upload
   111  // it to swarm
   112  func Open(path string) (*File, error) {
   113  	f, err := os.Open(path)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	stat, err := f.Stat()
   118  	if err != nil {
   119  		f.Close()
   120  		return nil, err
   121  	}
   122  	return &File{
   123  		ReadCloser: f,
   124  		ManifestEntry: api.ManifestEntry{
   125  			ContentType: mime.TypeByExtension(filepath.Ext(path)),
   126  			Mode:        int64(stat.Mode()),
   127  			Size:        stat.Size(),
   128  			ModTime:     stat.ModTime(),
   129  		},
   130  	}, nil
   131  }
   132  
   133  // Upload uploads a file to swarm and either adds it to an existing manifest
   134  // (if the manifest argument is non-empty) or creates a new manifest containing
   135  // the file, returning the resulting manifest hash (the file will then be
   136  // available at bzz:/<hash>/<path>)
   137  func (c *Client) Upload(file *File, manifest string, toEncrypt bool) (string, error) {
   138  	if file.Size <= 0 {
   139  		return "", errors.New("file size must be greater than zero")
   140  	}
   141  	return c.TarUpload(manifest, &FileUploader{file}, toEncrypt)
   142  }
   143  
   144  // Download downloads a file with the given path from the swarm manifest with
   145  // the given hash (i.e. it gets bzz:/<hash>/<path>)
   146  func (c *Client) Download(hash, path string) (*File, error) {
   147  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   148  	res, err := http.DefaultClient.Get(uri)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	if res.StatusCode != http.StatusOK {
   153  		res.Body.Close()
   154  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   155  	}
   156  	return &File{
   157  		ReadCloser: res.Body,
   158  		ManifestEntry: api.ManifestEntry{
   159  			ContentType: res.Header.Get("Content-Type"),
   160  			Size:        res.ContentLength,
   161  		},
   162  	}, nil
   163  }
   164  
   165  // UploadDirectory uploads a directory tree to swarm and either adds the files
   166  // to an existing manifest (if the manifest argument is non-empty) or creates a
   167  // new manifest, returning the resulting manifest hash (files from the
   168  // directory will then be available at bzz:/<hash>/path/to/file), with
   169  // the file specified in defaultPath being uploaded to the root of the manifest
   170  // (i.e. bzz:/<hash>/)
   171  func (c *Client) UploadDirectory(dir, defaultPath, manifest string, toEncrypt bool) (string, error) {
   172  	stat, err := os.Stat(dir)
   173  	if err != nil {
   174  		return "", err
   175  	} else if !stat.IsDir() {
   176  		return "", fmt.Errorf("not a directory: %s", dir)
   177  	}
   178  	return c.TarUpload(manifest, &DirectoryUploader{dir, defaultPath}, toEncrypt)
   179  }
   180  
   181  // DownloadDirectory downloads the files contained in a swarm manifest under
   182  // the given path into a local directory (existing files will be overwritten)
   183  func (c *Client) DownloadDirectory(hash, path, destDir string) error {
   184  	stat, err := os.Stat(destDir)
   185  	if err != nil {
   186  		return err
   187  	} else if !stat.IsDir() {
   188  		return fmt.Errorf("not a directory: %s", destDir)
   189  	}
   190  
   191  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   192  	req, err := http.NewRequest("GET", uri, nil)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	req.Header.Set("Accept", "application/x-tar")
   197  	res, err := http.DefaultClient.Do(req)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	defer res.Body.Close()
   202  	if res.StatusCode != http.StatusOK {
   203  		return fmt.Errorf("unexpected HTTP status: %s", res.Status)
   204  	}
   205  	tr := tar.NewReader(res.Body)
   206  	for {
   207  		hdr, err := tr.Next()
   208  		if err == io.EOF {
   209  			return nil
   210  		} else if err != nil {
   211  			return err
   212  		}
   213  		// ignore the default path file
   214  		if hdr.Name == "" {
   215  			continue
   216  		}
   217  
   218  		dstPath := filepath.Join(destDir, filepath.Clean(strings.TrimPrefix(hdr.Name, path)))
   219  		if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
   220  			return err
   221  		}
   222  		var mode os.FileMode = 0644
   223  		if hdr.Mode > 0 {
   224  			mode = os.FileMode(hdr.Mode)
   225  		}
   226  		dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   227  		if err != nil {
   228  			return err
   229  		}
   230  		n, err := io.Copy(dst, tr)
   231  		dst.Close()
   232  		if err != nil {
   233  			return err
   234  		} else if n != hdr.Size {
   235  			return fmt.Errorf("expected %s to be %d bytes but got %d", hdr.Name, hdr.Size, n)
   236  		}
   237  	}
   238  }
   239  
   240  // DownloadFile downloads a single file into the destination directory
   241  // if the manifest entry does not specify a file name - it will fallback
   242  // to the hash of the file as a filename
   243  func (c *Client) DownloadFile(hash, path, dest string) error {
   244  	hasDestinationFilename := false
   245  	if stat, err := os.Stat(dest); err == nil {
   246  		hasDestinationFilename = !stat.IsDir()
   247  	} else {
   248  		if os.IsNotExist(err) {
   249  			// does not exist - should be created
   250  			hasDestinationFilename = true
   251  		} else {
   252  			return fmt.Errorf("could not stat path: %v", err)
   253  		}
   254  	}
   255  
   256  	manifestList, err := c.List(hash, path)
   257  	if err != nil {
   258  		return fmt.Errorf("could not list manifest: %v", err)
   259  	}
   260  
   261  	switch len(manifestList.Entries) {
   262  	case 0:
   263  		return fmt.Errorf("could not find path requested at manifest address. make sure the path you've specified is correct")
   264  	case 1:
   265  		//continue
   266  	default:
   267  		return fmt.Errorf("got too many matches for this path")
   268  	}
   269  
   270  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   271  	req, err := http.NewRequest("GET", uri, nil)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	res, err := http.DefaultClient.Do(req)
   276  	if err != nil {
   277  		return err
   278  	}
   279  	defer res.Body.Close()
   280  
   281  	if res.StatusCode != http.StatusOK {
   282  		return fmt.Errorf("unexpected HTTP status: expected 200 OK, got %d", res.StatusCode)
   283  	}
   284  	filename := ""
   285  	if hasDestinationFilename {
   286  		filename = dest
   287  	} else {
   288  		// try to assert
   289  		re := regexp.MustCompile("[^/]+$") //everything after last slash
   290  
   291  		if results := re.FindAllString(path, -1); len(results) > 0 {
   292  			filename = results[len(results)-1]
   293  		} else {
   294  			if entry := manifestList.Entries[0]; entry.Path != "" && entry.Path != "/" {
   295  				filename = entry.Path
   296  			} else {
   297  				// assume hash as name if there's nothing from the command line
   298  				filename = hash
   299  			}
   300  		}
   301  		filename = filepath.Join(dest, filename)
   302  	}
   303  	filePath, err := filepath.Abs(filename)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	if err := os.MkdirAll(filepath.Dir(filePath), 0777); err != nil {
   309  		return err
   310  	}
   311  
   312  	dst, err := os.Create(filename)
   313  	if err != nil {
   314  		return err
   315  	}
   316  	defer dst.Close()
   317  
   318  	_, err = io.Copy(dst, res.Body)
   319  	return err
   320  }
   321  
   322  // UploadManifest uploads the given manifest to swarm
   323  func (c *Client) UploadManifest(m *api.Manifest, toEncrypt bool) (string, error) {
   324  	data, err := json.Marshal(m)
   325  	if err != nil {
   326  		return "", err
   327  	}
   328  	return c.UploadRaw(bytes.NewReader(data), int64(len(data)), toEncrypt)
   329  }
   330  
   331  // DownloadManifest downloads a swarm manifest
   332  func (c *Client) DownloadManifest(hash string) (*api.Manifest, bool, error) {
   333  	res, isEncrypted, err := c.DownloadRaw(hash)
   334  	if err != nil {
   335  		return nil, isEncrypted, err
   336  	}
   337  	defer res.Close()
   338  	var manifest api.Manifest
   339  	if err := json.NewDecoder(res).Decode(&manifest); err != nil {
   340  		return nil, isEncrypted, err
   341  	}
   342  	return &manifest, isEncrypted, nil
   343  }
   344  
   345  // List list files in a swarm manifest which have the given prefix, grouping
   346  // common prefixes using "/" as a delimiter.
   347  //
   348  // For example, if the manifest represents the following directory structure:
   349  //
   350  // file1.txt
   351  // file2.txt
   352  // dir1/file3.txt
   353  // dir1/dir2/file4.txt
   354  //
   355  // Then:
   356  //
   357  // - a prefix of ""      would return [dir1/, file1.txt, file2.txt]
   358  // - a prefix of "file"  would return [file1.txt, file2.txt]
   359  // - a prefix of "dir1/" would return [dir1/dir2/, dir1/file3.txt]
   360  //
   361  // where entries ending with "/" are common prefixes.
   362  func (c *Client) List(hash, prefix string) (*api.ManifestList, error) {
   363  	res, err := http.DefaultClient.Get(c.Gateway + "/bzz-list:/" + hash + "/" + prefix)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  	defer res.Body.Close()
   368  	if res.StatusCode != http.StatusOK {
   369  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   370  	}
   371  	var list api.ManifestList
   372  	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
   373  		return nil, err
   374  	}
   375  	return &list, nil
   376  }
   377  
   378  // Uploader uploads files to swarm using a provided UploadFn
   379  type Uploader interface {
   380  	Upload(UploadFn) error
   381  }
   382  
   383  type UploaderFunc func(UploadFn) error
   384  
   385  func (u UploaderFunc) Upload(upload UploadFn) error {
   386  	return u(upload)
   387  }
   388  
   389  // DirectoryUploader uploads all files in a directory, optionally uploading
   390  // a file to the default path
   391  type DirectoryUploader struct {
   392  	Dir         string
   393  	DefaultPath string
   394  }
   395  
   396  // Upload performs the upload of the directory and default path
   397  func (d *DirectoryUploader) Upload(upload UploadFn) error {
   398  	if d.DefaultPath != "" {
   399  		file, err := Open(d.DefaultPath)
   400  		if err != nil {
   401  			return err
   402  		}
   403  		if err := upload(file); err != nil {
   404  			return err
   405  		}
   406  	}
   407  	return filepath.Walk(d.Dir, func(path string, f os.FileInfo, err error) error {
   408  		if err != nil {
   409  			return err
   410  		}
   411  		if f.IsDir() {
   412  			return nil
   413  		}
   414  		file, err := Open(path)
   415  		if err != nil {
   416  			return err
   417  		}
   418  		relPath, err := filepath.Rel(d.Dir, path)
   419  		if err != nil {
   420  			return err
   421  		}
   422  		file.Path = filepath.ToSlash(relPath)
   423  		return upload(file)
   424  	})
   425  }
   426  
   427  // FileUploader uploads a single file
   428  type FileUploader struct {
   429  	File *File
   430  }
   431  
   432  // Upload performs the upload of the file
   433  func (f *FileUploader) Upload(upload UploadFn) error {
   434  	return upload(f.File)
   435  }
   436  
   437  // UploadFn is the type of function passed to an Uploader to perform the upload
   438  // of a single file (for example, a directory uploader would call a provided
   439  // UploadFn for each file in the directory tree)
   440  type UploadFn func(file *File) error
   441  
   442  // TarUpload uses the given Uploader to upload files to swarm as a tar stream,
   443  // returning the resulting manifest hash
   444  func (c *Client) TarUpload(hash string, uploader Uploader, toEncrypt bool) (string, error) {
   445  	reqR, reqW := io.Pipe()
   446  	defer reqR.Close()
   447  	addr := hash
   448  
   449  	// If there is a hash already (a manifest), then that manifest will determine if the upload has
   450  	// to be encrypted or not. If there is no manifest then the toEncrypt parameter decides if
   451  	// there is encryption or not.
   452  	if hash == "" && toEncrypt {
   453  		// This is the built-in address for the encrypted upload endpoint
   454  		addr = "encrypt"
   455  	}
   456  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+addr, reqR)
   457  	if err != nil {
   458  		return "", err
   459  	}
   460  	req.Header.Set("Content-Type", "application/x-tar")
   461  
   462  	// use 'Expect: 100-continue' so we don't send the request body if
   463  	// the server refuses the request
   464  	req.Header.Set("Expect", "100-continue")
   465  
   466  	tw := tar.NewWriter(reqW)
   467  
   468  	// define an UploadFn which adds files to the tar stream
   469  	uploadFn := func(file *File) error {
   470  		hdr := &tar.Header{
   471  			Name:    file.Path,
   472  			Mode:    file.Mode,
   473  			Size:    file.Size,
   474  			ModTime: file.ModTime,
   475  			Xattrs: map[string]string{
   476  				"user.swarm.content-type": file.ContentType,
   477  			},
   478  		}
   479  		if err := tw.WriteHeader(hdr); err != nil {
   480  			return err
   481  		}
   482  		_, err = io.Copy(tw, file)
   483  		return err
   484  	}
   485  
   486  	// run the upload in a goroutine so we can send the request headers and
   487  	// wait for a '100 Continue' response before sending the tar stream
   488  	go func() {
   489  		err := uploader.Upload(uploadFn)
   490  		if err == nil {
   491  			err = tw.Close()
   492  		}
   493  		reqW.CloseWithError(err)
   494  	}()
   495  
   496  	res, err := http.DefaultClient.Do(req)
   497  	if err != nil {
   498  		return "", err
   499  	}
   500  	defer res.Body.Close()
   501  	if res.StatusCode != http.StatusOK {
   502  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   503  	}
   504  	data, err := ioutil.ReadAll(res.Body)
   505  	if err != nil {
   506  		return "", err
   507  	}
   508  	return string(data), nil
   509  }
   510  
   511  // MultipartUpload uses the given Uploader to upload files to swarm as a
   512  // multipart form, returning the resulting manifest hash
   513  func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) {
   514  	reqR, reqW := io.Pipe()
   515  	defer reqR.Close()
   516  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
   517  	if err != nil {
   518  		return "", err
   519  	}
   520  
   521  	// use 'Expect: 100-continue' so we don't send the request body if
   522  	// the server refuses the request
   523  	req.Header.Set("Expect", "100-continue")
   524  
   525  	mw := multipart.NewWriter(reqW)
   526  	req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))
   527  
   528  	// define an UploadFn which adds files to the multipart form
   529  	uploadFn := func(file *File) error {
   530  		hdr := make(textproto.MIMEHeader)
   531  		hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", file.Path))
   532  		hdr.Set("Content-Type", file.ContentType)
   533  		hdr.Set("Content-Length", strconv.FormatInt(file.Size, 10))
   534  		w, err := mw.CreatePart(hdr)
   535  		if err != nil {
   536  			return err
   537  		}
   538  		_, err = io.Copy(w, file)
   539  		return err
   540  	}
   541  
   542  	// run the upload in a goroutine so we can send the request headers and
   543  	// wait for a '100 Continue' response before sending the multipart form
   544  	go func() {
   545  		err := uploader.Upload(uploadFn)
   546  		if err == nil {
   547  			err = mw.Close()
   548  		}
   549  		reqW.CloseWithError(err)
   550  	}()
   551  
   552  	res, err := http.DefaultClient.Do(req)
   553  	if err != nil {
   554  		return "", err
   555  	}
   556  	defer res.Body.Close()
   557  	if res.StatusCode != http.StatusOK {
   558  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   559  	}
   560  	data, err := ioutil.ReadAll(res.Body)
   561  	if err != nil {
   562  		return "", err
   563  	}
   564  	return string(data), nil
   565  }
   566  
   567  // CreateResource creates a Mutable Resource with the given name and frequency, initializing it with the provided
   568  // data. Data is interpreted as multihash or not depending on the multihash parameter.
   569  // startTime=0 means "now"
   570  // Returns the resulting Mutable Resource manifest address that you can use to include in an ENS Resolver (setContent)
   571  // or reference future updates (Client.UpdateResource)
   572  func (c *Client) CreateResource(request *mru.Request) (string, error) {
   573  	responseStream, err := c.updateResource(request)
   574  	if err != nil {
   575  		return "", err
   576  	}
   577  	defer responseStream.Close()
   578  
   579  	body, err := ioutil.ReadAll(responseStream)
   580  	if err != nil {
   581  		return "", err
   582  	}
   583  
   584  	var manifestAddress string
   585  	if err = json.Unmarshal(body, &manifestAddress); err != nil {
   586  		return "", err
   587  	}
   588  	return manifestAddress, nil
   589  }
   590  
   591  // UpdateResource allows you to set a new version of your content
   592  func (c *Client) UpdateResource(request *mru.Request) error {
   593  	_, err := c.updateResource(request)
   594  	return err
   595  }
   596  
   597  func (c *Client) updateResource(request *mru.Request) (io.ReadCloser, error) {
   598  	body, err := request.MarshalJSON()
   599  	if err != nil {
   600  		return nil, err
   601  	}
   602  
   603  	req, err := http.NewRequest("POST", c.Gateway+"/bzz-resource:/", bytes.NewBuffer(body))
   604  	if err != nil {
   605  		return nil, err
   606  	}
   607  
   608  	res, err := http.DefaultClient.Do(req)
   609  	if err != nil {
   610  		return nil, err
   611  	}
   612  
   613  	return res.Body, nil
   614  
   615  }
   616  
   617  // GetResource returns a byte stream with the raw content of the resource
   618  // manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver
   619  // points to that address
   620  func (c *Client) GetResource(manifestAddressOrDomain string) (io.ReadCloser, error) {
   621  
   622  	res, err := http.Get(c.Gateway + "/bzz-resource:/" + manifestAddressOrDomain)
   623  	if err != nil {
   624  		return nil, err
   625  	}
   626  	return res.Body, nil
   627  
   628  }
   629  
   630  // GetResourceMetadata returns a structure that describes the Mutable Resource
   631  // manifestAddressOrDomain is the address you obtained in CreateResource or an ENS domain whose Resolver
   632  // points to that address
   633  func (c *Client) GetResourceMetadata(manifestAddressOrDomain string) (*mru.Request, error) {
   634  
   635  	responseStream, err := c.GetResource(manifestAddressOrDomain + "/meta")
   636  	if err != nil {
   637  		return nil, err
   638  	}
   639  	defer responseStream.Close()
   640  
   641  	body, err := ioutil.ReadAll(responseStream)
   642  	if err != nil {
   643  		return nil, err
   644  	}
   645  
   646  	var metadata mru.Request
   647  	if err := metadata.UnmarshalJSON(body); err != nil {
   648  		return nil, err
   649  	}
   650  	return &metadata, nil
   651  }