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