forked from thomasdesr/external-mirror-cache
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paths3_cache.go
More file actions
132 lines (105 loc) · 3.77 KB
/
Copy paths3_cache.go
File metadata and controls
132 lines (105 loc) · 3.77 KB
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
130
131
132
package main
import (
"context"
"errors"
"io"
"net/http"
"net/url"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go"
"github.com/thomasdesr/external-mirror-cache/internal/errorutil"
"github.com/thomasdesr/external-mirror-cache/internal/reqlog"
)
// s3Uploader is the subset of transfermanager.Client that s3HTTPCache needs.
type s3Uploader interface {
UploadObject(
ctx context.Context,
input *transfermanager.UploadObjectInput,
opts ...func(*transfermanager.Options),
) (*transfermanager.UploadObjectOutput, error)
}
type s3HTTPCache struct {
s3c *s3.Client
s3pc *s3.PresignClient
s3u s3Uploader
bucket string
prefix string
}
// Head checks to see if the provided key has been cached in S3 and if so
// returns its original request's HTTP headers.
func (c *s3HTTPCache) Head(ctx context.Context, key CacheKey) (http.Header, error) {
s3Path := c.s3PathFor(key)
logger := reqlog.FromContext(ctx)
resp, err := c.s3c.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(c.bucket),
Key: aws.String(s3Path),
})
if err != nil {
// NotFound (404) is the normal cache miss. Forbidden (403) happens when
// the IAM role lacks s3:ListBucket — S3 returns 403 instead of 404 for
// non-existent keys. Both mean "not in cache."
if ae, ok := errors.AsType[smithy.APIError](err); ok && (ae.ErrorCode() == "NotFound" || ae.ErrorCode() == "Forbidden") {
logger.Debug("cache miss", "bucket", c.bucket, "key", s3Path)
return nil, nil //nolint:nilnil // nil,nil is the cache interface's "not found" contract
}
return nil, errorutil.Wrapf(err, "HeadObject(%s, %s)", c.bucket, s3Path)
}
logger.Debug("cache hit", "bucket", c.bucket, "key", s3Path)
return metadataToHeader(resp.Metadata)
}
// GetPresignedURL returns a presigned S3 URL for the provided key. This does
// not check if said URL exists.
func (c *s3HTTPCache) GetPresignedURL(ctx context.Context, key CacheKey) (string, error) {
objectPath := c.s3PathFor(key)
logger := reqlog.FromContext(ctx)
presignedResponse, err := c.s3pc.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(c.bucket),
Key: aws.String(objectPath),
})
if err != nil {
return "", errorutil.Wrapf(err, "PresignGetObject(%s, %s)", c.bucket, objectPath)
}
logger.Debug("presigned URL generated", "bucket", c.bucket, "key", objectPath)
return presignedResponse.URL, nil
}
// Put uploads the provided body to the appropriate path in S3 based on the
// provided key and attaches its headers as S3 Object metadata.
func (c *s3HTTPCache) Put(ctx context.Context, key CacheKey, headers http.Header, body io.Reader) error {
objectPath := c.s3PathFor(key)
logger := reqlog.FromContext(ctx)
metadata, err := headerToMetadata(headers)
if err != nil {
return errorutil.Wrapf(err, "headerToMetadata(%v)", headers)
}
logger.Debug("uploading to cache", "bucket", c.bucket, "key", objectPath)
var contentType *string
if ct := headers.Get("Content-Type"); ct != "" {
contentType = aws.String(ct)
}
_, err = c.s3u.UploadObject(ctx, &transfermanager.UploadObjectInput{
Bucket: aws.String(c.bucket),
Key: aws.String(objectPath),
Body: body,
Metadata: metadata,
ContentType: contentType,
})
if err != nil {
return errorutil.Wrapf(err, "UploadObject(%s, %s)", c.bucket, objectPath)
}
logger.Debug("upload complete", "bucket", c.bucket, "key", objectPath)
return nil
}
func (c *s3HTTPCache) s3PathFor(key CacheKey) string {
u := key.URL
path := strings.Join([]string{c.prefix, u.Host, strings.TrimPrefix(u.Path, "/")}, "/")
if u.RawQuery != "" {
path += "?" + url.QueryEscape(u.RawQuery)
}
if key.Variant != "" {
path += "//" + url.PathEscape(key.Variant)
}
return path
}