github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/renter/download.go (about)

     1  package renter
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/NebulousLabs/Sia/crypto"
    13  	"github.com/NebulousLabs/Sia/modules"
    14  	"github.com/NebulousLabs/Sia/modules/renter/contractor"
    15  )
    16  
    17  var (
    18  	errInsufficientHosts  = errors.New("insufficient hosts to recover file")
    19  	errInsufficientPieces = errors.New("couldn't fetch enough pieces to recover data")
    20  )
    21  
    22  // A fetcher fetches pieces from a host. This interface exists to facilitate
    23  // easy testing.
    24  type fetcher interface {
    25  	// pieces returns the set of pieces corresponding to a given chunk.
    26  	pieces(chunk uint64) []pieceData
    27  
    28  	// fetch returns the data specified by piece metadata.
    29  	fetch(pieceData) ([]byte, error)
    30  }
    31  
    32  // A hostFetcher fetches pieces from a host. It implements the fetcher
    33  // interface.
    34  type hostFetcher struct {
    35  	downloader contractor.Downloader
    36  	pieceMap   map[uint64][]pieceData
    37  	masterKey  crypto.TwofishKey
    38  }
    39  
    40  // pieces returns the pieces stored on this host that are part of a given
    41  // chunk.
    42  func (hf *hostFetcher) pieces(chunk uint64) []pieceData {
    43  	return hf.pieceMap[chunk]
    44  }
    45  
    46  // fetch downloads the piece specified by p.
    47  func (hf *hostFetcher) fetch(p pieceData) ([]byte, error) {
    48  	// request piece
    49  	data, err := hf.downloader.Sector(p.MerkleRoot)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	// generate decryption key
    55  	key := deriveKey(hf.masterKey, p.Chunk, p.Piece)
    56  
    57  	// decrypt and return
    58  	return key.DecryptBytes(data)
    59  }
    60  
    61  // newHostFetcher creates a new hostFetcher.
    62  func newHostFetcher(d contractor.Downloader, fc fileContract, masterKey crypto.TwofishKey) *hostFetcher {
    63  	// make piece map
    64  	pieceMap := make(map[uint64][]pieceData)
    65  	for _, p := range fc.Pieces {
    66  		pieceMap[p.Chunk] = append(pieceMap[p.Chunk], p)
    67  	}
    68  	return &hostFetcher{
    69  		downloader: d,
    70  		pieceMap:   pieceMap,
    71  		masterKey:  masterKey,
    72  	}
    73  }
    74  
    75  // checkHosts checks that a set of hosts is sufficient to download a file.
    76  func checkHosts(hosts []fetcher, minPieces int, numChunks uint64) error {
    77  	for i := uint64(0); i < numChunks; i++ {
    78  		pieces := 0
    79  		for _, h := range hosts {
    80  			pieces += len(h.pieces(i))
    81  		}
    82  		if pieces < minPieces {
    83  			return errInsufficientHosts
    84  		}
    85  	}
    86  	return nil
    87  }
    88  
    89  // A download is a file download that has been queued by the renter.
    90  type download struct {
    91  	// NOTE: received is the first field to ensure 64-bit alignment, which is
    92  	// required for atomic operations.
    93  	received uint64
    94  
    95  	startTime   time.Time
    96  	siapath     string
    97  	destination string
    98  
    99  	erasureCode modules.ErasureCoder
   100  	chunkSize   uint64
   101  	fileSize    uint64
   102  	hosts       []fetcher
   103  }
   104  
   105  // getPiece locates and downloads a specific piece.
   106  func (d *download) getPiece(chunkIndex, pieceIndex uint64) []byte {
   107  	for _, h := range d.hosts {
   108  		for _, p := range h.pieces(chunkIndex) {
   109  			if p.Piece == pieceIndex {
   110  				data, err := h.fetch(p)
   111  				if err != nil {
   112  					break // try next host
   113  				}
   114  				return data
   115  			}
   116  		}
   117  	}
   118  	return nil
   119  }
   120  
   121  // run performs the actual download. It spawns one worker per host, and
   122  // instructs them to sequentially download chunks. It then writes the
   123  // recovered chunks to w.
   124  func (d *download) run(w io.Writer) error {
   125  	var received uint64
   126  	for i := uint64(0); received < d.fileSize; i++ {
   127  		// load pieces into chunk
   128  		chunk := make([][]byte, d.erasureCode.NumPieces())
   129  		left := d.erasureCode.MinPieces()
   130  		// pick hosts at random
   131  		chunkOrder, err := crypto.Perm(len(chunk))
   132  		if err != nil {
   133  			return err
   134  		}
   135  		for _, j := range chunkOrder {
   136  			chunk[j] = d.getPiece(i, uint64(j))
   137  			if chunk[j] != nil {
   138  				left--
   139  			} else {
   140  			}
   141  			if left == 0 {
   142  				break
   143  			}
   144  		}
   145  		if left != 0 {
   146  			return errInsufficientPieces
   147  		}
   148  
   149  		// Write pieces to w. We always write chunkSize bytes unless this is
   150  		// the last chunk; in that case, we write the remainder.
   151  		n := d.chunkSize
   152  		if n > d.fileSize-received {
   153  			n = d.fileSize - received
   154  		}
   155  		err = d.erasureCode.Recover(chunk, uint64(n), w)
   156  		if err != nil {
   157  			return err
   158  		}
   159  		received += n
   160  		atomic.AddUint64(&d.received, n)
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  // newDownload initializes and returns a download object.
   167  func (f *file) newDownload(hosts []fetcher, destination string) *download {
   168  	return &download{
   169  		erasureCode: f.erasureCode,
   170  		chunkSize:   f.chunkSize(),
   171  		fileSize:    f.size,
   172  		hosts:       hosts,
   173  
   174  		startTime:   time.Now(),
   175  		received:    0,
   176  		siapath:     f.name,
   177  		destination: destination,
   178  	}
   179  }
   180  
   181  // Download downloads a file, identified by its path, to the destination
   182  // specified.
   183  func (r *Renter) Download(path, destination string) error {
   184  	// Lookup the file associated with the nickname.
   185  	lockID := r.mu.Lock()
   186  	file, exists := r.files[path]
   187  	r.mu.Unlock(lockID)
   188  	if !exists {
   189  		return errors.New("no file with that path")
   190  	}
   191  
   192  	if !r.wallet.Unlocked() {
   193  		return errors.New("wallet must be unlocked before downloading")
   194  	}
   195  
   196  	// Copy the file's metadata
   197  	// TODO: this is ugly because we only have the Contracts method for
   198  	// looking up contracts.
   199  	contracts := make(map[*fileContract]modules.RenterContract)
   200  	file.mu.RLock()
   201  	for _, c := range r.hostContractor.Contracts() {
   202  		fc, ok := file.contracts[c.ID]
   203  		if ok {
   204  			contracts[&fc] = c
   205  		}
   206  	}
   207  	file.mu.RUnlock()
   208  	r.log.Debugf("Starting Download, found %v contracts\n", len(contracts))
   209  
   210  	if len(contracts) == 0 {
   211  		return errors.New("no record of that file's contracts")
   212  	}
   213  
   214  	// Initiate connections to each host.
   215  	var hosts []fetcher
   216  	var errs []string
   217  	for fc, c := range contracts {
   218  		// TODO: connect in parallel
   219  		d, err := r.hostContractor.Downloader(c)
   220  		if err != nil {
   221  			errs = append(errs, fmt.Sprintf("\t%v: %v", c.NetAddress, err))
   222  			continue
   223  		}
   224  		defer d.Close()
   225  		hosts = append(hosts, newHostFetcher(d, *fc, file.masterKey))
   226  	}
   227  	if len(hosts) < file.erasureCode.MinPieces() {
   228  		return errors.New("Could not connect to enough hosts:\n" + strings.Join(errs, "\n"))
   229  	}
   230  
   231  	// Check that this host set is sufficient to download the file.
   232  	err := checkHosts(hosts, file.erasureCode.MinPieces(), file.numChunks())
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	// Create file on disk with the correct permissions.
   238  	perm := os.FileMode(file.mode)
   239  	if perm == 0 {
   240  		// sane default
   241  		perm = 0666
   242  	}
   243  	f, err := os.OpenFile(destination, os.O_CREATE|os.O_RDWR|os.O_TRUNC, perm)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	defer f.Close()
   248  
   249  	// Create the download object.
   250  	d := file.newDownload(hosts, destination)
   251  
   252  	// Add the download to the download queue.
   253  	lockID = r.mu.Lock()
   254  	r.downloadQueue = append(r.downloadQueue, d)
   255  	r.mu.Unlock(lockID)
   256  
   257  	// Perform download.
   258  	err = d.run(f)
   259  	if err != nil {
   260  		// File could not be downloaded; delete the copy on disk.
   261  		os.Remove(destination)
   262  		return err
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  // DownloadQueue returns the list of downloads in the queue.
   269  func (r *Renter) DownloadQueue() []modules.DownloadInfo {
   270  	lockID := r.mu.RLock()
   271  	defer r.mu.RUnlock(lockID)
   272  
   273  	// order from most recent to least recent
   274  	downloads := make([]modules.DownloadInfo, len(r.downloadQueue))
   275  	for i := range r.downloadQueue {
   276  		d := r.downloadQueue[len(r.downloadQueue)-i-1]
   277  		downloads[i] = modules.DownloadInfo{
   278  			SiaPath:     d.siapath,
   279  			Destination: d.destination,
   280  			Filesize:    d.fileSize,
   281  			Received:    atomic.LoadUint64(&d.received),
   282  			StartTime:   d.startTime,
   283  		}
   284  	}
   285  	return downloads
   286  }