git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/storage/s3/s3.go (about) 1 package s3 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "net/http" 8 "path/filepath" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/credentials" 12 "github.com/aws/aws-sdk-go/aws/endpoints" 13 "github.com/aws/aws-sdk-go/aws/session" 14 "github.com/aws/aws-sdk-go/service/s3" 15 ) 16 17 type S3Storage struct { 18 basePath string 19 s3Client *s3.S3 20 bucket string 21 } 22 23 type Config struct { 24 AccessKeyID string 25 SecretAccessKey string 26 Endpoint string 27 Region string 28 BaseDirectory string 29 Bucket string 30 HttpClient *http.Client 31 } 32 33 func NewS3Storage(config Config) (*S3Storage, error) { 34 var endpointResolver endpoints.Resolver 35 36 if config.Endpoint != "" { 37 // see https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/endpoints/ 38 // https://stackoverflow.com/questions/67575681/is-aws-go-sdk-v2-integrated-with-local-minio-server 39 // https://stackoverflow.com/questions/71088064/how-can-i-use-the-aws-sdk-v2-for-go-with-digitalocean-spaces 40 endpointResolver = endpoints.ResolverFunc(func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { 41 return endpoints.ResolvedEndpoint{ 42 SigningRegion: config.Region, 43 URL: config.Endpoint, 44 }, nil 45 }) 46 } 47 48 awsSession, err := session.NewSession(&aws.Config{ 49 Endpoint: aws.String(config.Endpoint), 50 Region: aws.String(config.Region), 51 Credentials: credentials.NewStaticCredentials(config.AccessKeyID, config.SecretAccessKey, ""), 52 HTTPClient: config.HttpClient, 53 EndpointResolver: endpointResolver, 54 }) 55 if err != nil { 56 return nil, err 57 } 58 59 // Create S3 service client 60 s3Client := s3.New(awsSession) 61 62 return &S3Storage{ 63 basePath: config.BaseDirectory, 64 s3Client: s3Client, 65 bucket: config.Bucket, 66 }, nil 67 } 68 69 func (storage *S3Storage) BasePath() string { 70 return storage.basePath 71 } 72 73 func (storage *S3Storage) CopyObject(ctx context.Context, from string, to string) error { 74 from = filepath.Join(storage.bucket, storage.basePath, from) 75 to = filepath.Join(storage.basePath, from) 76 77 _, err := storage.s3Client.CopyObject(&s3.CopyObjectInput{ 78 Bucket: aws.String(storage.bucket), 79 Key: aws.String(to), 80 CopySource: aws.String(from), 81 }) 82 if err != nil { 83 return err 84 } 85 86 return nil 87 } 88 89 func (storage *S3Storage) DeleteObject(ctx context.Context, key string) error { 90 objectKey := filepath.Join(storage.basePath, key) 91 92 _, err := storage.s3Client.DeleteObject(&s3.DeleteObjectInput{ 93 Bucket: aws.String(storage.bucket), 94 Key: aws.String(objectKey), 95 }) 96 if err != nil { 97 return err 98 } 99 100 return nil 101 } 102 103 func (storage *S3Storage) GetObject(ctx context.Context, key string) (io.ReadCloser, error) { 104 objectKey := filepath.Join(storage.basePath, key) 105 106 result, err := storage.s3Client.GetObject(&s3.GetObjectInput{ 107 Bucket: aws.String(storage.bucket), 108 Key: aws.String(objectKey), 109 }) 110 if err != nil { 111 return nil, err 112 } 113 114 return result.Body, nil 115 } 116 117 func (storage *S3Storage) GetObjectSize(ctx context.Context, key string) (int64, error) { 118 objectKey := filepath.Join(storage.basePath, key) 119 120 result, err := storage.s3Client.HeadObject(&s3.HeadObjectInput{ 121 Bucket: aws.String(storage.bucket), 122 Key: aws.String(objectKey), 123 }) 124 if err != nil { 125 return 0, err 126 } 127 128 if result.ContentLength == nil { 129 return 0, errors.New("s3: object size is null") 130 } 131 132 return *result.ContentLength, nil 133 } 134 135 // func (storage *S3Storage) GetPresignedUploadUrl(ctx context.Context, key string, size uint64) (string, error) { 136 // objectKey := filepath.Join(storage.basePath, key) 137 138 // req, _ := storage.s3Client.PutObjectRequest(&s3.PutObjectInput{ 139 // Bucket: aws.String(storage.bucket), 140 // Key: aws.String(objectKey), 141 // ContentLength: aws.Int64(int64(size)), 142 // }) 143 144 // url, err := req.Presign(2 * time.Hour) 145 // if err != nil { 146 // return "", err 147 // } 148 149 // return url, nil 150 // } 151 152 // TODO?: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html 153 func (storage *S3Storage) PutObject(ctx context.Context, key string, contentType string, size int64, object io.Reader) error { 154 objectKey := filepath.Join(storage.basePath, key) 155 156 _, err := storage.s3Client.PutObject(&s3.PutObjectInput{ 157 Bucket: aws.String(storage.bucket), 158 Key: aws.String(objectKey), 159 Body: aws.ReadSeekCloser(object), 160 ContentType: aws.String(contentType), 161 ContentLength: aws.Int64(int64(size)), 162 }) 163 if err != nil { 164 return err 165 } 166 167 return nil 168 } 169 170 func (storage *S3Storage) DeleteObjectsWithPrefix(ctx context.Context, prefix string) (err error) { 171 s3Prefix := filepath.Join(storage.basePath, prefix) 172 var continuationToken *string 173 174 for { 175 var res *s3.ListObjectsV2Output 176 177 res, err = storage.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{ 178 Bucket: aws.String(storage.bucket), 179 Prefix: aws.String(s3Prefix), 180 ContinuationToken: continuationToken, 181 }) 182 183 for _, object := range res.Contents { 184 err = storage.DeleteObject(ctx, *object.Key) 185 if err != nil { 186 return 187 } 188 } 189 190 continuationToken = res.ContinuationToken 191 192 if continuationToken == nil { 193 break 194 } 195 } 196 197 return 198 }