github.com/code-to-go/safepool.lib@v0.0.0-20221205180519-ee25e63c226e/transport/s3.go (about)

     1  package transport
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"net/url"
     9  	"path"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/code-to-go/safepool.lib/core"
    14  
    15  	"github.com/aws/aws-sdk-go/aws"
    16  	"github.com/aws/aws-sdk-go/aws/awserr"
    17  	"github.com/aws/aws-sdk-go/aws/credentials"
    18  	"github.com/aws/aws-sdk-go/aws/session"
    19  	"github.com/aws/aws-sdk-go/service/s3"
    20  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    21  	"github.com/sirupsen/logrus"
    22  )
    23  
    24  type S3Config struct {
    25  	Region     string `json:"region" yaml:"region"`
    26  	Endpoint   string `json:"endpoint" yaml:"endpoint"`
    27  	Bucket     string `json:"bucket" yaml:"bucket"`
    28  	AccessKey  string `json:"accessKey" yaml:"accessKey"`
    29  	Secret     string `json:"secret" yaml:"secret"`
    30  	DisableSSL bool   `json:"disableSSL" yaml:"disableSSL"`
    31  }
    32  
    33  type S3 struct {
    34  	uploader *s3manager.Uploader
    35  	svc      *s3.S3
    36  	bucket   string
    37  	url      string
    38  	touch    map[string]time.Time
    39  }
    40  
    41  func getAWSConfig(c S3Config) *aws.Config {
    42  	s3c := aws.Config{}
    43  	if c.Region != "" {
    44  		s3c.Region = aws.String(c.Region)
    45  	}
    46  	if c.AccessKey != "" && c.Secret != "" {
    47  		s3c.Credentials = credentials.NewStaticCredentials(
    48  			c.AccessKey,
    49  			c.Secret,
    50  			"",
    51  		)
    52  	}
    53  	if c.Endpoint != "" {
    54  		s3c.Endpoint = aws.String(c.Endpoint)
    55  	}
    56  	s3c.DisableSSL = aws.Bool(c.DisableSSL)
    57  	return &s3c
    58  }
    59  
    60  func NewS3(c S3Config) (Exchanger, error) {
    61  	url := fmt.Sprintf("s3://%s@%s/%s#region-%s", c.AccessKey, c.Endpoint, c.Bucket, c.Region)
    62  	sess, err := session.NewSession(getAWSConfig(c))
    63  	if core.IsErr(err, "cannot create S3 session for %s:%v", url) {
    64  		return nil, err
    65  	}
    66  
    67  	s := &S3{
    68  		uploader: s3manager.NewUploader(sess),
    69  		svc:      s3.New(sess),
    70  		url:      url,
    71  		bucket:   c.Bucket,
    72  		touch:    map[string]time.Time{},
    73  	}
    74  	err = s.createBucketIfNeeded()
    75  	return s, err
    76  }
    77  
    78  func (s *S3) createBucketIfNeeded() error {
    79  	_, err := s.svc.HeadBucket(&s3.HeadBucketInput{
    80  		Bucket: aws.String(s.bucket),
    81  	})
    82  	if err == nil {
    83  		return err
    84  	}
    85  
    86  	_, err = s.svc.CreateBucket(&s3.CreateBucketInput{
    87  		Bucket: aws.String(s.bucket),
    88  	})
    89  	if err != nil {
    90  		logrus.Errorf("cannot create bucket %s: %v", s.bucket, err)
    91  	}
    92  
    93  	return err
    94  }
    95  
    96  func (s *S3) Touched(name string) bool {
    97  	touchFile := fmt.Sprintf("%s.touch", name)
    98  	h, err := s.svc.HeadObject(&s3.HeadObjectInput{
    99  		Bucket: aws.String(s.bucket),
   100  		Key:    aws.String(touchFile),
   101  	})
   102  
   103  	touched := err != nil || h.LastModified.After(s.touch[name])
   104  	if touched {
   105  		if !core.IsErr(s.Write(touchFile, &bytes.Buffer{}), "cannot write touch file: %v") {
   106  			s.touch[name] = *h.LastModified
   107  		}
   108  	}
   109  	return touched
   110  }
   111  
   112  func (s *S3) Read(name string, rang *Range, dest io.Writer) error {
   113  	var r *string
   114  	if rang != nil {
   115  		r = aws.String(fmt.Sprintf("byte%d-%d", rang.From, rang.To))
   116  	}
   117  
   118  	rawObject, err := s.svc.GetObject(
   119  		&s3.GetObjectInput{
   120  			Bucket: &s.bucket,
   121  			Key:    &name,
   122  			Range:  r,
   123  		})
   124  	if err != nil {
   125  		logrus.Errorf("cannot read %s/%s: %v", s, name, err)
   126  		return err
   127  	}
   128  
   129  	// b, err := io.ReadAll(rawObject.Body)
   130  	// dest.Write(b)
   131  	io.Copy(dest, rawObject.Body)
   132  	// print(n)
   133  	rawObject.Body.Close()
   134  	return nil
   135  }
   136  
   137  func (s *S3) Write(name string, source io.Reader) error {
   138  
   139  	_, err := s.uploader.Upload(&s3manager.UploadInput{
   140  		Bucket: &s.bucket,
   141  		Key:    &name,
   142  		Body:   source,
   143  	})
   144  	if err != nil {
   145  		logrus.Errorf("cannot write %s/%s: %v", s.String(), name, err)
   146  	}
   147  	return err
   148  }
   149  
   150  func (s *S3) ReadDir(prefix string, opts ListOption) ([]fs.FileInfo, error) {
   151  	if prefix != "" && opts&IsPrefix == 0 {
   152  		prefix += "/"
   153  	}
   154  
   155  	input := &s3.ListObjectsInput{
   156  		Bucket:    aws.String(s.bucket),
   157  		Prefix:    aws.String(prefix),
   158  		Delimiter: aws.String("/"),
   159  	}
   160  
   161  	result, err := s.svc.ListObjects(input)
   162  	if err != nil {
   163  		logrus.Errorf("cannot list %s/%s: %v", s.String(), prefix, err)
   164  		return nil, err
   165  	}
   166  
   167  	var infos []fs.FileInfo
   168  	for _, item := range result.Contents {
   169  		cut := strings.LastIndex(prefix, "/")
   170  		name := (*item.Key)[cut+1:]
   171  
   172  		infos = append(infos, simpleFileInfo{
   173  			name:    name,
   174  			size:    *item.Size,
   175  			isDir:   false,
   176  			modTime: *item.LastModified,
   177  		})
   178  
   179  	}
   180  
   181  	return infos, nil
   182  }
   183  
   184  func (s *S3) Stat(name string) (fs.FileInfo, error) {
   185  	head, err := s.svc.HeadObject(&s3.HeadObjectInput{
   186  		Bucket: &s.bucket,
   187  		Key:    &name,
   188  	})
   189  	if err != nil {
   190  		if aerr, ok := err.(awserr.Error); ok {
   191  			switch aerr.Code() {
   192  			case "NotFound": // s3.ErrCodeNoSuchKey does not work, aws is missing this error code so we hardwire a string
   193  				return nil, fs.ErrNotExist
   194  			default:
   195  				return nil, fs.ErrInvalid
   196  			}
   197  		}
   198  		return nil, err
   199  	}
   200  
   201  	return simpleFileInfo{
   202  		name:    path.Base(name),
   203  		size:    *head.ContentLength,
   204  		isDir:   strings.HasSuffix(name, "/"),
   205  		modTime: *head.LastModified,
   206  	}, nil
   207  }
   208  
   209  func (s *S3) Rename(old, new string) error {
   210  	_, err := s.svc.CopyObject(&s3.CopyObjectInput{
   211  		Bucket:     &s.bucket,
   212  		CopySource: aws.String(url.QueryEscape(old)),
   213  		Key:        aws.String(new),
   214  	})
   215  	return err
   216  }
   217  
   218  func (s *S3) Delete(name string) error {
   219  	input := &s3.ListObjectsInput{
   220  		Bucket:    aws.String(s.bucket),
   221  		Prefix:    aws.String(name + "/"),
   222  		Delimiter: aws.String("/"),
   223  	}
   224  
   225  	result, err := s.svc.ListObjects(input)
   226  	if err == nil && len(result.Contents) > 0 {
   227  		for _, item := range result.Contents {
   228  			_, err = s.svc.DeleteObject(&s3.DeleteObjectInput{
   229  				Bucket: &s.bucket,
   230  				Key:    item.Key,
   231  			})
   232  			if core.IsErr(err, "cannot delete %s: %v", item.Key) {
   233  				return err
   234  			}
   235  		}
   236  	} else {
   237  		_, err = s.svc.DeleteObject(&s3.DeleteObjectInput{
   238  			Bucket: &s.bucket,
   239  			Key:    &name,
   240  		})
   241  	}
   242  
   243  	core.IsErr(err, "cannot delete %s: %v", name)
   244  	return err
   245  }
   246  
   247  func (s *S3) Close() error {
   248  	return nil
   249  }
   250  
   251  func (s *S3) String() string {
   252  	return s.url
   253  }