github.com/puellanivis/breton@v0.2.16/lib/files/s3files/s3.go (about)

     1  // Package s3files implements the "s3:" URL scheme.
     2  package s3files
     3  
     4  import (
     5  	"context"
     6  	"net/http"
     7  	"net/url"
     8  	"os"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/puellanivis/breton/lib/files"
    14  	"github.com/puellanivis/breton/lib/files/wrapper"
    15  
    16  	"github.com/aws/aws-sdk-go/aws"
    17  	"github.com/aws/aws-sdk-go/aws/session"
    18  	"github.com/aws/aws-sdk-go/service/s3"
    19  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    20  )
    21  
    22  type region struct {
    23  	region string
    24  
    25  	sess *session.Session
    26  	cl   *s3.S3
    27  }
    28  
    29  type handler struct {
    30  	mu sync.Mutex
    31  
    32  	defRegion string
    33  	rmap      map[string]*region
    34  }
    35  
    36  const defaultRegion = "us-east-1"
    37  
    38  func init() {
    39  	h := &handler{
    40  		defRegion: defaultRegion,
    41  		rmap:      make(map[string]*region),
    42  	}
    43  
    44  	files.RegisterScheme(h, "s3")
    45  }
    46  
    47  func newRegion(r string) (*region, error) {
    48  	conf := &aws.Config{
    49  		Region: aws.String(r),
    50  	}
    51  
    52  	sess, err := session.NewSession(conf)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	return &region{
    58  		region: r,
    59  		sess:   sess,
    60  		cl:     s3.New(sess, conf),
    61  	}, nil
    62  }
    63  
    64  // lookup looks up a specific region from the handler’s map.
    65  //
    66  // Caller MUST be holding the handler‘s mutex.
    67  func (h *handler) lookup(region string) (*region, error) {
    68  	if r := h.rmap[region]; r != nil {
    69  		return r, nil
    70  	}
    71  
    72  	r, err := newRegion(region)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	h.rmap[region] = r
    77  
    78  	return r, nil
    79  }
    80  
    81  func (h *handler) getClient(ctx context.Context, bucket string) (*s3.S3, error) {
    82  	h.mu.Lock()
    83  	defer h.mu.Unlock()
    84  
    85  	region := h.defRegion
    86  	if i := strings.LastIndexByte(bucket, '.'); i >= 0 {
    87  		bucket, region = bucket[:i], bucket[i+1:]
    88  	}
    89  
    90  	r, err := h.lookup(region)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	region, err = s3manager.GetBucketRegion(ctx, r.sess, bucket, region)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	r, err = h.lookup(region)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	return r.cl, nil
   106  }
   107  
   108  func getBucketKey(op string, uri *url.URL) (bucket, key string, err error) {
   109  	if uri.Host == "" || uri.Path == "" {
   110  		return "", "", files.PathError(op, uri.String(), os.ErrInvalid)
   111  	}
   112  
   113  	return uri.Host, uri.Path, nil
   114  }
   115  
   116  func (h *handler) List(ctx context.Context, uri *url.URL) ([]os.FileInfo, error) {
   117  	if uri.Host == "" {
   118  		return nil, files.PathError("list", uri.String(), os.ErrInvalid)
   119  	}
   120  
   121  	bucket, key := uri.Host, strings.TrimPrefix(uri.Path, "/")
   122  
   123  	cl, err := h.getClient(ctx, bucket)
   124  	if err != nil {
   125  		return nil, files.PathError("list", uri.String(), err)
   126  	}
   127  
   128  	req := &s3.ListObjectsInput{
   129  		Bucket:    aws.String(bucket),
   130  		Delimiter: aws.String("/"),
   131  		Prefix:    aws.String(key),
   132  	}
   133  
   134  	res, err := cl.ListObjectsWithContext(ctx, req)
   135  	if err != nil {
   136  		return nil, files.PathError("list", uri.String(), normalizeError(err))
   137  	}
   138  
   139  	var fi []os.FileInfo
   140  	for _, o := range res.Contents {
   141  		var name string
   142  		if o.Key != nil {
   143  			name = *o.Key
   144  		}
   145  
   146  		var sz int64
   147  		if o.Size != nil {
   148  			sz = *o.Size
   149  		}
   150  
   151  		var lm time.Time
   152  		if o.LastModified != nil {
   153  			lm = *o.LastModified
   154  		}
   155  
   156  		uri := &url.URL{
   157  			Scheme: uri.Scheme,
   158  			Host:   bucket,
   159  			Path:   name,
   160  		}
   161  
   162  		fi = append(fi, wrapper.NewInfo(uri, int(sz), lm))
   163  	}
   164  
   165  	return fi, nil
   166  }
   167  
   168  func normalizeError(err error) error {
   169  	type StatusCoder interface{ StatusCode() int }
   170  
   171  	sc, ok := err.(StatusCoder)
   172  	if !ok {
   173  		return err
   174  	}
   175  
   176  	switch sc.StatusCode() {
   177  	case http.StatusUnauthorized, http.StatusForbidden:
   178  		return os.ErrPermission
   179  	case http.StatusNotFound:
   180  		return os.ErrNotExist
   181  	default:
   182  		return err
   183  	}
   184  }