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