Home Download Docs Code Community
     1	/*
     2	Copyright 2011 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	/*
    18	Package s3 registers the "s3" blobserver storage type, storing
    19	blobs in an Amazon Web Services' S3 storage bucket.
    20	
    21	Example low-level config:
    22	
    23		"/r1/": {
    24		    "handler": "storage-s3",
    25		    "handlerArgs": {
    26		       "bucket": "foo",
    27		       "aws_region": "us-east-1",
    28		       "aws_access_key": "...",
    29		       "aws_secret_access_key": "...",
    30		       "skipStartupCheck": false
    31		     }
    32		},
    33	*/
    34	package s3 // import "perkeep.org/pkg/blobserver/s3"
    35	
    36	import (
    37		"context"
    38		"fmt"
    39		"net/http"
    40		"strings"
    41	
    42		"perkeep.org/pkg/blob"
    43		"perkeep.org/pkg/blobserver"
    44		"perkeep.org/pkg/blobserver/memory"
    45		"perkeep.org/pkg/blobserver/proxycache"
    46	
    47		"github.com/aws/aws-sdk-go/aws"
    48		"github.com/aws/aws-sdk-go/aws/awserr"
    49		"github.com/aws/aws-sdk-go/aws/credentials"
    50		"github.com/aws/aws-sdk-go/aws/session"
    51		"github.com/aws/aws-sdk-go/service/s3"
    52		"github.com/aws/aws-sdk-go/service/s3/s3iface"
    53		"go4.org/fault"
    54		"go4.org/jsonconfig"
    55	)
    56	
    57	var (
    58		_ blob.SubFetcher               = (*s3Storage)(nil)
    59		_ blobserver.MaxEnumerateConfig = (*s3Storage)(nil)
    60	)
    61	
    62	var (
    63		faultReceive   = fault.NewInjector("s3_receive")
    64		faultEnumerate = fault.NewInjector("s3_enumerate")
    65		faultStat      = fault.NewInjector("s3_stat")
    66		faultGet       = fault.NewInjector("s3_get")
    67	)
    68	
    69	type s3Storage struct {
    70		client s3iface.S3API
    71		bucket string
    72		// optional "directory" where the blobs are stored, instead of at the root of the bucket.
    73		// S3 is actually flat, which in effect just means that all the objects should have this
    74		// dirPrefix as a prefix of their key.
    75		// If non empty, it should be a slash separated path with a trailing slash and no starting
    76		// slash.
    77		dirPrefix string
    78		// hostname indicates the hostname of the server providing an S3 compatible endpoint.
    79		// It should not be set for AWS's S3 since the correct endpoint will be
    80		// automatically identified based on the bucket name (and, if provided, the
    81		// 'aws_region' low-level config option).
    82		hostname string
    83	}
    84	
    85	func (s *s3Storage) String() string {
    86		if s.dirPrefix != "" {
    87			return fmt.Sprintf("\"S3\" blob storage at host %q, bucket %q, directory %q", s.hostname, s.bucket, s.dirPrefix)
    88		}
    89		return fmt.Sprintf("\"S3\" blob storage at host %q, bucket %q", s.hostname, s.bucket)
    90	}
    91	
    92	func newFromConfig(l blobserver.Loader, config jsonconfig.Obj) (blobserver.Storage, error) {
    93		return newFromConfigWithTransport(l, config, nil)
    94	}
    95	
    96	// newFromConfigWithTransport constructs a s3 blobserver using the given
    97	// transport for all s3 requests.  The transport may be set to 'nil' to use a
    98	// default transport.
    99	// This is used for unit tests.
   100	func newFromConfigWithTransport(_ blobserver.Loader, config jsonconfig.Obj, transport http.RoundTripper) (blobserver.Storage, error) {
   101		hostname := config.OptionalString("hostname", "")
   102		region := config.OptionalString("aws_region", "us-east-1")
   103	
   104		cacheSize := config.OptionalInt64("cacheSize", 32<<20)
   105		s3Cfg := aws.NewConfig().WithCredentials(credentials.NewStaticCredentials(
   106			config.RequiredString("aws_access_key"),
   107			config.RequiredString("aws_secret_access_key"),
   108			"",
   109		))
   110		if hostname != "" {
   111			s3Cfg.WithEndpoint(hostname)
   112		}
   113		s3Cfg.WithRegion(region)
   114		if transport != nil {
   115			httpClient := *http.DefaultClient
   116			httpClient.Transport = transport
   117			s3Cfg.WithHTTPClient(&httpClient)
   118		}
   119		awsSession := session.New(s3Cfg)
   120	
   121		bucket := config.RequiredString("bucket")
   122		var dirPrefix string
   123		if parts := strings.SplitN(bucket, "/", 2); len(parts) > 1 {
   124			dirPrefix = parts[1]
   125			bucket = parts[0]
   126		}
   127		if dirPrefix != "" && !strings.HasSuffix(dirPrefix, "/") {
   128			dirPrefix += "/"
   129		}
   130	
   131		skipStartupCheck := config.OptionalBool("skipStartupCheck", false)
   132		if err := config.Validate(); err != nil {
   133			return nil, err
   134		}
   135	
   136		ctx := context.TODO() // TODO: 5 min timeout or something?
   137		if !skipStartupCheck {
   138			info, err := normalizeBucketLocation(ctx, awsSession, hostname, bucket, region)
   139			if err != nil {
   140				return nil, err
   141			}
   142			awsSession.Config.WithRegion(info.region)
   143			awsSession.Config.WithEndpoint(info.endpoint)
   144			if !info.isAWS {
   145				awsSession.Config.WithS3ForcePathStyle(true)
   146			}
   147		} else {
   148			// safer default if we can't determine more info
   149			awsSession.Config.WithS3ForcePathStyle(true)
   150		}
   151	
   152		sto := &s3Storage{
   153			client:    s3.New(awsSession),
   154			bucket:    bucket,
   155			dirPrefix: dirPrefix,
   156			hostname:  hostname,
   157		}
   158	
   159		if cacheSize != 0 {
   160			// This has two layers of LRU caching (proxycache and memory).
   161			// We make the outer one 4x the size so that it doesn't evict from the
   162			// underlying one when it's about to perform its own eviction.
   163			return proxycache.New(cacheSize<<2, memory.NewCache(cacheSize), sto), nil
   164		}
   165		return sto, nil
   166	}
   167	
   168	func init() {
   169		blobserver.RegisterStorageConstructor("s3", blobserver.StorageConstructor(newFromConfig))
   170		blobserver.RegisterStorageConstructor("b2", blobserver.StorageConstructor(newFromConfig))
   171	}
   172	
   173	// isNotFound checks for s3 errors which indicate the object doesn't exist.
   174	func isNotFound(err error) bool {
   175		if err == nil {
   176			return false
   177		}
   178		if aerr, ok := err.(awserr.Error); ok {
   179			return aerr.Code() == s3.ErrCodeNoSuchKey ||
   180				// Check 'NotFound' as well because it's returned for some requests, even
   181				// though the API model doesn't include it (hence why there isn't an
   182				// 's3.ErrCodeNotFound' for comparison)
   183				aerr.Code() == "NotFound"
   184		}
   185		return false
   186	}
Website layout inspired by memcached.
Content by the authors.