client.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. // Copyright 2016 The go-ethereum Authors
  2. // This file is part of go-ethereum.
  3. //
  4. // go-ethereum is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU 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. // go-ethereum 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 General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
  16. package client
  17. import (
  18. "bytes"
  19. "encoding/json"
  20. "fmt"
  21. "io"
  22. "io/ioutil"
  23. "mime"
  24. "net/http"
  25. "os"
  26. "path/filepath"
  27. "strings"
  28. "github.com/ethereum/go-ethereum/log"
  29. )
  30. var (
  31. DefaultGateway = "http://localhost:8500"
  32. DefaultClient = NewClient(DefaultGateway)
  33. )
  34. // Manifest represents a swarm manifest.
  35. type Manifest struct {
  36. Entries []ManifestEntry `json:"entries,omitempty"`
  37. }
  38. // ManifestEntry represents an entry in a swarm manifest.
  39. type ManifestEntry struct {
  40. Hash string `json:"hash,omitempty"`
  41. ContentType string `json:"contentType,omitempty"`
  42. Path string `json:"path,omitempty"`
  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. func (c *Client) UploadDirectory(dir string, defaultPath string) (string, error) {
  54. mhash, err := c.postRaw("application/json", 2, ioutil.NopCloser(bytes.NewReader([]byte("{}"))))
  55. if err != nil {
  56. return "", fmt.Errorf("failed to upload empty manifest")
  57. }
  58. if len(defaultPath) > 0 {
  59. fi, err := os.Stat(defaultPath)
  60. if err != nil {
  61. return "", err
  62. }
  63. mhash, err = c.uploadToManifest(mhash, "", defaultPath, fi)
  64. if err != nil {
  65. return "", err
  66. }
  67. }
  68. prefix := filepath.ToSlash(filepath.Clean(dir)) + "/"
  69. err = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
  70. if err != nil || fi.IsDir() {
  71. return err
  72. }
  73. if !strings.HasPrefix(path, dir) {
  74. return fmt.Errorf("path %s outside directory %s", path, dir)
  75. }
  76. uripath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean(path)), prefix)
  77. mhash, err = c.uploadToManifest(mhash, uripath, path, fi)
  78. return err
  79. })
  80. return mhash, err
  81. }
  82. func (c *Client) UploadFile(file string, fi os.FileInfo, mimetype_hint string) (ManifestEntry, error) {
  83. var mimetype string
  84. hash, err := c.uploadFileContent(file, fi)
  85. if mimetype_hint != "" {
  86. mimetype = mimetype_hint
  87. log.Info("Mime type set by override", "mime", mimetype)
  88. } else {
  89. ext := filepath.Ext(file)
  90. log.Info("Ext", "ext", ext, "file", file)
  91. if ext != "" {
  92. mimetype = mime.TypeByExtension(filepath.Ext(fi.Name()))
  93. log.Info("Mime type set by fileextension", "mime", mimetype, "ext", filepath.Ext(file))
  94. } else {
  95. f, err := os.Open(file)
  96. if err == nil {
  97. first512 := make([]byte, 512)
  98. fread, _ := f.ReadAt(first512, 0)
  99. if fread > 0 {
  100. mimetype = http.DetectContentType(first512[:fread])
  101. log.Info("Mime type set by autodetection", "mime", mimetype)
  102. }
  103. }
  104. f.Close()
  105. }
  106. }
  107. m := ManifestEntry{
  108. Hash: hash,
  109. ContentType: mime.TypeByExtension(filepath.Ext(fi.Name())),
  110. }
  111. return m, err
  112. }
  113. func (c *Client) uploadFileContent(file string, fi os.FileInfo) (string, error) {
  114. fd, err := os.Open(file)
  115. if err != nil {
  116. return "", err
  117. }
  118. defer fd.Close()
  119. log.Info("Uploading swarm content", "file", file, "bytes", fi.Size())
  120. return c.postRaw("application/octet-stream", fi.Size(), fd)
  121. }
  122. func (c *Client) UploadManifest(m Manifest) (string, error) {
  123. jsm, err := json.Marshal(m)
  124. if err != nil {
  125. panic(err)
  126. }
  127. log.Info("Uploading swarm manifest")
  128. return c.postRaw("application/json", int64(len(jsm)), ioutil.NopCloser(bytes.NewReader(jsm)))
  129. }
  130. func (c *Client) uploadToManifest(mhash string, path string, fpath string, fi os.FileInfo) (string, error) {
  131. fd, err := os.Open(fpath)
  132. if err != nil {
  133. return "", err
  134. }
  135. defer fd.Close()
  136. log.Info("Uploading swarm content and path", "file", fpath, "bytes", fi.Size(), "path", path)
  137. req, err := http.NewRequest("PUT", c.Gateway+"/bzz:/"+mhash+"/"+path, fd)
  138. if err != nil {
  139. return "", err
  140. }
  141. req.Header.Set("content-type", mime.TypeByExtension(filepath.Ext(fi.Name())))
  142. req.ContentLength = fi.Size()
  143. resp, err := http.DefaultClient.Do(req)
  144. if err != nil {
  145. return "", err
  146. }
  147. defer resp.Body.Close()
  148. if resp.StatusCode >= 400 {
  149. return "", fmt.Errorf("bad status: %s", resp.Status)
  150. }
  151. content, err := ioutil.ReadAll(resp.Body)
  152. return string(content), err
  153. }
  154. func (c *Client) postRaw(mimetype string, size int64, body io.ReadCloser) (string, error) {
  155. req, err := http.NewRequest("POST", c.Gateway+"/bzzr:/", body)
  156. if err != nil {
  157. return "", err
  158. }
  159. req.Header.Set("content-type", mimetype)
  160. req.ContentLength = size
  161. resp, err := http.DefaultClient.Do(req)
  162. if err != nil {
  163. return "", err
  164. }
  165. defer resp.Body.Close()
  166. if resp.StatusCode >= 400 {
  167. return "", fmt.Errorf("bad status: %s", resp.Status)
  168. }
  169. content, err := ioutil.ReadAll(resp.Body)
  170. return string(content), err
  171. }
  172. func (c *Client) DownloadManifest(mhash string) (Manifest, error) {
  173. mroot := Manifest{}
  174. req, err := http.NewRequest("GET", c.Gateway+"/bzzr:/"+mhash, nil)
  175. if err != nil {
  176. return mroot, err
  177. }
  178. resp, err := http.DefaultClient.Do(req)
  179. if err != nil {
  180. return mroot, err
  181. }
  182. defer resp.Body.Close()
  183. if resp.StatusCode >= 400 {
  184. return mroot, fmt.Errorf("bad status: %s", resp.Status)
  185. }
  186. content, err := ioutil.ReadAll(resp.Body)
  187. err = json.Unmarshal(content, &mroot)
  188. if err != nil {
  189. return mroot, fmt.Errorf("Manifest %v is malformed: %v", mhash, err)
  190. }
  191. return mroot, err
  192. }
  193. // ManifestFileList downloads the manifest with the given hash and generates a
  194. // list of files and directory prefixes which have the specified prefix.
  195. //
  196. // For example, if the manifest represents the following directory structure:
  197. //
  198. // file1.txt
  199. // file2.txt
  200. // dir1/file3.txt
  201. // dir1/dir2/file4.txt
  202. //
  203. // Then:
  204. //
  205. // - a prefix of "" would return [dir1/, file1.txt, file2.txt]
  206. // - a prefix of "file" would return [file1.txt, file2.txt]
  207. // - a prefix of "dir1/" would return [dir1/dir2/, dir1/file3.txt]
  208. func (c *Client) ManifestFileList(hash, prefix string) (entries []ManifestEntry, err error) {
  209. manifest, err := c.DownloadManifest(hash)
  210. if err != nil {
  211. return nil, err
  212. }
  213. // handleFile handles a manifest entry which is a direct reference to a
  214. // file (i.e. it is not a swarm manifest)
  215. handleFile := func(entry ManifestEntry) {
  216. // ignore the file if it doesn't have the specified prefix
  217. if !strings.HasPrefix(entry.Path, prefix) {
  218. return
  219. }
  220. // if the path after the prefix contains a directory separator,
  221. // add a directory prefix to the entries, otherwise add the
  222. // file
  223. suffix := strings.TrimPrefix(entry.Path, prefix)
  224. if sepIndex := strings.Index(suffix, "/"); sepIndex > -1 {
  225. entries = append(entries, ManifestEntry{
  226. Path: prefix + suffix[:sepIndex+1],
  227. ContentType: "DIR",
  228. })
  229. } else {
  230. if entry.Path == "" {
  231. entry.Path = "/"
  232. }
  233. entries = append(entries, entry)
  234. }
  235. }
  236. // handleManifest handles a manifest entry which is a reference to
  237. // another swarm manifest.
  238. handleManifest := func(entry ManifestEntry) error {
  239. // if the manifest's path is a prefix of the specified prefix
  240. // then just recurse into the manifest by stripping its path
  241. // from the prefix
  242. if strings.HasPrefix(prefix, entry.Path) {
  243. subPrefix := strings.TrimPrefix(prefix, entry.Path)
  244. subEntries, err := c.ManifestFileList(entry.Hash, subPrefix)
  245. if err != nil {
  246. return err
  247. }
  248. // prefix the manifest's path to the sub entries and
  249. // add them to the returned entries
  250. for i, subEntry := range subEntries {
  251. subEntry.Path = entry.Path + subEntry.Path
  252. subEntries[i] = subEntry
  253. }
  254. entries = append(entries, subEntries...)
  255. return nil
  256. }
  257. // if the manifest's path has the specified prefix, then if the
  258. // path after the prefix contains a directory separator, add a
  259. // directory prefix to the entries, otherwise recurse into the
  260. // manifest
  261. if strings.HasPrefix(entry.Path, prefix) {
  262. suffix := strings.TrimPrefix(entry.Path, prefix)
  263. sepIndex := strings.Index(suffix, "/")
  264. if sepIndex > -1 {
  265. entries = append(entries, ManifestEntry{
  266. Path: prefix + suffix[:sepIndex+1],
  267. ContentType: "DIR",
  268. })
  269. return nil
  270. }
  271. subEntries, err := c.ManifestFileList(entry.Hash, "")
  272. if err != nil {
  273. return err
  274. }
  275. // prefix the manifest's path to the sub entries and
  276. // add them to the returned entries
  277. for i, subEntry := range subEntries {
  278. subEntry.Path = entry.Path + subEntry.Path
  279. subEntries[i] = subEntry
  280. }
  281. entries = append(entries, subEntries...)
  282. return nil
  283. }
  284. return nil
  285. }
  286. for _, entry := range manifest.Entries {
  287. if entry.ContentType == "application/bzz-manifest+json" {
  288. if err := handleManifest(entry); err != nil {
  289. return nil, err
  290. }
  291. } else {
  292. handleFile(entry)
  293. }
  294. }
  295. return
  296. }