diff --git a/Gopkg.lock b/Gopkg.lock index ed1b2a79315..9371ff8e205 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -130,12 +130,12 @@ version = "v1.2.2" [[projects]] - digest = "1:832de9226bb481653db83c615e2c5d1c9ef873be810c021bc54bd090e71bf5d6" + digest = "1:db8e2f3c8cc717afe53c6941776312bd7046d45d9fb757c5f3f32fd3cc46562d" name = "github.com/codeclysm/extract" packages = ["."] pruneopts = "UT" - revision = "d489ebb956c95f16e9435028ee81b332628a3aa6" - version = "v1.1.1" + revision = "de8493db4b3e06921c3ee97fa0b200435a8a796d" + version = "v2.0.0" [[projects]] digest = "1:7cb4fdca4c251b3ef8027c90ea35f70c7b661a593b9eeae34753c65499098bb1" @@ -387,6 +387,14 @@ revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" +[[projects]] + digest = "1:76e2b7e208091c639435b893c9400e802d9a45efed22066cdb3312e4eb9d2025" + name = "go.bug.st/cleanup" + packages = ["."] + pruneopts = "UT" + revision = "a55db27f18335e11f995bfc0fb4a7ab38ce5669d" + version = "v1.0.0" + [[projects]] branch = "master" digest = "1:5cd3c0f530ab660c7b08f563020dc00b8e761c2f7650457a3f669b6601f7f28d" @@ -545,6 +553,7 @@ "github.com/spf13/cobra/doc", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", + "go.bug.st/cleanup", "go.bug.st/downloader", "go.bug.st/relaxed-semver", "go.bug.st/serial.v1", diff --git a/Gopkg.toml b/Gopkg.toml index 4b49b12e8f2..b37f3464760 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -59,10 +59,6 @@ name = "github.com/codeclysm/cc" version = "1.2.1" -[[constraint]] - name = "github.com/codeclysm/extract" - version = "1.1.1" - [[constraint]] branch = "master" name = "github.com/mitchellh/go-homedir" diff --git a/arduino/cores/cores.go b/arduino/cores/cores.go index 28a076b7445..c7037b10681 100644 --- a/arduino/cores/cores.go +++ b/arduino/cores/cores.go @@ -214,6 +214,11 @@ func (release *PlatformRelease) GetLibrariesDir() *paths.Path { return nil } +// IsInstalled returns true if the PlatformRelease is installed +func (release *PlatformRelease) IsInstalled() bool { + return release.InstallDir != nil +} + func (release *PlatformRelease) String() string { version := "" if release.Version != nil { diff --git a/arduino/resources/install.go b/arduino/resources/install.go index 7c421ce5d87..376562b849b 100644 --- a/arduino/resources/install.go +++ b/arduino/resources/install.go @@ -18,11 +18,13 @@ package resources import ( + "context" "fmt" "os" paths "github.com/arduino/go-paths-helper" "github.com/codeclysm/extract" + "go.bug.st/cleanup" ) // Install installs the resource in three steps: @@ -54,7 +56,9 @@ func (release *DownloadResource) Install(downloadDir, tempPath, destDir *paths.P defer file.Close() // Extract into temp directory - if err := extract.Archive(file, tempDir.String(), nil); err != nil { + ctx, cancel := cleanup.InterruptableContext(context.Background()) + defer cancel() + if err := extract.Archive(ctx, file, tempDir.String(), nil); err != nil { return fmt.Errorf("extracting archive: %s", err) } diff --git a/commands/commands.go b/commands/commands.go index 1fed0b5bebe..e96cb10059e 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -90,7 +90,7 @@ func InitPackageManager() *packagemanager.PackageManager { Config.DownloadsDir(), Config.DataDir.Join("tmp")) - for _, URL := range configs.BoardManagerAdditionalUrls { + for _, URL := range Config.BoardManagerAdditionalUrls { if err := pm.LoadPackageIndex(URL); err != nil { formatter.PrintError(err, "Failed to load "+URL.String()+" package index.\n"+ "Try updating all indexes with `"+AppName+" core update-index`.") diff --git a/commands/commands_test.go b/commands/commands_test.go index 288e52df807..b2934c20e59 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -426,6 +426,42 @@ func TestCompileCommands(t *testing.T) { require.True(t, paths.New("anothertest", "test2.hex").Exist()) } +func TestInvalidCoreURL(t *testing.T) { + defer makeTempDataDir(t)() + defer makeTempSketchbookDir(t)() + + tmp, err := paths.MkTempDir("", "") + require.NoError(t, err, "making temporary dir") + defer tmp.RemoveAll() + + configFile := tmp.Join("cli-config.yml") + configFile.WriteFile([]byte(` +board_manager: + additional_urls: + - http://www.example.com/package_example_index.json +`)) + + require.NoError(t, currDataDir.MkdirAll()) + err = currDataDir.Join("package_index.json").WriteFile([]byte(`{ "packages": [] }`)) + require.NoError(t, err, "Writing empty json index file") + err = currDataDir.Join("package_example_index.json").WriteFile([]byte(`{ "packages": [] }`)) + require.NoError(t, err, "Writing empty json index file") + + // Empty cores list + exitCode, d := executeWithArgs(t, "--config-file", configFile.String(), "core", "list") + require.Zero(t, exitCode, "exit code") + require.Empty(t, strings.TrimSpace(string(d))) + + // Empty cores list + exitCode, _ = executeWithArgs(t, "--config-file", configFile.String(), "core", "update-index") + require.NotZero(t, exitCode, "exit code") + + // Empty cores list + exitCode, d = executeWithArgs(t, "--config-file", configFile.String(), "core", "list") + require.Zero(t, exitCode, "exit code") + require.Empty(t, strings.TrimSpace(string(d))) +} + func TestCoreCommands(t *testing.T) { defer makeTempDataDir(t)() defer makeTempSketchbookDir(t)() diff --git a/commands/core/download.go b/commands/core/download.go index 64f7bc44f52..a40a714558a 100644 --- a/commands/core/download.go +++ b/commands/core/download.go @@ -60,31 +60,30 @@ func downloadPlatformByRef(pm *packagemanager.PackageManager, platformsRef *pack formatter.PrintError(err, "Could not determine platform dependencies") os.Exit(commands.ErrBadCall) } - - // Check if all tools have a flavor available for the current OS + downloadPlatform(pm, platform) for _, tool := range tools { - if tool.GetCompatibleFlavour() == nil { - formatter.PrintErrorMessage("The tool " + tool.String() + " is not available for the current OS") - os.Exit(commands.ErrGeneric) - } + downloadTool(pm, tool) } +} - // Download tools - for _, tool := range tools { - DownloadToolRelease(pm, tool) - } +func downloadPlatform(pm *packagemanager.PackageManager, platformRelease *cores.PlatformRelease) { + // Download platform + resp, err := pm.DownloadPlatformRelease(platformRelease) + download(resp, err, platformRelease.String()) +} - // Download cores - formatter.Print("Downloading " + platform.String() + "...") - resp, err := pm.DownloadPlatformRelease(platform) - download(resp, err, platform.String()) +func downloadTool(pm *packagemanager.PackageManager, tool *cores.ToolRelease) { + // Check if tool has a flavor available for the current OS + if tool.GetCompatibleFlavour() == nil { + formatter.PrintErrorMessage("The tool " + tool.String() + " is not available for the current OS") + os.Exit(commands.ErrGeneric) + } - logrus.Info("Done") + DownloadToolRelease(pm, tool) } // DownloadToolRelease downloads a ToolRelease func DownloadToolRelease(pm *packagemanager.PackageManager, toolRelease *cores.ToolRelease) { - formatter.Print("Downloading " + toolRelease.String() + "...") resp, err := pm.DownloadToolRelease(toolRelease) download(resp, err, toolRelease.String()) } @@ -98,6 +97,7 @@ func download(d *downloader.Downloader, err error, label string) { formatter.Print(label + " already downloaded") return } + formatter.Print("Downloading " + label + "...") formatter.DownloadProgressBar(d, label) if d.Error() != nil { formatter.PrintError(d.Error(), "Error downloading "+label) diff --git a/commands/core/install.go b/commands/core/install.go index 52a2aea1375..1fc27b41149 100644 --- a/commands/core/install.go +++ b/commands/core/install.go @@ -50,7 +50,6 @@ func runInstallCommand(cmd *cobra.Command, args []string) { pm := commands.InitPackageManagerWithoutBundles() for _, platformRef := range platformsRefs { - downloadPlatformByRef(pm, platformRef) installPlatformByRef(pm, platformRef) } @@ -64,19 +63,39 @@ func installPlatformByRef(pm *packagemanager.PackageManager, platformRef *packag os.Exit(commands.ErrBadCall) } - // TODO: Check install prerequisites here + installPlatform(pm, platform, tools) +} - // TODO: Download here +func installPlatform(pm *packagemanager.PackageManager, platformRelease *cores.PlatformRelease, requiredTools []*cores.ToolRelease) { + log := pm.Log.WithField("platform", platformRelease) - for _, tool := range tools { - InstallToolRelease(pm, tool) + // Prerequisite checks before install + if platformRelease.IsInstalled() { + log.Warn("Platform already installed") + formatter.Print("Platform " + platformRelease.String() + " already installed") + return + } + toolsToInstall := []*cores.ToolRelease{} + for _, tool := range requiredTools { + if tool.IsInstalled() { + log.WithField("tool", tool).Warn("Tool already installed") + formatter.Print("Tool " + tool.String() + " already installed") + } else { + toolsToInstall = append(toolsToInstall, tool) + } } - installPlatformRelease(pm, platform) -} -func installPlatformRelease(pm *packagemanager.PackageManager, platformRelease *cores.PlatformRelease) { - log := pm.Log.WithField("platform", platformRelease) + // Package download + for _, tool := range toolsToInstall { + downloadTool(pm, tool) + } + downloadPlatform(pm, platformRelease) + + for _, tool := range toolsToInstall { + InstallToolRelease(pm, tool) + } + // Are we installing or upgrading? platform := platformRelease.Platform installed := pm.GetInstalledPlatformRelease(platform) if installed == nil { @@ -87,12 +106,8 @@ func installPlatformRelease(pm *packagemanager.PackageManager, platformRelease * formatter.Print("Updating " + installed.String() + " with " + platformRelease.String() + "...") } + // Install err := pm.InstallPlatform(platformRelease) - if os.IsExist(err) { - log.Warn("Platform already installed") - formatter.Print("Platform " + platformRelease.String() + " already installed") - return - } if err != nil { log.WithError(err).Error("Cannot install platform") formatter.PrintError(err, "Cannot install platform") @@ -125,15 +140,15 @@ func installPlatformRelease(pm *packagemanager.PackageManager, platformRelease * func InstallToolRelease(pm *packagemanager.PackageManager, toolRelease *cores.ToolRelease) { log := pm.Log.WithField("Tool", toolRelease) - log.Info("Installing tool") - formatter.Print("Installing " + toolRelease.String()) - - err := pm.InstallTool(toolRelease) - if os.IsExist(err) { + if toolRelease.IsInstalled() { log.Warn("Tool already installed") formatter.Print("Tool " + toolRelease.String() + " already installed") return } + + log.Info("Installing tool") + formatter.Print("Installing " + toolRelease.String() + "...") + err := pm.InstallTool(toolRelease) if err != nil { log.WithError(err).Warn("Cannot install tool") formatter.PrintError(err, "Cannot install tool: "+toolRelease.String()) diff --git a/commands/core/uninstall.go b/commands/core/uninstall.go index 7ea4df4e158..f54919d1bd3 100644 --- a/commands/core/uninstall.go +++ b/commands/core/uninstall.go @@ -103,7 +103,7 @@ func uninstallToolRelease(pm *packagemanager.PackageManager, toolRelease *cores. log := pm.Log.WithField("Tool", toolRelease) log.Info("Uninstalling tool") - formatter.Print("Uninstalling " + toolRelease.String()) + formatter.Print("Uninstalling " + toolRelease.String() + "...") if err := pm.UninstallTool(toolRelease); err != nil { log.WithError(err).Error("Error uninstalling") diff --git a/commands/core/update_index.go b/commands/core/update_index.go index a27e92ecf08..245c6b07d26 100644 --- a/commands/core/update_index.go +++ b/commands/core/update_index.go @@ -25,9 +25,9 @@ import ( "go.bug.st/downloader" + "github.com/arduino/arduino-cli/arduino/cores/packageindex" "github.com/arduino/arduino-cli/commands" "github.com/arduino/arduino-cli/common/formatter" - "github.com/arduino/arduino-cli/configs" "github.com/arduino/go-paths-helper" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -50,7 +50,7 @@ func runUpdateIndexCommand(cmd *cobra.Command, args []string) { } func updateIndexes() { - for _, URL := range configs.BoardManagerAdditionalUrls { + for _, URL := range commands.Config.BoardManagerAdditionalUrls { updateIndex(URL) } } @@ -58,34 +58,43 @@ func updateIndexes() { // TODO: This should be in packagemanager...... func updateIndex(URL *url.URL) { logrus.WithField("url", URL).Print("Updating index") - indexDirPath := commands.Config.IndexesDir() - coreIndexPath := indexDirPath.Join(path.Base(URL.Path)) tmpFile, err := ioutil.TempFile("", "") if err != nil { formatter.PrintError(err, "Error creating temp file for download") os.Exit(commands.ErrGeneric) } - defer os.Remove(tmpFile.Name()) - tmpFile.Close() + if err := tmpFile.Close(); err != nil { + formatter.PrintError(err, "Error creating temp file for download") + os.Exit(commands.ErrGeneric) + } + tmp := paths.New(tmpFile.Name()) + defer tmp.Remove() - d, err := downloader.Download(tmpFile.Name(), URL.String()) + d, err := downloader.Download(tmp.String(), URL.String()) if err != nil { formatter.PrintError(err, "Error downloading index "+URL.String()) os.Exit(commands.ErrNetwork) } + indexDirPath := commands.Config.IndexesDir() + coreIndexPath := indexDirPath.Join(path.Base(URL.Path)) formatter.DownloadProgressBar(d, "Updating index: "+coreIndexPath.Base()) if d.Error() != nil { formatter.PrintError(d.Error(), "Error downloading index "+URL.String()) os.Exit(commands.ErrNetwork) } + if _, err := packageindex.LoadIndex(tmp); err != nil { + formatter.PrintError(err, "Invalid package index in "+URL.String()) + os.Exit(commands.ErrGeneric) + } + if err := indexDirPath.MkdirAll(); err != nil { formatter.PrintError(err, "Can't create data directory "+indexDirPath.String()) os.Exit(commands.ErrGeneric) } - if err := paths.New(tmpFile.Name()).CopyTo(coreIndexPath); err != nil { + if err := tmp.CopyTo(coreIndexPath); err != nil { formatter.PrintError(err, "Error saving downloaded index "+URL.String()) os.Exit(commands.ErrGeneric) } diff --git a/configs/boards_manager.go b/configs/boards_manager.go deleted file mode 100644 index 4253311f3b0..00000000000 --- a/configs/boards_manager.go +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of arduino-cli. - * - * Copyright 2018 ARDUINO SA (http://www.arduino.cc/) - * - * This software is released under the GNU General Public License version 3, - * which covers the main part of arduino-cli. - * The terms of this license can be found at: - * https://www.gnu.org/licenses/gpl-3.0.en.html - * - * You can be released from the requirements of the above licenses by purchasing - * a commercial license. Buying such a license is mandatory if you want to modify or - * otherwise use the software for commercial activities involving the Arduino - * software without disclosing the source code of your own applications. To purchase - * a commercial license, send an email to license@arduino.cc. - */ - -package configs - -import "net/url" - -var defaultPackageIndexURL, _ = url.Parse("https://downloads.arduino.cc/packages/package_index.json") - -// BoardManagerAdditionalUrls contains the additional URL for 3rd party packages -var BoardManagerAdditionalUrls = []*url.URL{defaultPackageIndexURL} diff --git a/configs/configuration.go b/configs/configuration.go index abb6b7a1ee8..7fa17acc6d7 100644 --- a/configs/configuration.go +++ b/configs/configuration.go @@ -20,6 +20,7 @@ package configs import ( "fmt" + "net/url" "github.com/arduino/go-paths-helper" ) @@ -42,8 +43,13 @@ type Configuration struct { // the field is true if the CLI is bundled with the Arduino IDE, false if the CLI is running // standalone or nil if the detection has not been performed. IDEBundledCheckResult *bool + + // BoardManagerAdditionalUrls contains the additional URL for 3rd party packages + BoardManagerAdditionalUrls []*url.URL } +var defaultPackageIndexURL, _ = url.Parse("https://downloads.arduino.cc/packages/package_index.json") + // NewConfiguration returns a new Configuration with the default values func NewConfiguration() (*Configuration, error) { dataDir, err := getDefaultArduinoDataDir() @@ -56,9 +62,10 @@ func NewConfiguration() (*Configuration, error) { } return &Configuration{ - ConfigFile: getDefaultConfigFilePath(), - DataDir: dataDir, - SketchbookDir: sketchbookDir, + ConfigFile: getDefaultConfigFilePath(), + DataDir: dataDir, + SketchbookDir: sketchbookDir, + BoardManagerAdditionalUrls: []*url.URL{defaultPackageIndexURL}, }, nil } diff --git a/configs/preferences_txt_serializer.go b/configs/preferences_txt_serializer.go index cee9e71f4e9..b132b34eb8a 100644 --- a/configs/preferences_txt_serializer.go +++ b/configs/preferences_txt_serializer.go @@ -87,7 +87,7 @@ func (config *Configuration) LoadFromDesktopIDEPreferences() error { if URLs, has := props.GetOk("boardsmanager.additional.urls"); has { for _, URL := range strings.Split(URLs, ",") { if newURL, err := url.Parse(URL); err == nil { - BoardManagerAdditionalUrls = append(BoardManagerAdditionalUrls, newURL) + config.BoardManagerAdditionalUrls = append(config.BoardManagerAdditionalUrls, newURL) } } } diff --git a/configs/yaml_serializer.go b/configs/yaml_serializer.go index 173484ab344..36bd03caace 100644 --- a/configs/yaml_serializer.go +++ b/configs/yaml_serializer.go @@ -81,7 +81,7 @@ func (config *Configuration) LoadFromYAML(path *paths.Path) error { logrus.WithError(err).Warn("Error parsing config") continue } - BoardManagerAdditionalUrls = append(BoardManagerAdditionalUrls, url) + config.BoardManagerAdditionalUrls = append(config.BoardManagerAdditionalUrls, url) } } return nil @@ -104,9 +104,9 @@ func (config *Configuration) SerializeToYAML() ([]byte, error) { Password: ProxyPassword, } } - if len(BoardManagerAdditionalUrls) > 1 { + if len(config.BoardManagerAdditionalUrls) > 1 { c.BoardsManager = &yamlBoardsManagerConfig{AdditionalURLS: []string{}} - for _, URL := range BoardManagerAdditionalUrls[1:] { + for _, URL := range config.BoardManagerAdditionalUrls[1:] { c.BoardsManager.AdditionalURLS = append(c.BoardsManager.AdditionalURLS, URL.String()) } } diff --git a/vendor/github.com/codeclysm/extract/cancelable_reader.go b/vendor/github.com/codeclysm/extract/cancelable_reader.go new file mode 100644 index 00000000000..368a3b9973f --- /dev/null +++ b/vendor/github.com/codeclysm/extract/cancelable_reader.go @@ -0,0 +1,32 @@ +package extract + +import ( + "context" + "errors" + "io" +) + +func copyCancel(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) { + return io.Copy(dst, newCancelableReader(ctx, src)) +} + +type cancelableReader struct { + ctx context.Context + src io.Reader +} + +func (r *cancelableReader) Read(p []byte) (int, error) { + select { + case <-r.ctx.Done(): + return 0, errors.New("interrupted") + default: + return r.src.Read(p) + } +} + +func newCancelableReader(ctx context.Context, src io.Reader) *cancelableReader { + return &cancelableReader{ + ctx: ctx, + src: src, + } +} diff --git a/vendor/github.com/codeclysm/extract/extract.go b/vendor/github.com/codeclysm/extract/extract.go index e1211391770..617b7178b97 100644 --- a/vendor/github.com/codeclysm/extract/extract.go +++ b/vendor/github.com/codeclysm/extract/extract.go @@ -32,6 +32,7 @@ import ( "bytes" "compress/bzip2" "compress/gzip" + "context" "io" "io/ioutil" "os" @@ -52,7 +53,7 @@ type Renamer func(string) string // It automatically detects the archive type and accepts a rename function to // handle the names of the files. // If the file is not an archive, an error is returned. -func Archive(body io.Reader, location string, rename Renamer) error { +func Archive(ctx context.Context, body io.Reader, location string, rename Renamer) error { body, kind, err := match(body) if err != nil { errors.Annotatef(err, "Detect archive type") @@ -60,13 +61,13 @@ func Archive(body io.Reader, location string, rename Renamer) error { switch kind.Extension { case "zip": - return Zip(body, location, rename) + return Zip(ctx, body, location, rename) case "gz": - return Gz(body, location, rename) + return Gz(ctx, body, location, rename) case "bz2": - return Bz2(body, location, rename) + return Bz2(ctx, body, location, rename) case "tar": - return Tar(body, location, rename) + return Tar(ctx, body, location, rename) default: return errors.New("Not a supported archive") } @@ -74,7 +75,7 @@ func Archive(body io.Reader, location string, rename Renamer) error { // Bz2 extracts a .bz2 or .tar.bz2 archived stream of data in the specified location. // It accepts a rename function to handle the names of the files (see the example) -func Bz2(body io.Reader, location string, rename Renamer) error { +func Bz2(ctx context.Context, body io.Reader, location string, rename Renamer) error { reader := bzip2.NewReader(body) body, kind, err := match(reader) @@ -83,10 +84,10 @@ func Bz2(body io.Reader, location string, rename Renamer) error { } if kind.Extension == "tar" { - return Tar(body, location, rename) + return Tar(ctx, body, location, rename) } - err = copy(location, 0666, body) + err = copy(ctx, location, 0666, body) if err != nil { return err } @@ -95,7 +96,7 @@ func Bz2(body io.Reader, location string, rename Renamer) error { // Gz extracts a .gz or .tar.gz archived stream of data in the specified location. // It accepts a rename function to handle the names of the files (see the example) -func Gz(body io.Reader, location string, rename Renamer) error { +func Gz(ctx context.Context, body io.Reader, location string, rename Renamer) error { reader, err := gzip.NewReader(body) if err != nil { return errors.Annotatef(err, "Gunzip") @@ -107,9 +108,9 @@ func Gz(body io.Reader, location string, rename Renamer) error { } if kind.Extension == "tar" { - return Tar(body, location, rename) + return Tar(ctx, body, location, rename) } - err = copy(location, 0666, body) + err = copy(ctx, location, 0666, body) if err != nil { return err } @@ -128,7 +129,7 @@ type link struct { // Tar extracts a .tar archived stream of data in the specified location. // It accepts a rename function to handle the names of the files (see the example) -func Tar(body io.Reader, location string, rename Renamer) error { +func Tar(ctx context.Context, body io.Reader, location string, rename Renamer) error { files := []file{} links := []link{} symlinks := []link{} @@ -137,6 +138,12 @@ func Tar(body io.Reader, location string, rename Renamer) error { // attempting to create a file where there's no folder tr := tar.NewReader(body) for { + select { + case <-ctx.Done(): + return errors.New("interrupted") + default: + } + header, err := tr.Next() if err == io.EOF { break @@ -165,7 +172,7 @@ func Tar(body io.Reader, location string, rename Renamer) error { } case tar.TypeReg, tar.TypeRegA: var data bytes.Buffer - if _, err := io.Copy(&data, tr); err != nil { + if _, err := copyCancel(ctx, &data, tr); err != nil { return errors.Annotatef(err, "Read contents of file %s", path) } files = append(files, file{Path: path, Mode: info.Mode(), Data: data}) @@ -184,18 +191,28 @@ func Tar(body io.Reader, location string, rename Renamer) error { // Now we make another pass creating the files and links for i := range files { - if err := copy(files[i].Path, files[i].Mode, &files[i].Data); err != nil { + if err := copy(ctx, files[i].Path, files[i].Mode, &files[i].Data); err != nil { return errors.Annotatef(err, "Create file %s", files[i].Path) } } for i := range links { + select { + case <-ctx.Done(): + return errors.New("interrupted") + default: + } if err := os.Link(links[i].Name, links[i].Path); err != nil { return errors.Annotatef(err, "Create link %s", links[i].Path) } } for i := range symlinks { + select { + case <-ctx.Done(): + return errors.New("interrupted") + default: + } if err := os.Symlink(symlinks[i].Name, symlinks[i].Path); err != nil { return errors.Annotatef(err, "Create link %s", symlinks[i].Path) } @@ -205,10 +222,10 @@ func Tar(body io.Reader, location string, rename Renamer) error { // Zip extracts a .zip archived stream of data in the specified location. // It accepts a rename function to handle the names of the files (see the example). -func Zip(body io.Reader, location string, rename Renamer) error { +func Zip(ctx context.Context, body io.Reader, location string, rename Renamer) error { // read the whole body into a buffer. Not sure this is the best way to do it buffer := bytes.NewBuffer([]byte{}) - io.Copy(buffer, body) + copyCancel(ctx, buffer, body) archive, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(buffer.Len())) if err != nil { @@ -221,6 +238,12 @@ func Zip(body io.Reader, location string, rename Renamer) error { // We make the first pass creating the directory structure, or we could end up // attempting to create a file where there's no folder for _, header := range archive.File { + select { + case <-ctx.Done(): + return errors.New("interrupted") + default: + } + path := header.Name if rename != nil { path = rename(path) @@ -255,7 +278,7 @@ func Zip(body io.Reader, location string, rename Renamer) error { return errors.Annotatef(err, "Open file %s", path) } var data bytes.Buffer - if _, err := io.Copy(&data, f); err != nil { + if _, err := copyCancel(ctx, &data, f); err != nil { return errors.Annotatef(err, "Read contents of file %s", path) } files = append(files, file{Path: path, Mode: info.Mode(), Data: data}) @@ -264,12 +287,17 @@ func Zip(body io.Reader, location string, rename Renamer) error { // Now we make another pass creating the files and links for i := range files { - if err := copy(files[i].Path, files[i].Mode, &files[i].Data); err != nil { + if err := copy(ctx, files[i].Path, files[i].Mode, &files[i].Data); err != nil { return errors.Annotatef(err, "Create file %s", files[i].Path) } } for i := range links { + select { + case <-ctx.Done(): + return errors.New("interrupted") + default: + } if err := os.Symlink(links[i].Name, links[i].Path); err != nil { return errors.Annotatef(err, "Create link %s", links[i].Path) } @@ -278,7 +306,7 @@ func Zip(body io.Reader, location string, rename Renamer) error { return nil } -func copy(path string, mode os.FileMode, src io.Reader) error { +func copy(ctx context.Context, path string, mode os.FileMode, src io.Reader) error { // We add the execution permission to be able to create files inside it err := os.MkdirAll(filepath.Dir(path), mode|os.ModeDir|100) if err != nil { @@ -289,7 +317,7 @@ func copy(path string, mode os.FileMode, src io.Reader) error { return err } defer file.Close() - _, err = io.Copy(file, src) + _, err = copyCancel(ctx, file, src) return err } diff --git a/vendor/go.bug.st/cleanup/LICENSE b/vendor/go.bug.st/cleanup/LICENSE new file mode 100644 index 00000000000..05cd1e25a5d --- /dev/null +++ b/vendor/go.bug.st/cleanup/LICENSE @@ -0,0 +1,33 @@ + +Copyright (c) 2018, Cristian Maglie. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/go.bug.st/cleanup/context.go b/vendor/go.bug.st/cleanup/context.go new file mode 100644 index 00000000000..3971ae99849 --- /dev/null +++ b/vendor/go.bug.st/cleanup/context.go @@ -0,0 +1,31 @@ +// +// Copyright 2018 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package cleanup + +import ( + "context" + "os" + "os/signal" +) + +// InterruptableContext adds to a context the capability to be interrupted by the os.Interrupt signal. +func InterruptableContext(inCtx context.Context) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(inCtx) + go func() { + ctrlC := make(chan os.Signal, 1) + signal.Notify(ctrlC, os.Interrupt) + select { + case <-ctx.Done(): + break // used to show test coverage + case <-ctrlC: + cancel() + } + signal.Stop(ctrlC) + close(ctrlC) + }() + return ctx, cancel +} diff --git a/vendor/go.bug.st/cleanup/go.mod b/vendor/go.bug.st/cleanup/go.mod new file mode 100644 index 00000000000..945a75a7187 --- /dev/null +++ b/vendor/go.bug.st/cleanup/go.mod @@ -0,0 +1,7 @@ +module go.bug.st/cleanup + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 +) diff --git a/vendor/go.bug.st/cleanup/go.sum b/vendor/go.bug.st/cleanup/go.sum new file mode 100644 index 00000000000..e03ee77d9e3 --- /dev/null +++ b/vendor/go.bug.st/cleanup/go.sum @@ -0,0 +1,6 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=