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 `