client.go 20 KB

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