1package getter 2 3import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 urlhelper "github.com/hashicorp/go-getter/helper/url" 13 safetemp "github.com/hashicorp/go-safetemp" 14) 15 16// Client is a client for downloading things. 17// 18// Top-level functions such as Get are shortcuts for interacting with a client. 19// Using a client directly allows more fine-grained control over how downloading 20// is done, as well as customizing the protocols supported. 21type Client struct { 22 // Ctx for cancellation 23 Ctx context.Context 24 25 // Src is the source URL to get. 26 // 27 // Dst is the path to save the downloaded thing as. If Dir is set to 28 // true, then this should be a directory. If the directory doesn't exist, 29 // it will be created for you. 30 // 31 // Pwd is the working directory for detection. If this isn't set, some 32 // detection may fail. Client will not default pwd to the current 33 // working directory for security reasons. 34 Src string 35 Dst string 36 Pwd string 37 38 // Mode is the method of download the client will use. See ClientMode 39 // for documentation. 40 Mode ClientMode 41 42 // Umask is used to mask file permissions when storing local files or decompressing 43 // an archive 44 Umask os.FileMode 45 46 // Detectors is the list of detectors that are tried on the source. 47 // If this is nil, then the default Detectors will be used. 48 Detectors []Detector 49 50 // Decompressors is the map of decompressors supported by this client. 51 // If this is nil, then the default value is the Decompressors global. 52 Decompressors map[string]Decompressor 53 54 // Getters is the map of protocols supported by this client. If this 55 // is nil, then the default Getters variable will be used. 56 Getters map[string]Getter 57 58 // Dir, if true, tells the Client it is downloading a directory (versus 59 // a single file). This distinction is necessary since filenames and 60 // directory names follow the same format so disambiguating is impossible 61 // without knowing ahead of time. 62 // 63 // WARNING: deprecated. If Mode is set, that will take precedence. 64 Dir bool 65 66 // ProgressListener allows to track file downloads. 67 // By default a no op progress listener is used. 68 ProgressListener ProgressTracker 69 70 Options []ClientOption 71} 72 73// umask returns the effective umask for the Client, defaulting to the process umask 74func (c *Client) umask() os.FileMode { 75 if c == nil { 76 return 0 77 } 78 return c.Umask 79} 80 81// mode returns file mode umasked by the Client umask 82func (c *Client) mode(mode os.FileMode) os.FileMode { 83 m := mode & ^c.umask() 84 return m 85} 86 87// Get downloads the configured source to the destination. 88func (c *Client) Get() error { 89 if err := c.Configure(c.Options...); err != nil { 90 return err 91 } 92 93 // Store this locally since there are cases we swap this 94 mode := c.Mode 95 if mode == ClientModeInvalid { 96 if c.Dir { 97 mode = ClientModeDir 98 } else { 99 mode = ClientModeFile 100 } 101 } 102 103 src, err := Detect(c.Src, c.Pwd, c.Detectors) 104 if err != nil { 105 return err 106 } 107 108 // Determine if we have a forced protocol, i.e. "git::http://..." 109 force, src := getForcedGetter(src) 110 111 // If there is a subdir component, then we download the root separately 112 // and then copy over the proper subdir. 113 var realDst string 114 dst := c.Dst 115 src, subDir := SourceDirSubdir(src) 116 if subDir != "" { 117 td, tdcloser, err := safetemp.Dir("", "getter") 118 if err != nil { 119 return err 120 } 121 defer tdcloser.Close() 122 123 realDst = dst 124 dst = td 125 } 126 127 u, err := urlhelper.Parse(src) 128 if err != nil { 129 return err 130 } 131 if force == "" { 132 force = u.Scheme 133 } 134 135 g, ok := c.Getters[force] 136 if !ok { 137 return fmt.Errorf( 138 "download not supported for scheme '%s'", force) 139 } 140 141 // We have magic query parameters that we use to signal different features 142 q := u.Query() 143 144 // Determine if we have an archive type 145 archiveV := q.Get("archive") 146 if archiveV != "" { 147 // Delete the paramter since it is a magic parameter we don't 148 // want to pass on to the Getter 149 q.Del("archive") 150 u.RawQuery = q.Encode() 151 152 // If we can parse the value as a bool and it is false, then 153 // set the archive to "-" which should never map to a decompressor 154 if b, err := strconv.ParseBool(archiveV); err == nil && !b { 155 archiveV = "-" 156 } 157 } 158 if archiveV == "" { 159 // We don't appear to... but is it part of the filename? 160 matchingLen := 0 161 for k := range c.Decompressors { 162 if strings.HasSuffix(u.Path, "."+k) && len(k) > matchingLen { 163 archiveV = k 164 matchingLen = len(k) 165 } 166 } 167 } 168 169 // If we have a decompressor, then we need to change the destination 170 // to download to a temporary path. We unarchive this into the final, 171 // real path. 172 var decompressDst string 173 var decompressDir bool 174 decompressor := c.Decompressors[archiveV] 175 if decompressor != nil { 176 // Create a temporary directory to store our archive. We delete 177 // this at the end of everything. 178 td, err := ioutil.TempDir("", "getter") 179 if err != nil { 180 return fmt.Errorf( 181 "Error creating temporary directory for archive: %s", err) 182 } 183 defer os.RemoveAll(td) 184 185 // Swap the download directory to be our temporary path and 186 // store the old values. 187 decompressDst = dst 188 decompressDir = mode != ClientModeFile 189 dst = filepath.Join(td, "archive") 190 mode = ClientModeFile 191 } 192 193 // Determine checksum if we have one 194 checksum, err := c.extractChecksum(u) 195 if err != nil { 196 return fmt.Errorf("invalid checksum: %s", err) 197 } 198 199 // Delete the query parameter if we have it. 200 q.Del("checksum") 201 u.RawQuery = q.Encode() 202 203 if mode == ClientModeAny { 204 // Ask the getter which client mode to use 205 mode, err = g.ClientMode(u) 206 if err != nil { 207 return err 208 } 209 210 // Destination is the base name of the URL path in "any" mode when 211 // a file source is detected. 212 if mode == ClientModeFile { 213 filename := filepath.Base(u.Path) 214 215 // Determine if we have a custom file name 216 if v := q.Get("filename"); v != "" { 217 // Delete the query parameter if we have it. 218 q.Del("filename") 219 u.RawQuery = q.Encode() 220 221 filename = v 222 } 223 224 dst = filepath.Join(dst, filename) 225 } 226 } 227 228 // If we're not downloading a directory, then just download the file 229 // and return. 230 if mode == ClientModeFile { 231 getFile := true 232 if checksum != nil { 233 if err := checksum.checksum(dst); err == nil { 234 // don't get the file if the checksum of dst is correct 235 getFile = false 236 } 237 } 238 if getFile { 239 err := g.GetFile(dst, u) 240 if err != nil { 241 return err 242 } 243 244 if checksum != nil { 245 if err := checksum.checksum(dst); err != nil { 246 return err 247 } 248 } 249 } 250 251 if decompressor != nil { 252 // We have a decompressor, so decompress the current destination 253 // into the final destination with the proper mode. 254 err := decompressor.Decompress(decompressDst, dst, decompressDir, c.umask()) 255 if err != nil { 256 return err 257 } 258 259 // Swap the information back 260 dst = decompressDst 261 if decompressDir { 262 mode = ClientModeAny 263 } else { 264 mode = ClientModeFile 265 } 266 } 267 268 // We check the dir value again because it can be switched back 269 // if we were unarchiving. If we're still only Get-ing a file, then 270 // we're done. 271 if mode == ClientModeFile { 272 return nil 273 } 274 } 275 276 // If we're at this point we're either downloading a directory or we've 277 // downloaded and unarchived a directory and we're just checking subdir. 278 // In the case we have a decompressor we don't Get because it was Get 279 // above. 280 if decompressor == nil { 281 // If we're getting a directory, then this is an error. You cannot 282 // checksum a directory. TODO: test 283 if checksum != nil { 284 return fmt.Errorf( 285 "checksum cannot be specified for directory download") 286 } 287 288 // We're downloading a directory, which might require a bit more work 289 // if we're specifying a subdir. 290 err := g.Get(dst, u) 291 if err != nil { 292 err = fmt.Errorf("error downloading '%s': %s", src, err) 293 return err 294 } 295 } 296 297 // If we have a subdir, copy that over 298 if subDir != "" { 299 if err := os.RemoveAll(realDst); err != nil { 300 return err 301 } 302 if err := os.MkdirAll(realDst, c.mode(0755)); err != nil { 303 return err 304 } 305 306 // Process any globs 307 subDir, err := SubdirGlob(dst, subDir) 308 if err != nil { 309 return err 310 } 311 312 return copyDir(c.Ctx, realDst, subDir, false, c.umask()) 313 } 314 315 return nil 316} 317