client.go 18 KB

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