client.go 12 KB

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