github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/swarm/api/client/client.go (about)

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