client.go 15 KB

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