github.com/dahs81/otto@v0.2.1-0.20160126165905-6400716cf085/helper/hashitools/installer_go.go (about)

     1  package hashitools
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/signal"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/hashicorp/go-cleanhttp"
    15  	"github.com/hashicorp/go-version"
    16  	"github.com/hashicorp/otto/ui"
    17  	"github.com/mitchellh/ioprogress"
    18  	"gopkg.in/flosch/pongo2.v3"
    19  )
    20  
    21  // GoInstaller is an Installer that knows how to install Go projects.
    22  type GoInstaller struct {
    23  	// Name is the name of the project to install
    24  	Name string
    25  
    26  	// Dir is the directory where projects will be installed. They will
    27  	// be installed to a sub-directory of the project name. Example:
    28  	// if Dir is "/foo", then the Packer binary would be installed to
    29  	// "/foo/packer/packer"
    30  	Dir string
    31  
    32  	// Ui is the Otto UI for asking the user for input and outputting
    33  	// the status of installation.
    34  	Ui ui.Ui
    35  }
    36  
    37  func (i *GoInstaller) InstallAsk(installed, required, latest *version.Version) (bool, error) {
    38  	input := &ui.InputOpts{
    39  		Id:      fmt.Sprintf("%s_install", i.Name),
    40  		Query:   fmt.Sprintf("Would you like Otto to install %s?", strings.Title(i.Name)),
    41  		Default: "",
    42  	}
    43  
    44  	// Figure out the description text to use for input
    45  	var tplString string
    46  	if installed == nil {
    47  		tplString = installRequired
    48  	} else {
    49  		tplString = installRequiredUpdate
    50  	}
    51  
    52  	// Parse the template and render it
    53  	tpl, err := pongo2.FromString(strings.TrimSpace(tplString))
    54  	if err != nil {
    55  		return false, err
    56  	}
    57  	input.Description, err = tpl.Execute(map[string]interface{}{
    58  		"name":      i.Name,
    59  		"installed": installed,
    60  		"latest":    latest,
    61  		"required":  required,
    62  	})
    63  	if err != nil {
    64  		return false, err
    65  	}
    66  
    67  	result, err := i.Ui.Input(input)
    68  	if err != nil {
    69  		return false, err
    70  	}
    71  
    72  	return strings.ToLower(result) == "yes", nil
    73  }
    74  
    75  func (i *GoInstaller) Install(vsn *version.Version) error {
    76  	// All Go projects use a standard URL format
    77  	url := fmt.Sprintf(
    78  		"https://releases.hashicorp.com/%s/%s/%s_%s_%s_%s.zip",
    79  		i.Name, vsn, i.Name, vsn, runtime.GOOS, runtime.GOARCH)
    80  
    81  	// Create the temporary directory where we'll store the data
    82  	td, err := ioutil.TempDir("", "otto")
    83  	if err != nil {
    84  		return err
    85  	}
    86  	defer os.RemoveAll(td)
    87  
    88  	// Create the ZIP file
    89  	zipPath := filepath.Join(td, "project.zip")
    90  	f, err := os.Create(zipPath)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// Download the ZIP
    96  	i.Ui.Header(fmt.Sprintf("Downloading %s v%s...", i.Name, vsn))
    97  	i.Ui.Message("URL: " + url)
    98  	i.Ui.Message("")
    99  	resp, err := cleanhttp.DefaultClient().Get(url)
   100  	if err != nil {
   101  		f.Close()
   102  		return err
   103  	}
   104  	if resp.StatusCode != 200 {
   105  		resp.Body.Close()
   106  		f.Close()
   107  		return fmt.Errorf("Error downloading, status code %d", resp.StatusCode)
   108  	}
   109  
   110  	// Build the progress bar for our download
   111  	progressR := &ioprogress.Reader{
   112  		Reader:   resp.Body,
   113  		Size:     resp.ContentLength,
   114  		DrawFunc: ioprogress.DrawTerminalf(os.Stdout, i.progressFormat),
   115  	}
   116  
   117  	// Listen for interrupts so we can cancel the download
   118  	sigCh := make(chan os.Signal, 1)
   119  	signal.Notify(sigCh, os.Interrupt)
   120  	defer signal.Stop(sigCh)
   121  
   122  	// Copy the zip data
   123  	errCh := make(chan error, 1)
   124  	go func() {
   125  		_, err := io.Copy(f, progressR)
   126  		errCh <- err
   127  	}()
   128  
   129  	// Wait for an interrupt or finish
   130  	select {
   131  	case err = <-errCh:
   132  	case <-sigCh:
   133  		err = fmt.Errorf("interrupted")
   134  	}
   135  
   136  	// Finish up
   137  	resp.Body.Close()
   138  	f.Close()
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	// Open the zip file
   144  	i.Ui.Header("Unzipping downloaded package...")
   145  	zipR, err := zip.OpenReader(zipPath)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	defer zipR.Close()
   150  
   151  	// Clear our install directory
   152  	installDir := filepath.Join(i.Dir, i.Name)
   153  	if err := os.RemoveAll(installDir); err != nil {
   154  		return err
   155  	}
   156  	if err := os.MkdirAll(installDir, 0755); err != nil {
   157  		return err
   158  	}
   159  
   160  	// Copy all the files
   161  	for _, f := range zipR.File {
   162  		dst, err := os.OpenFile(
   163  			filepath.Join(installDir, f.Name),
   164  			os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
   165  			f.Mode())
   166  		if err != nil {
   167  			return err
   168  		}
   169  
   170  		fr, err := f.Open()
   171  		if err != nil {
   172  			dst.Close()
   173  			return err
   174  		}
   175  
   176  		_, err = io.Copy(dst, fr)
   177  		fr.Close()
   178  		dst.Close()
   179  		if err != nil {
   180  			return err
   181  		}
   182  	}
   183  
   184  	i.Ui.Header(fmt.Sprintf("[green]%s installed successfully!", i.Name))
   185  	return nil
   186  }
   187  
   188  func (i *GoInstaller) Path() string {
   189  	path := filepath.Join(i.Dir, i.Name, i.Name)
   190  	if _, err := os.Stat(path); err == nil {
   191  		return path
   192  	}
   193  
   194  	return ""
   195  }
   196  
   197  func (i *GoInstaller) progressFormat(progress, total int64) string {
   198  	return fmt.Sprintf("    %s", ioprogress.DrawTextFormatBytes(progress, total))
   199  }
   200  
   201  const installRequired = `
   202  Otto requires {{name}} to be installed, but it couldn't be found on your
   203  system. Otto can install the latest version of {{name}} for you. Otto will
   204  install this into its own private data directory so it doesn't conflict
   205  with anything else on your system. Would you like Otto to install {{name}}
   206  for you? Alternatively, you may install this on your own.
   207  
   208  If you answer yes, Otto will install {{name}} version {{latest}}.
   209  
   210  Please enter 'yes' to continue. Any other value will exit.
   211  `
   212  
   213  const installRequiredUpdate = `
   214  An older version of {{name}} was found installed ({{installed}}). Otto requires
   215  version {{required}} or higher. Otto can install the latest version of {{name}}
   216  for you ({{latest}}). Would you like Otto to update {{name}} for you?
   217  
   218  Please enter 'yes' to continue. Any other value will exit.
   219  `