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  }