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