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