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 ®ion{ 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 }