-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathupload.go
More file actions
121 lines (107 loc) · 3.09 KB
/
Copy pathupload.go
File metadata and controls
121 lines (107 loc) · 3.09 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
package main
import (
"context"
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type inputError struct {
msg string
}
func (e inputError) Error() string {
return e.msg
}
type uploadURLParams struct {
Bucket string
Key string
Filename string
ContentType string
ExpiresIn string
}
type uploadURLResponse struct {
Message string `json:"message"`
UploadURL string `json:"uploadUrl"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
Bucket string `json:"bucket"`
Key string `json:"key"`
ContentType string `json:"contentType"`
SingleUse bool `json:"singleUse"`
ExpiresIn int64 `json:"expiresIn"`
ExpiresAt string `json:"expiresAt"`
Location string `json:"location"`
}
func generateUploadURL(ctx context.Context, params uploadURLParams) (*uploadURLResponse, error) {
bucket := strings.TrimSpace(params.Bucket)
if bucket == "" {
return nil, inputError{msg: "missing required field: bucket"}
}
key := strings.TrimSpace(params.Key)
if key == "" {
filename := strings.TrimSpace(params.Filename)
if filename != "" {
key = filepath.Base(filename)
} else {
key = fmt.Sprintf("uploads/%d", time.Now().UnixNano())
}
}
contentType := strings.TrimSpace(params.ContentType)
if contentType == "" {
contentType = "application/octet-stream"
}
expiresInSec, err := parseExpiresIn(params.ExpiresIn)
if err != nil {
return nil, err
}
presignedReq, err := presignClient.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ContentType: aws.String(contentType),
}, func(opts *s3.PresignOptions) {
opts.Expires = time.Duration(expiresInSec) * time.Second
})
if err != nil {
return nil, fmt.Errorf("failed to generate upload URL: %w", err)
}
headers := map[string]string{}
for k, vals := range presignedReq.SignedHeader {
if strings.EqualFold(k, "host") {
continue
}
if len(vals) > 0 {
headers[k] = vals[0]
}
}
expiresAt := time.Now().Add(time.Duration(expiresInSec) * time.Second).UTC().Format(time.RFC3339)
return &uploadURLResponse{
Message: "presigned upload URL generated",
UploadURL: presignedReq.URL,
Method: presignedReq.Method,
Headers: headers,
Bucket: bucket,
Key: key,
ContentType: contentType,
SingleUse: true,
ExpiresIn: expiresInSec,
ExpiresAt: expiresAt,
Location: fmt.Sprintf("s3://%s/%s", bucket, key),
}, nil
}
func parseExpiresIn(raw string) (int64, error) {
expiresInSec := int64(120)
if strings.TrimSpace(raw) != "" {
parsed, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil {
return 0, inputError{msg: "invalid expiresIn: must be an integer number of seconds"}
}
expiresInSec = parsed
}
if expiresInSec < 60 || expiresInSec > 3600 {
return 0, inputError{msg: "expiresIn must be between 60 and 3600 seconds"}
}
return expiresInSec, nil
}