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 }