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