github.com/status-im/status-go@v1.1.0/ipfs/ipfs.go (about)

     1  package ipfs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/ipfs/go-cid"
    14  	"github.com/wealdtech/go-multicodec"
    15  
    16  	"github.com/ethereum/go-ethereum/common/hexutil"
    17  	"github.com/ethereum/go-ethereum/log"
    18  	"github.com/status-im/status-go/params"
    19  )
    20  
    21  const maxRequestsPerSecond = 3
    22  
    23  type taskResponse struct {
    24  	err      error
    25  	response []byte
    26  }
    27  
    28  type taskRequest struct {
    29  	cid      string
    30  	download bool
    31  	doneChan chan taskResponse
    32  }
    33  
    34  type Downloader struct {
    35  	ctx             context.Context
    36  	cancel          func()
    37  	ipfsDir         string
    38  	wg              sync.WaitGroup
    39  	rateLimiterChan chan taskRequest
    40  	inputTaskChan   chan taskRequest
    41  	client          *http.Client
    42  
    43  	quit chan struct{}
    44  }
    45  
    46  func NewDownloader(rootDir string) *Downloader {
    47  	ipfsDir := filepath.Clean(filepath.Join(rootDir, "./ipfs"))
    48  	if err := os.MkdirAll(ipfsDir, 0700); err != nil {
    49  		panic("could not create IPFSDir")
    50  	}
    51  
    52  	ctx, cancel := context.WithCancel(context.TODO())
    53  
    54  	d := &Downloader{
    55  		ctx:             ctx,
    56  		cancel:          cancel,
    57  		ipfsDir:         ipfsDir,
    58  		rateLimiterChan: make(chan taskRequest, maxRequestsPerSecond),
    59  		inputTaskChan:   make(chan taskRequest, 1000),
    60  		wg:              sync.WaitGroup{},
    61  		client: &http.Client{
    62  			Timeout: time.Second * 5,
    63  		},
    64  
    65  		quit: make(chan struct{}, 1),
    66  	}
    67  
    68  	go d.taskDispatcher()
    69  	go d.worker()
    70  
    71  	return d
    72  }
    73  
    74  func (d *Downloader) Stop() {
    75  	close(d.quit)
    76  
    77  	d.cancel()
    78  
    79  	d.wg.Wait()
    80  
    81  	close(d.inputTaskChan)
    82  	close(d.rateLimiterChan)
    83  }
    84  
    85  func (d *Downloader) worker() {
    86  	for request := range d.rateLimiterChan {
    87  		resp, err := d.download(request.cid, request.download)
    88  		request.doneChan <- taskResponse{
    89  			err:      err,
    90  			response: resp,
    91  		}
    92  	}
    93  }
    94  
    95  func (d *Downloader) taskDispatcher() {
    96  	ticker := time.NewTicker(time.Second / maxRequestsPerSecond)
    97  	defer ticker.Stop()
    98  
    99  	for {
   100  		<-ticker.C
   101  		request, ok := <-d.inputTaskChan
   102  		if !ok {
   103  			return
   104  		}
   105  		d.rateLimiterChan <- request
   106  
   107  	}
   108  }
   109  
   110  func hashToCid(hash []byte) (string, error) {
   111  	// contract response includes a contenthash, which needs to be decoded to reveal
   112  	// an IPFS identifier. Once decoded, download the content from IPFS. This content
   113  	// is in EDN format, ie https://ipfs.infura.io/ipfs/QmWVVLwVKCwkVNjYJrRzQWREVvEk917PhbHYAUhA1gECTM
   114  	// and it also needs to be decoded in to a nim type
   115  
   116  	data, codec, err := multicodec.RemoveCodec(hash)
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  
   121  	codecName, err := multicodec.Name(codec)
   122  	if err != nil {
   123  		return "", err
   124  	}
   125  
   126  	if codecName != "ipfs-ns" {
   127  		return "", errors.New("codecName is not ipfs-ns")
   128  	}
   129  
   130  	thisCID, err := cid.Parse(data)
   131  	if err != nil {
   132  		return "", err
   133  	}
   134  
   135  	return thisCID.Hash().B58String(), nil
   136  }
   137  
   138  func decodeStringHash(input string) (string, error) {
   139  	hash, err := hexutil.Decode("0x" + input)
   140  	if err != nil {
   141  		return "", err
   142  	}
   143  
   144  	cid, err := hashToCid(hash)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	return cid, nil
   150  }
   151  
   152  // Get checks if an IPFS image exists and returns it from cache
   153  // otherwise downloads it from INFURA's ipfs gateway
   154  func (d *Downloader) Get(hash string, download bool) ([]byte, error) {
   155  	cid, err := decodeStringHash(hash)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	exists, content, err := d.exists(cid)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	if exists {
   165  		return content, nil
   166  	}
   167  
   168  	doneChan := make(chan taskResponse, 1)
   169  
   170  	d.wg.Add(1)
   171  
   172  	d.inputTaskChan <- taskRequest{
   173  		cid:      cid,
   174  		download: download,
   175  		doneChan: doneChan,
   176  	}
   177  
   178  	done := <-doneChan
   179  	close(doneChan)
   180  
   181  	d.wg.Done()
   182  
   183  	return done.response, done.err
   184  }
   185  
   186  func (d *Downloader) exists(cid string) (bool, []byte, error) {
   187  	path := filepath.Join(d.ipfsDir, cid)
   188  	_, err := os.Stat(path)
   189  	if err == nil {
   190  		fileContent, err := os.ReadFile(path)
   191  		return true, fileContent, err
   192  	}
   193  
   194  	return false, nil, nil
   195  }
   196  
   197  func (d *Downloader) download(cid string, download bool) ([]byte, error) {
   198  	path := filepath.Join(d.ipfsDir, cid)
   199  
   200  	req, err := http.NewRequest(http.MethodGet, params.IpfsGatewayURL+cid, nil)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	req = req.WithContext(d.ctx)
   206  
   207  	resp, err := d.client.Do(req)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	defer func() {
   213  		if err := resp.Body.Close(); err != nil {
   214  			log.Error("failed to close the stickerpack request body", "err", err)
   215  		}
   216  	}()
   217  
   218  	if resp.StatusCode < 200 || resp.StatusCode > 299 {
   219  		log.Error("could not load data for", "cid", cid, "code", resp.StatusCode)
   220  		return nil, errors.New("could not load ipfs data")
   221  	}
   222  
   223  	fileContent, err := ioutil.ReadAll(resp.Body)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	if download {
   229  		// #nosec G306
   230  		err = os.WriteFile(path, fileContent, 0700)
   231  		if err != nil {
   232  			return nil, err
   233  		}
   234  	}
   235  
   236  	return fileContent, nil
   237  }