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 }