github.com/VertebrateResequencing/muxfys@v3.0.5+incompatible/s3.go (about) 1 // Copyright © 2017, 2018 Genome Research Limited 2 // Author: Sendu Bala <sb10@sanger.ac.uk>. 3 // The target parsing code in this file is based on code in 4 // https://github.com/minio/minfs Copyright 2016 Minio, Inc. 5 // licensed under the Apache License, Version 2.0 (the "License"), stating: 6 // "You may not use this file except in compliance with the License. You may 7 // obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0" 8 // 9 // This file is part of muxfys. 10 // 11 // muxfys is free software: you can redistribute it and/or modify 12 // it under the terms of the GNU Lesser General Public License as published by 13 // the Free Software Foundation, either version 3 of the License, or 14 // (at your option) any later version. 15 // 16 // muxfys is distributed in the hope that it will be useful, 17 // but WITHOUT ANY WARRANTY; without even the implied warranty of 18 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 // GNU Lesser General Public License for more details. 20 // 21 // You should have received a copy of the GNU Lesser General Public License 22 // along with muxfys. If not, see <http://www.gnu.org/licenses/>. 23 24 package muxfys 25 26 // This file contains an implementation of RemoteAccessor for S3-like object 27 // stores. 28 29 import ( 30 "bufio" 31 "fmt" 32 "io" 33 "net/url" 34 "os" 35 "path" 36 "path/filepath" 37 "strings" 38 39 "github.com/go-ini/ini" 40 "github.com/minio/minio-go" 41 "github.com/mitchellh/go-homedir" 42 ) 43 44 const ( 45 defaultS3Domain = "s3.amazonaws.com" 46 ) 47 48 // S3Config struct lets you provide details of the S3 bucket you wish to mount. 49 // If you have Amazon's s3cmd or other tools configured to work using config 50 // files and/or environment variables, you can make one of these with the 51 // S3ConfigFromEnvironment() method. 52 type S3Config struct { 53 // The full URL of your bucket and possible sub-path, eg. 54 // https://cog.domain.com/bucket/subpath. For performance reasons, you 55 // should specify the deepest subpath that holds all your files. 56 Target string 57 58 // Region is optional if you need to use a specific region. 59 Region string 60 61 // AccessKey and SecretKey are your access credentials, and could be empty 62 // strings for access to a public bucket. 63 AccessKey string 64 SecretKey string 65 } 66 67 // S3ConfigFromEnvironment makes an S3Config with Target, AccessKey, SecretKey 68 // and possibly Region filled in for you. 69 // 70 // It determines these by looking primarily at the given profile section of 71 // ~/.s3cfg (s3cmd's config file). If profile is an empty string, it comes from 72 // $AWS_DEFAULT_PROFILE or $AWS_PROFILE or defaults to "default". 73 // 74 // If ~/.s3cfg doesn't exist or isn't fully specified, missing values will be 75 // taken from the file pointed to by $AWS_SHARED_CREDENTIALS_FILE, or 76 // ~/.aws/credentials (in the AWS CLI format) if that is not set. 77 // 78 // If this file also doesn't exist, ~/.awssecret (in the format used by s3fs) is 79 // used instead. 80 // 81 // AccessKey and SecretKey values will always preferably come from 82 // $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY respectively, if those are set. 83 // 84 // If no config file specified host_base, the default domain used is 85 // s3.amazonaws.com. Region is set by the $AWS_DEFAULT_REGION environment 86 // variable, or if that is not set, by checking the file pointed to by 87 // $AWS_CONFIG_FILE (~/.aws/config if unset). 88 // 89 // To allow the use of a single configuration file, users can create a non- 90 // standard file that specifies all relevant options: use_https, host_base, 91 // region, access_key (or aws_access_key_id) and secret_key (or 92 // aws_secret_access_key) (saved in any of the files except ~/.awssecret). 93 // 94 // The path argument should at least be the bucket name, but ideally should also 95 // specify the deepest subpath that holds all the files that need to be 96 // accessed. Because reading from a public s3.amazonaws.com bucket requires no 97 // credentials, no error is raised on failure to find any values in the 98 // environment when profile is supplied as an empty string. 99 func S3ConfigFromEnvironment(profile, path string) (*S3Config, error) { 100 if path == "" { 101 return nil, fmt.Errorf("S3ConfigFromEnvironment requires a path") 102 } 103 104 profileSpecified := true 105 if profile == "" { 106 if profile = os.Getenv("AWS_DEFAULT_PROFILE"); profile == "" { 107 if profile = os.Getenv("AWS_PROFILE"); profile == "" { 108 profile = "default" 109 profileSpecified = false 110 } 111 } 112 } 113 114 s3cfg, err := homedir.Expand("~/.s3cfg") 115 if err != nil { 116 return nil, err 117 } 118 ascf, err := homedir.Expand(os.Getenv("AWS_SHARED_CREDENTIALS_FILE")) 119 if err != nil { 120 return nil, err 121 } 122 acred, err := homedir.Expand("~/.aws/credentials") 123 if err != nil { 124 return nil, err 125 } 126 aconf, err := homedir.Expand(os.Getenv("AWS_CONFIG_FILE")) 127 if err != nil { 128 return nil, err 129 } 130 acon, err := homedir.Expand("~/.aws/config") 131 if err != nil { 132 return nil, err 133 } 134 135 aws, err := ini.LooseLoad(s3cfg, ascf, acred, aconf, acon) 136 if err != nil { 137 return nil, fmt.Errorf("S3ConfigFromEnvironment() loose loading of config files failed: %s", err) 138 } 139 140 var domain, key, secret, region string 141 var https bool 142 section, err := aws.GetSection(profile) 143 if err == nil { 144 https = section.Key("use_https").MustBool(false) 145 domain = section.Key("host_base").String() 146 region = section.Key("region").String() 147 key = section.Key("access_key").MustString(section.Key("aws_access_key_id").MustString(os.Getenv("AWS_ACCESS_KEY_ID"))) 148 secret = section.Key("secret_key").MustString(section.Key("aws_secret_access_key").MustString(os.Getenv("AWS_SECRET_ACCESS_KEY"))) 149 } else if profileSpecified { 150 return nil, fmt.Errorf("S3ConfigFromEnvironment could not find config files with profile %s", profile) 151 } 152 153 if key == "" && secret == "" { 154 // last resort, check ~/.awssecret 155 var awsSec string 156 awsSec, err = homedir.Expand("~/.awssecret") 157 if err != nil { 158 return nil, err 159 } 160 if file, erro := os.Open(awsSec); erro == nil { 161 defer func() { 162 err = file.Close() 163 }() 164 165 scanner := bufio.NewScanner(file) 166 if scanner.Scan() { 167 line := scanner.Text() 168 if line != "" { 169 line = strings.TrimSuffix(line, "\n") 170 ks := strings.Split(line, ":") 171 if len(ks) == 2 { 172 key = ks[0] 173 secret = ks[1] 174 } 175 } 176 } 177 } 178 } 179 180 if os.Getenv("AWS_ACCESS_KEY_ID") != "" { 181 key = os.Getenv("AWS_ACCESS_KEY_ID") 182 } 183 if os.Getenv("AWS_SECRET_ACCESS_KEY") != "" { 184 secret = os.Getenv("AWS_SECRET_ACCESS_KEY") 185 } 186 187 if domain == "" { 188 domain = defaultS3Domain 189 } 190 191 scheme := "http" 192 if https { 193 scheme += "s" 194 } 195 u := &url.URL{ 196 Scheme: scheme, 197 Host: domain, 198 Path: path, 199 } 200 201 if os.Getenv("AWS_DEFAULT_REGION") != "" { 202 region = os.Getenv("AWS_DEFAULT_REGION") 203 } 204 205 return &S3Config{ 206 Target: u.String(), 207 Region: region, 208 AccessKey: key, 209 SecretKey: secret, 210 }, err 211 } 212 213 // S3Accessor implements the RemoteAccessor interface by embedding minio-go. 214 type S3Accessor struct { 215 client *minio.Client 216 bucket string 217 target string 218 host string 219 basePath string 220 } 221 222 // NewS3Accessor creates an S3Accessor for interacting with S3-like object 223 // stores. 224 func NewS3Accessor(config *S3Config) (*S3Accessor, error) { 225 // parse the target to get secure, host, bucket and basePath 226 if config.Target == "" { 227 return nil, fmt.Errorf("no Target defined") 228 } 229 230 u, err := url.Parse(config.Target) 231 if err != nil { 232 return nil, err 233 } 234 235 var secure bool 236 if strings.HasPrefix(config.Target, "https") { 237 secure = true 238 } 239 240 host := u.Host 241 var bucket, basePath string 242 if len(u.Path) > 1 { 243 parts := strings.Split(u.Path[1:], "/") 244 if len(parts) >= 0 { 245 bucket = parts[0] 246 } 247 if len(parts) >= 1 { 248 basePath = path.Join(parts[1:]...) 249 } 250 } 251 252 if bucket == "" { 253 return nil, fmt.Errorf("no bucket could be determined from [%s]", config.Target) 254 } 255 256 a := &S3Accessor{ 257 target: config.Target, 258 bucket: bucket, 259 host: host, 260 basePath: basePath, 261 } 262 263 // create a client for interacting with S3 (we do this here instead of 264 // as-needed inside remote because there's large overhead in creating these) 265 if config.Region != "" { 266 a.client, err = minio.NewWithRegion(host, config.AccessKey, config.SecretKey, secure, config.Region) 267 } else { 268 // *** we are temporarily forcing use of V2 signatures for full 269 // compatibility with ceph and uploading 0 byte files; hopefully 270 // minio-go or ceph gets bugfixed to avoid this... 271 a.client, err = minio.NewV2(host, config.AccessKey, config.SecretKey, secure) 272 } 273 274 // test that the client actually works (credentials are ok?) 275 _, err = a.ListEntries("/") 276 if err != nil { 277 err = fmt.Errorf("could not access S3: %s", err) 278 } 279 280 return a, err 281 } 282 283 // DownloadFile implements RemoteAccessor by deferring to minio. 284 func (a *S3Accessor) DownloadFile(source, dest string) error { 285 return a.client.FGetObject(a.bucket, source, dest, minio.GetObjectOptions{}) 286 } 287 288 // UploadFile implements RemoteAccessor by deferring to minio. 289 func (a *S3Accessor) UploadFile(source, dest, contentType string) error { 290 _, err := a.client.FPutObject(a.bucket, dest, source, minio.PutObjectOptions{ContentType: contentType}) 291 return err 292 } 293 294 // UploadData implements RemoteAccessor by deferring to minio. 295 func (a *S3Accessor) UploadData(data io.Reader, dest string) error { 296 //*** try and do our own buffered read to initially get the mime type? 297 _, err := a.client.PutObject(a.bucket, dest, data, -1, minio.PutObjectOptions{}) 298 return err 299 } 300 301 // ListEntries implements RemoteAccessor by deferring to minio. 302 func (a *S3Accessor) ListEntries(dir string) ([]RemoteAttr, error) { 303 doneCh := make(chan struct{}) 304 oiCh := a.client.ListObjects(a.bucket, dir, false, doneCh) 305 var ras []RemoteAttr 306 for oi := range oiCh { 307 if oi.Err != nil { 308 close(doneCh) 309 return nil, oi.Err 310 } 311 ras = append(ras, RemoteAttr{ 312 Name: oi.Key, 313 Size: oi.Size, 314 MTime: oi.LastModified, 315 MD5: oi.ETag, 316 }) 317 } 318 return ras, nil 319 } 320 321 // OpenFile implements RemoteAccessor by deferring to minio. 322 func (a *S3Accessor) OpenFile(path string, offset int64) (io.ReadCloser, error) { 323 opts := minio.GetObjectOptions{} 324 if offset > 0 { 325 err := opts.SetRange(offset, 0) 326 if err != nil { 327 return nil, err 328 } 329 } 330 core := minio.Core{Client: a.client} 331 reader, _, err := core.GetObject(a.bucket, path, opts) 332 return reader, err 333 } 334 335 // Seek implements RemoteAccessor by deferring to minio. 336 func (a *S3Accessor) Seek(path string, rc io.ReadCloser, offset int64) (io.ReadCloser, error) { 337 err := rc.Close() 338 if err != nil { 339 return nil, err 340 } 341 opts := minio.GetObjectOptions{} 342 err = opts.SetRange(offset, 0) 343 if err != nil { 344 return nil, err 345 } 346 core := minio.Core{Client: a.client} 347 reader, _, err := core.GetObject(a.bucket, path, opts) 348 return reader, err 349 } 350 351 // CopyFile implements RemoteAccessor by deferring to minio. 352 func (a *S3Accessor) CopyFile(source, dest string) error { 353 destInfo, _ := minio.NewDestinationInfo(a.bucket, dest, nil, nil) 354 return a.client.CopyObject(destInfo, minio.NewSourceInfo(a.bucket, source, nil)) 355 } 356 357 // DeleteFile implements RemoteAccessor by deferring to minio. 358 func (a *S3Accessor) DeleteFile(path string) error { 359 return a.client.RemoveObject(a.bucket, path) 360 } 361 362 // DeleteIncompleteUpload implements RemoteAccessor by deferring to minio. 363 func (a *S3Accessor) DeleteIncompleteUpload(path string) error { 364 return a.client.RemoveIncompleteUpload(a.bucket, path) 365 } 366 367 // ErrorIsNotExists implements RemoteAccessor by looking for the NoSuchKey error 368 // code. 369 func (a *S3Accessor) ErrorIsNotExists(err error) bool { 370 merr, ok := err.(minio.ErrorResponse) 371 return ok && merr.Code == "NoSuchKey" 372 } 373 374 // ErrorIsNoQuota implements RemoteAccessor by looking for the QuotaExceeded 375 // error code. 376 func (a *S3Accessor) ErrorIsNoQuota(err error) bool { 377 merr, ok := err.(minio.ErrorResponse) 378 return ok && merr.Code == "QuotaExceeded" 379 } 380 381 // Target implements RemoteAccessor by returning the initial target we were 382 // configured with. 383 func (a *S3Accessor) Target() string { 384 return a.target 385 } 386 387 // RemotePath implements RemoteAccessor by using the initially configured base 388 // path. 389 func (a *S3Accessor) RemotePath(relPath string) string { 390 return filepath.Join(a.basePath, relPath) 391 } 392 393 // LocalPath implements RemoteAccessor by including the initially configured 394 // host and bucket in the return value. 395 func (a *S3Accessor) LocalPath(baseDir, remotePath string) string { 396 return filepath.Join(baseDir, a.host, a.bucket, remotePath) 397 }