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 }