Skip to content

Commit 2c47e33

Browse files
committed
Added resumable download
1 parent b3836b1 commit 2c47e33

File tree

7 files changed

+149
-81
lines changed

7 files changed

+149
-81
lines changed

cmd/arduino_lib.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func executeDownloadCommand(cmd *cobra.Command, args []string) error {
159159
}
160160
}
161161

162-
libs, failed := purgeInvalidLibraries(parseLibArgs(args), status.(*libraries.StatusContext))
162+
libs, failed := purgeInvalidLibraries(parseLibArgs(args), status.(libraries.StatusContext))
163163
libraryResults := parallelLibDownloads(libs, true, "Downloaded")
164164

165165
for libFail, err := range failed {
@@ -226,7 +226,7 @@ func parallelLibDownloads(items map[*libraries.Library]string, forced bool, OkSt
226226

227227
for library, version := range items {
228228
release := library.GetVersion(version)
229-
if release != nil && !library.IsCached(version) || forced {
229+
if forced || release != nil && !library.IsCached(version) || release.CheckLocalArchive() != nil {
230230
var pBar *pb.ProgressBar
231231
if textMode {
232232
pBar = pb.StartNew(release.Size).SetUnits(pb.U_BYTES).Prefix(fmt.Sprintf("%-20s", library.Name))

common/checksums/checksums.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package checksums
2+
3+
import (
4+
"bytes"
5+
"crypto"
6+
"encoding/hex"
7+
"hash"
8+
"io"
9+
"os"
10+
"strings"
11+
12+
"github.com/bcmi-labs/arduino-cli/common"
13+
)
14+
15+
func getHashAlgoAndComponent(checksum string) (hash.Hash, []byte) {
16+
components := strings.SplitN(checksum, ":", 2)
17+
hashAlgo := components[0]
18+
hashMid, err := hex.DecodeString(components[1])
19+
if err != nil {
20+
return nil, nil
21+
}
22+
23+
hash := []byte(hashMid)
24+
switch hashAlgo {
25+
case "SHA-256":
26+
return crypto.SHA256.New(), hash
27+
case "SHA1":
28+
return crypto.SHA1.New(), hash
29+
case "MD5":
30+
return crypto.MD5.New(), hash
31+
default:
32+
return nil, nil
33+
}
34+
}
35+
36+
// Match checks the checksum of a Release archive, in compliance with
37+
// What Checksum is expected.
38+
func Match(r common.Release) bool {
39+
hash, content := getHashAlgoAndComponent(r.ExpectedChecksum())
40+
filePath, err := r.ArchivePath()
41+
if err != nil {
42+
return false
43+
}
44+
45+
file, err := os.Open(filePath)
46+
if err != nil {
47+
return false
48+
}
49+
defer file.Close()
50+
io.Copy(hash, file)
51+
return bytes.Compare(hash.Sum(nil), content) == 0
52+
}

common/functions.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ package common
3131

3232
import (
3333
"archive/zip"
34-
"bytes"
34+
"errors"
3535
"fmt"
36+
"io"
3637
"io/ioutil"
3738
"net/http"
3839
"os"
@@ -102,7 +103,7 @@ func GetFolder(folder string, messageName string) (string, error) {
102103
}
103104

104105
// Unzip extracts a zip file to a specified destination path.
105-
func Unzip(archive *zip.Reader, destination string) error {
106+
func Unzip(archive *zip.ReadCloser, destination string) error {
106107
for _, file := range archive.File {
107108
path := filepath.Join(destination, file.Name)
108109
if file.FileInfo().IsDir() {
@@ -155,32 +156,44 @@ func TruncateDir(dir string) error {
155156
}
156157

157158
// DownloadPackage downloads a package from arduino repository, applying a label for the progress bar.
158-
func DownloadPackage(URL string, downloadLabel string, progressBar *pb.ProgressBar, initialData []byte) ([]byte, error) {
159+
func DownloadPackage(URL string, downloadLabel string, progressBar *pb.ProgressBar, initialData *os.File, totalSize int) error {
159160
client := http.DefaultClient
160161

162+
if initialData == nil {
163+
return errors.New("Cannot fill a nil file pointer")
164+
}
165+
161166
request, err := http.NewRequest("GET", URL, nil)
162167
if err != nil {
163-
return nil, fmt.Errorf("Cannot create HTTP request: %s", err)
168+
return fmt.Errorf("Cannot create HTTP request: %s", err)
164169
}
165170

166171
var initialSize int
167-
if initialData == nil {
172+
stats, err := initialData.Stat()
173+
if err != nil {
168174
initialSize = 0
169175
} else {
170-
initialSize = len(initialData)
176+
fileSize := int(stats.Size())
177+
if fileSize >= totalSize {
178+
initialSize = 0
179+
} else {
180+
initialSize = fileSize
181+
}
171182
}
172183

173184
if initialSize > 0 {
174185
request.Header.Add("Range", fmt.Sprintf("bytes=%d-", initialSize))
175186
}
176-
//TODO : how to add progressbar with resume download?
187+
177188
response, err := client.Do(request)
178189

179190
if err != nil {
180-
return nil, fmt.Errorf("Cannot fetch %s. Response creation error", downloadLabel)
181-
} else if response.StatusCode != 200 {
191+
return fmt.Errorf("Cannot fetch %s. Response creation error", downloadLabel)
192+
} else if response.StatusCode != 200 &&
193+
response.StatusCode != 206 &&
194+
response.StatusCode != 416 {
182195
response.Body.Close()
183-
return nil, fmt.Errorf("Cannot fetch %s. Source responded with a status %d code", downloadLabel, response.StatusCode)
196+
return fmt.Errorf("Cannot fetch %s. Source responded with a status %d code", downloadLabel, response.StatusCode)
184197
}
185198
defer response.Body.Close()
186199

@@ -190,16 +203,9 @@ func DownloadPackage(URL string, downloadLabel string, progressBar *pb.ProgressB
190203
source = progressBar.NewProxyReader(response.Body)
191204
}
192205

193-
body, err := ioutil.ReadAll(source)
206+
_, err = io.Copy(initialData, source)
194207
if err != nil {
195-
return nil, fmt.Errorf("Cannot read response body")
196-
}
197-
var total []byte
198-
if initialData != nil {
199-
total = bytes.Join([][]byte{initialData, body}, nil)
200-
} else {
201-
total = body
208+
return fmt.Errorf("Cannot read response body %s", err)
202209
}
203-
204-
return total, nil
210+
return nil
205211
}

common/interfaces.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ type StatusContext interface {
3939
Names() []string // Names Returns an array with all the names of the items.
4040
Items() map[string]interface{} // Items Returns a map of all items with their names.
4141
}
42+
43+
// Release represents a generic release.
44+
type Release interface {
45+
ArchivePath() (string, error) // ArchivePath returns the fullPath of the Archive of this release.
46+
ExpectedChecksum() string // Checksum returns the expected checksum for this release.
47+
}

libraries/download.go

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,15 @@ const (
5151
func DownloadAndCache(library *Library, progBar *pb.ProgressBar, version string) task.Wrapper {
5252
return task.Wrapper{
5353
Task: func() task.Result {
54-
zipContent, err := downloadRelease(library, progBar, version)
54+
err := downloadRelease(library, progBar, version)
5555
if err != nil {
5656
return task.Result{
5757
Result: nil,
5858
Error: err,
5959
}
6060
}
61-
62-
zipArchive, err := prepareInstall(library, zipContent, version)
63-
if err != nil {
64-
return task.Result{
65-
Result: nil,
66-
Error: err,
67-
}
68-
}
69-
7061
return task.Result{
71-
Result: zipArchive,
62+
Result: nil,
7263
Error: nil,
7364
}
7465
},
@@ -77,16 +68,20 @@ func DownloadAndCache(library *Library, progBar *pb.ProgressBar, version string)
7768

7869
// DownloadLatest downloads Latest version of a library.
7970
func downloadLatest(library *Library, progBar *pb.ProgressBar) ([]byte, error) {
80-
return downloadRelease(library, progBar, library.latestVersion())
71+
return nil, downloadRelease(library, progBar, library.latestVersion())
8172
}
8273

83-
func downloadRelease(library *Library, progBar *pb.ProgressBar, version string) ([]byte, error) {
74+
func downloadRelease(library *Library, progBar *pb.ProgressBar, version string) error {
8475
release := library.GetVersion(version)
8576
if release == nil {
86-
return nil, errors.New("Invalid version number")
77+
return errors.New("Invalid version number")
78+
}
79+
initialData, err := release.OpenLocalArchiveForDownload()
80+
if err != nil {
81+
return fmt.Errorf("Cannot get Archive file of this release : %s", err)
8782
}
88-
initialData := release.ReadLocalArchive()
89-
return common.DownloadPackage(release.URL, fmt.Sprintf("library %s", library.Name), progBar, initialData)
83+
defer initialData.Close()
84+
return common.DownloadPackage(release.URL, fmt.Sprintf("library %s", library.Name), progBar, initialData, release.Size)
9085
}
9186

9287
// DownloadLibrariesFile downloads the lib file from arduino repository.

libraries/install_uninstall.go

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@ package libraries
3131

3232
import (
3333
"archive/zip"
34-
"bytes"
3534
"errors"
3635
"fmt"
37-
"io/ioutil"
3836
"os"
3937
"path/filepath"
4038

@@ -95,15 +93,12 @@ func InstallLib(library *Library, version string) error {
9593
}
9694

9795
cacheFilePath := filepath.Join(stagingFolder, release.ArchiveFileName)
98-
content, err := ioutil.ReadFile(cacheFilePath)
99-
if err != nil {
100-
return err
101-
}
10296

103-
zipArchive, err := zip.NewReader(bytes.NewReader(content), int64(len(content)))
97+
zipArchive, err := zip.OpenReader(cacheFilePath)
10498
if err != nil {
10599
return err
106100
}
101+
defer zipArchive.Close()
107102

108103
err = common.Unzip(zipArchive, libFolder)
109104
if err != nil {
@@ -124,29 +119,3 @@ func removeRelease(l *Library, r *Release) error {
124119
path := filepath.Join(libFolder, fmt.Sprintf("%s-%s", name, r.Version))
125120
return os.RemoveAll(path)
126121
}
127-
128-
// prepareInstall move a downloaded library to a cache folder, before installation.
129-
func prepareInstall(library *Library, body []byte, version string) (*zip.Reader, error) {
130-
reader := bytes.NewReader(body)
131-
release := library.GetVersion(version)
132-
if release == nil {
133-
return nil, errors.New("Invalid version number")
134-
}
135-
136-
archive, err := zip.NewReader(reader, int64(reader.Len()))
137-
if err != nil {
138-
return nil, fmt.Errorf("Cannot read downloaded archive")
139-
}
140-
141-
// if I can read the archive I save it to staging folder.
142-
stagingFolder, err := getDownloadCacheFolder()
143-
if err != nil {
144-
return nil, fmt.Errorf("Cannot get download cache folder")
145-
}
146-
147-
err = ioutil.WriteFile(filepath.Join(stagingFolder, release.ArchiveFileName), body, 0666)
148-
if err != nil {
149-
return nil, fmt.Errorf("Cannot write download to cache folder, %s", err.Error())
150-
}
151-
return archive, nil
152-
}

libraries/libraries.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ package libraries
3131

3232
import (
3333
"bufio"
34+
"errors"
3435
"strconv"
3536

3637
"strings"
@@ -44,6 +45,7 @@ import (
4445
"fmt"
4546

4647
"github.com/bcmi-labs/arduino-cli/common"
48+
"github.com/bcmi-labs/arduino-cli/common/checksums"
4749
"github.com/blang/semver"
4850
)
4951

@@ -128,21 +130,59 @@ type Release struct {
128130
Checksum string `json:"checksum"`
129131
}
130132

131-
// ReadLocalArchive reads the data from the local archive if present,
133+
// OpenLocalArchiveForDownload reads the data from the local archive if present,
132134
// and returns the []byte of the file content. Used by resume Download.
133-
func (r Release) ReadLocalArchive() []byte {
135+
// Creates an empty file if not found.
136+
func (r Release) OpenLocalArchiveForDownload() (*os.File, error) {
137+
path, err := r.ArchivePath()
138+
if err != nil {
139+
return nil, err
140+
}
141+
stats, err := os.Stat(path)
142+
if os.IsNotExist(err) || err == nil && int(stats.Size()) >= r.Size {
143+
return os.Create(path)
144+
}
145+
return os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
146+
}
147+
148+
// ArchivePath returns the fullPath of the Archive of this release.
149+
func (r Release) ArchivePath() (string, error) {
134150
staging, err := getDownloadCacheFolder()
135151
if err != nil {
136-
return nil
152+
return "", err
153+
}
154+
return filepath.Join(staging, r.ArchiveFileName), nil
155+
}
156+
157+
// CheckLocalArchive check for integrity of the local archive.
158+
func (r Release) CheckLocalArchive() error {
159+
archivePath, err := r.ArchivePath()
160+
if err != nil {
161+
return err
137162
}
138-
path := filepath.Join(staging, r.ArchiveFileName)
139-
content, err := ioutil.ReadFile(path)
140-
// if the size is the same the archive has been downloaded and if we force redownload
141-
// it won't work.
142-
if err != nil || len(content) == r.Size {
143-
return nil
163+
stats, err := os.Stat(archivePath)
164+
if os.IsNotExist(err) {
165+
return errors.New("Archive does not exist")
144166
}
145-
return content
167+
if err != nil {
168+
return err
169+
}
170+
if int(stats.Size()) > r.Size {
171+
return errors.New("Archive size does not match with specification of this release, assuming corruption")
172+
}
173+
if !r.checksumMatches() {
174+
return errors.New("Checksum does not match, assuming corruption")
175+
}
176+
return nil
177+
}
178+
179+
func (r Release) checksumMatches() bool {
180+
return checksums.Match(r)
181+
}
182+
183+
// ExpectedChecksum returns the expected checksum for this release.
184+
func (r Release) ExpectedChecksum() string {
185+
return r.Checksum
146186
}
147187

148188
/*

0 commit comments

Comments
 (0)