client.go 20 KB

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