-
-
Notifications
You must be signed in to change notification settings - Fork 127
/
s3storage.go
129 lines (118 loc) · 3.01 KB
/
s3storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package s3storage
import (
"bytes"
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/cshum/imagor"
"github.com/cshum/imagor/imagorpath"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
)
type S3Storage struct {
S3 *s3.S3
Uploader *s3manager.Uploader
Bucket string
BaseDir string
PathPrefix string
ACL string
SafeChars string
safeChars map[byte]bool
}
func New(sess *session.Session, bucket string, options ...Option) *S3Storage {
baseDir := "/"
if idx := strings.Index(bucket, "/"); idx > -1 {
baseDir = bucket[idx:]
bucket = bucket[:idx]
}
s := &S3Storage{
S3: s3.New(sess),
Uploader: s3manager.NewUploader(sess),
Bucket: bucket,
BaseDir: baseDir,
PathPrefix: "/",
ACL: s3.ObjectCannedACLPublicRead,
safeChars: map[byte]bool{},
}
for _, option := range options {
option(s)
}
for _, c := range s.SafeChars {
s.safeChars[byte(c)] = true
}
return s
}
func (s *S3Storage) shouldEscape(c byte) bool {
// alphanum
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
return false
}
switch c {
case '/': // should not escape path segment
return false
case '-', '_', '.', '~': // Unreserved characters
return false
case '!', '\'', '(', ')', '*':
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines-safe-characters
return false
}
if len(s.safeChars) > 0 && s.safeChars[c] {
// safe chars from config
return false
}
// Everything else must be escaped.
return true
}
func (s *S3Storage) Path(image string) (string, bool) {
image = "/" + imagorpath.Normalize(image, s.shouldEscape)
if !strings.HasPrefix(image, s.PathPrefix) {
return "", false
}
return filepath.Join(s.BaseDir, strings.TrimPrefix(image, s.PathPrefix)), true
}
func (s *S3Storage) Load(r *http.Request, image string) (*imagor.Blob, error) {
image, ok := s.Path(image)
if !ok {
return nil, imagor.ErrPass
}
input := &s3.GetObjectInput{
Bucket: aws.String(s.Bucket),
Key: aws.String(image),
}
out, err := s.S3.GetObjectWithContext(r.Context(), input)
if e, ok := err.(awserr.Error); ok && e.Code() == s3.ErrCodeNoSuchKey {
return nil, imagor.ErrNotFound
} else if err != nil {
return nil, err
}
buf, err := io.ReadAll(out.Body)
if err != nil {
return nil, err
}
return imagor.NewBlobBytes(buf), err
}
func (s *S3Storage) Save(ctx context.Context, image string, blob *imagor.Blob) error {
image, ok := s.Path(image)
if !ok {
return imagor.ErrPass
}
buf, err := blob.ReadAll()
if err != nil {
return err
}
input := &s3manager.UploadInput{
ACL: aws.String(s.ACL),
Body: bytes.NewReader(buf),
Bucket: aws.String(s.Bucket),
ContentType: aws.String(mime.TypeByExtension(filepath.Ext(image))),
Key: aws.String(image),
}
_, err = s.Uploader.UploadWithContext(ctx, input)
return err
}