client.go 12.4 KB
Newer Older
1 2
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
3
//
4 5
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
6 7 8
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
9
// The go-ethereum library is distributed in the hope that it will be useful,
10 11
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
// GNU Lesser General Public License for more details.
13
//
14 15
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16 17 18 19

package client

import (
20
	"archive/tar"
21 22
	"bytes"
	"encoding/json"
23
	"errors"
24 25 26 27
	"fmt"
	"io"
	"io/ioutil"
	"mime"
28
	"mime/multipart"
29
	"net/http"
30
	"net/textproto"
31 32
	"os"
	"path/filepath"
33
	"strconv"
34 35
	"strings"

36
	"github.com/ethereum/go-ethereum/swarm/api"
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
)

var (
	DefaultGateway = "http://localhost:8500"
	DefaultClient  = NewClient(DefaultGateway)
)

func NewClient(gateway string) *Client {
	return &Client{
		Gateway: gateway,
	}
}

// Client wraps interaction with a swarm HTTP gateway.
type Client struct {
	Gateway string
}

55 56 57 58
// UploadRaw uploads raw data to swarm and returns the resulting hash
func (c *Client) UploadRaw(r io.Reader, size int64) (string, error) {
	if size <= 0 {
		return "", errors.New("data size must be greater than zero")
59
	}
60 61 62
	req, err := http.NewRequest("POST", c.Gateway+"/bzzr:/", r)
	if err != nil {
		return "", err
63
	}
64 65 66 67
	req.ContentLength = size
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
68
	}
69 70 71
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
72
	}
73
	data, err := ioutil.ReadAll(res.Body)
74 75 76
	if err != nil {
		return "", err
	}
77
	return string(data), nil
78 79
}

80 81 82 83
// DownloadRaw downloads raw data from swarm
func (c *Client) DownloadRaw(hash string) (io.ReadCloser, error) {
	uri := c.Gateway + "/bzzr:/" + hash
	res, err := http.DefaultClient.Get(uri)
84
	if err != nil {
85 86 87 88 89
		return nil, err
	}
	if res.StatusCode != http.StatusOK {
		res.Body.Close()
		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
90
	}
91
	return res.Body, nil
92 93
}

94 95 96 97 98 99 100 101 102 103 104
// File represents a file in a swarm manifest and is used for uploading and
// downloading content to and from swarm
type File struct {
	io.ReadCloser
	api.ManifestEntry
}

// Open opens a local file which can then be passed to client.Upload to upload
// it to swarm
func Open(path string) (*File, error) {
	f, err := os.Open(path)
105
	if err != nil {
106
		return nil, err
107
	}
108
	stat, err := f.Stat()
109
	if err != nil {
110 111
		f.Close()
		return nil, err
112
	}
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
	return &File{
		ReadCloser: f,
		ManifestEntry: api.ManifestEntry{
			ContentType: mime.TypeByExtension(filepath.Ext(path)),
			Mode:        int64(stat.Mode()),
			Size:        stat.Size(),
			ModTime:     stat.ModTime(),
		},
	}, nil
}

// Upload uploads a file to swarm and either adds it to an existing manifest
// (if the manifest argument is non-empty) or creates a new manifest containing
// the file, returning the resulting manifest hash (the file will then be
// available at bzz:/<hash>/<path>)
func (c *Client) Upload(file *File, manifest string) (string, error) {
	if file.Size <= 0 {
		return "", errors.New("file size must be greater than zero")
131
	}
132
	return c.TarUpload(manifest, &FileUploader{file})
133 134
}

135 136 137 138 139
// Download downloads a file with the given path from the swarm manifest with
// the given hash (i.e. it gets bzz:/<hash>/<path>)
func (c *Client) Download(hash, path string) (*File, error) {
	uri := c.Gateway + "/bzz:/" + hash + "/" + path
	res, err := http.DefaultClient.Get(uri)
140
	if err != nil {
141
		return nil, err
142
	}
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
	if res.StatusCode != http.StatusOK {
		res.Body.Close()
		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
	}
	return &File{
		ReadCloser: res.Body,
		ManifestEntry: api.ManifestEntry{
			ContentType: res.Header.Get("Content-Type"),
			Size:        res.ContentLength,
		},
	}, nil
}

// UploadDirectory uploads a directory tree to swarm and either adds the files
// to an existing manifest (if the manifest argument is non-empty) or creates a
// new manifest, returning the resulting manifest hash (files from the
// directory will then be available at bzz:/<hash>/path/to/file), with
// the file specified in defaultPath being uploaded to the root of the manifest
// (i.e. bzz:/<hash>/)
func (c *Client) UploadDirectory(dir, defaultPath, manifest string) (string, error) {
	stat, err := os.Stat(dir)
164 165
	if err != nil {
		return "", err
166 167
	} else if !stat.IsDir() {
		return "", fmt.Errorf("not a directory: %s", dir)
168
	}
169
	return c.TarUpload(manifest, &DirectoryUploader{dir, defaultPath})
170 171
}

172 173 174 175 176 177 178 179 180
// DownloadDirectory downloads the files contained in a swarm manifest under
// the given path into a local directory (existing files will be overwritten)
func (c *Client) DownloadDirectory(hash, path, destDir string) error {
	stat, err := os.Stat(destDir)
	if err != nil {
		return err
	} else if !stat.IsDir() {
		return fmt.Errorf("not a directory: %s", destDir)
	}
181

182 183
	uri := c.Gateway + "/bzz:/" + hash + "/" + path
	req, err := http.NewRequest("GET", uri, nil)
184
	if err != nil {
185
		return err
186
	}
187 188
	req.Header.Set("Accept", "application/x-tar")
	res, err := http.DefaultClient.Do(req)
189
	if err != nil {
190
		return err
191
	}
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected HTTP status: %s", res.Status)
	}
	tr := tar.NewReader(res.Body)
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			return nil
		} else if err != nil {
			return err
		}
		// ignore the default path file
		if hdr.Name == "" {
			continue
		}
208

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
		dstPath := filepath.Join(destDir, filepath.Clean(strings.TrimPrefix(hdr.Name, path)))
		if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
			return err
		}
		var mode os.FileMode = 0644
		if hdr.Mode > 0 {
			mode = os.FileMode(hdr.Mode)
		}
		dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
		if err != nil {
			return err
		}
		n, err := io.Copy(dst, tr)
		dst.Close()
		if err != nil {
			return err
		} else if n != hdr.Size {
			return fmt.Errorf("expected %s to be %d bytes but got %d", hdr.Name, hdr.Size, n)
		}
	}
}
230

231 232 233 234 235
// UploadManifest uploads the given manifest to swarm
func (c *Client) UploadManifest(m *api.Manifest) (string, error) {
	data, err := json.Marshal(m)
	if err != nil {
		return "", err
236
	}
237 238
	return c.UploadRaw(bytes.NewReader(data), int64(len(data)))
}
239

240 241 242
// DownloadManifest downloads a swarm manifest
func (c *Client) DownloadManifest(hash string) (*api.Manifest, error) {
	res, err := c.DownloadRaw(hash)
243
	if err != nil {
244 245 246 247 248 249
		return nil, err
	}
	defer res.Close()
	var manifest api.Manifest
	if err := json.NewDecoder(res).Decode(&manifest); err != nil {
		return nil, err
250
	}
251
	return &manifest, nil
252 253
}

254 255
// List list files in a swarm manifest which have the given prefix, grouping
// common prefixes using "/" as a delimiter.
256 257 258 259 260 261 262 263 264 265 266 267 268
//
// For example, if the manifest represents the following directory structure:
//
// file1.txt
// file2.txt
// dir1/file3.txt
// dir1/dir2/file4.txt
//
// Then:
//
// - a prefix of ""      would return [dir1/, file1.txt, file2.txt]
// - a prefix of "file"  would return [file1.txt, file2.txt]
// - a prefix of "dir1/" would return [dir1/dir2/, dir1/file3.txt]
269 270 271 272
//
// where entries ending with "/" are common prefixes.
func (c *Client) List(hash, prefix string) (*api.ManifestList, error) {
	res, err := http.DefaultClient.Get(c.Gateway + "/bzz:/" + hash + "/" + prefix + "?list=true")
273 274 275
	if err != nil {
		return nil, err
	}
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
	}
	var list api.ManifestList
	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
		return nil, err
	}
	return &list, nil
}

// Uploader uploads files to swarm using a provided UploadFn
type Uploader interface {
	Upload(UploadFn) error
}

type UploaderFunc func(UploadFn) error

func (u UploaderFunc) Upload(upload UploadFn) error {
	return u(upload)
}

// DirectoryUploader uploads all files in a directory, optionally uploading
// a file to the default path
type DirectoryUploader struct {
	Dir         string
	DefaultPath string
}
304

305 306 307 308 309 310
// Upload performs the upload of the directory and default path
func (d *DirectoryUploader) Upload(upload UploadFn) error {
	if d.DefaultPath != "" {
		file, err := Open(d.DefaultPath)
		if err != nil {
			return err
311
		}
312 313
		if err := upload(file); err != nil {
			return err
314 315
		}
	}
316 317 318 319 320
	return filepath.Walk(d.Dir, func(path string, f os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if f.IsDir() {
321 322
			return nil
		}
323 324 325 326 327 328 329 330 331 332 333 334
		file, err := Open(path)
		if err != nil {
			return err
		}
		relPath, err := filepath.Rel(d.Dir, path)
		if err != nil {
			return err
		}
		file.Path = filepath.ToSlash(relPath)
		return upload(file)
	})
}
335

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
// FileUploader uploads a single file
type FileUploader struct {
	File *File
}

// Upload performs the upload of the file
func (f *FileUploader) Upload(upload UploadFn) error {
	return upload(f.File)
}

// UploadFn is the type of function passed to an Uploader to perform the upload
// of a single file (for example, a directory uploader would call a provided
// UploadFn for each file in the directory tree)
type UploadFn func(file *File) error

// TarUpload uses the given Uploader to upload files to swarm as a tar stream,
// returning the resulting manifest hash
func (c *Client) TarUpload(hash string, uploader Uploader) (string, error) {
	reqR, reqW := io.Pipe()
	defer reqR.Close()
	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/x-tar")

	// use 'Expect: 100-continue' so we don't send the request body if
	// the server refuses the request
	req.Header.Set("Expect", "100-continue")

	tw := tar.NewWriter(reqW)

	// define an UploadFn which adds files to the tar stream
	uploadFn := func(file *File) error {
		hdr := &tar.Header{
			Name:    file.Path,
			Mode:    file.Mode,
			Size:    file.Size,
			ModTime: file.ModTime,
			Xattrs: map[string]string{
				"user.swarm.content-type": file.ContentType,
			},
		}
		if err := tw.WriteHeader(hdr); err != nil {
			return err
381
		}
382 383
		_, err = io.Copy(tw, file)
		return err
384 385
	}

386 387 388 389 390 391
	// run the upload in a goroutine so we can send the request headers and
	// wait for a '100 Continue' response before sending the tar stream
	go func() {
		err := uploader.Upload(uploadFn)
		if err == nil {
			err = tw.Close()
392
		}
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
		reqW.CloseWithError(err)
	}()

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
	}
	data, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// MultipartUpload uses the given Uploader to upload files to swarm as a
// multipart form, returning the resulting manifest hash
func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) {
	reqR, reqW := io.Pipe()
	defer reqR.Close()
	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
	if err != nil {
		return "", err
419 420
	}

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
	// use 'Expect: 100-continue' so we don't send the request body if
	// the server refuses the request
	req.Header.Set("Expect", "100-continue")

	mw := multipart.NewWriter(reqW)
	req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))

	// define an UploadFn which adds files to the multipart form
	uploadFn := func(file *File) error {
		hdr := make(textproto.MIMEHeader)
		hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", file.Path))
		hdr.Set("Content-Type", file.ContentType)
		hdr.Set("Content-Length", strconv.FormatInt(file.Size, 10))
		w, err := mw.CreatePart(hdr)
		if err != nil {
			return err
		}
		_, err = io.Copy(w, file)
		return err
	}

	// run the upload in a goroutine so we can send the request headers and
	// wait for a '100 Continue' response before sending the multipart form
	go func() {
		err := uploader.Upload(uploadFn)
		if err == nil {
			err = mw.Close()
		}
		reqW.CloseWithError(err)
	}()

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
	}
	data, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	return string(data), nil
465
}