Move Spaces FromEnv into blob; Upload takes Object; drop logSpaces.
This commit is contained in:
+1
-22
@@ -25,9 +25,7 @@ func main() {
|
|||||||
st := openStore()
|
st := openStore()
|
||||||
defer st.Close()
|
defer st.Close()
|
||||||
|
|
||||||
uploader := spacesUploader()
|
uploader := blob.FromEnv()
|
||||||
logSpaces(uploader)
|
|
||||||
|
|
||||||
handler := newHandler(st, uploader)
|
handler := newHandler(st, uploader)
|
||||||
run(&http.Server{Addr: listenAddr(), Handler: handler})
|
run(&http.Server{Addr: listenAddr(), Handler: handler})
|
||||||
}
|
}
|
||||||
@@ -45,14 +43,6 @@ func openStore() *store.Store {
|
|||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
func logSpaces(uploader blob.Uploader) {
|
|
||||||
if uploader.Enabled() {
|
|
||||||
log.Printf("avatars: digitalocean spaces")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("avatars: uploads disabled (set SPACES_* to enable)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHandler(st *store.Store, uploader blob.Uploader) http.Handler {
|
func newHandler(st *store.Store, uploader blob.Uploader) http.Handler {
|
||||||
srv, err := web.New(st, st.SessionStore(), plumber.TemplateFS, plumber.StaticFS, web.Config{
|
srv, err := web.New(st, st.SessionStore(), plumber.TemplateFS, plumber.StaticFS, web.Config{
|
||||||
AdminUsername: os.Getenv("ADMIN_USERNAME"),
|
AdminUsername: os.Getenv("ADMIN_USERNAME"),
|
||||||
@@ -93,17 +83,6 @@ func run(httpSrv *http.Server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func spacesUploader() blob.Uploader {
|
|
||||||
return blob.NewSpaces(blob.SpacesConfig{
|
|
||||||
Key: os.Getenv("SPACES_KEY"),
|
|
||||||
Secret: os.Getenv("SPACES_SECRET"),
|
|
||||||
Region: os.Getenv("SPACES_REGION"),
|
|
||||||
Bucket: os.Getenv("SPACES_BUCKET"),
|
|
||||||
Endpoint: os.Getenv("SPACES_ENDPOINT"),
|
|
||||||
CDNBase: os.Getenv("SPACES_CDN_BASE"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// listenAddr prefers PORT (App Platform / PaaS), then LISTEN, then :8080.
|
// listenAddr prefers PORT (App Platform / PaaS), then LISTEN, then :8080.
|
||||||
func listenAddr() string {
|
func listenAddr() string {
|
||||||
if p := strings.TrimSpace(os.Getenv("PORT")); p != "" {
|
if p := strings.TrimSpace(os.Getenv("PORT")); p != "" {
|
||||||
|
|||||||
+29
-8
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
@@ -15,7 +16,15 @@ import (
|
|||||||
// Uploader stores public avatar objects.
|
// Uploader stores public avatar objects.
|
||||||
type Uploader interface {
|
type Uploader interface {
|
||||||
Enabled() bool
|
Enabled() bool
|
||||||
Upload(ctx context.Context, key string, body io.Reader, contentType string, size int64) (publicURL string, err error)
|
Upload(ctx context.Context, obj Object) (publicURL string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object is a file to upload to object storage.
|
||||||
|
type Object struct {
|
||||||
|
Key string
|
||||||
|
Body io.Reader
|
||||||
|
ContentType string
|
||||||
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disabled is a no-op uploader used when Spaces is not configured.
|
// Disabled is a no-op uploader used when Spaces is not configured.
|
||||||
@@ -38,10 +47,22 @@ type spaces struct {
|
|||||||
|
|
||||||
func (Disabled) Enabled() bool { return false }
|
func (Disabled) Enabled() bool { return false }
|
||||||
|
|
||||||
func (Disabled) Upload(context.Context, string, io.Reader, string, int64) (string, error) {
|
func (Disabled) Upload(context.Context, Object) (string, error) {
|
||||||
return "", fmt.Errorf("avatar uploads are not configured")
|
return "", fmt.Errorf("avatar uploads are not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromEnv builds an Uploader from SPACES_* environment variables.
|
||||||
|
func FromEnv() Uploader {
|
||||||
|
return NewSpaces(SpacesConfig{
|
||||||
|
Key: os.Getenv("SPACES_KEY"),
|
||||||
|
Secret: os.Getenv("SPACES_SECRET"),
|
||||||
|
Region: os.Getenv("SPACES_REGION"),
|
||||||
|
Bucket: os.Getenv("SPACES_BUCKET"),
|
||||||
|
Endpoint: os.Getenv("SPACES_ENDPOINT"),
|
||||||
|
CDNBase: os.Getenv("SPACES_CDN_BASE"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// NewSpaces returns an Uploader when required env is present; otherwise Disabled.
|
// NewSpaces returns an Uploader when required env is present; otherwise Disabled.
|
||||||
func NewSpaces(cfg SpacesConfig) Uploader {
|
func NewSpaces(cfg SpacesConfig) Uploader {
|
||||||
cfg.Key = strings.TrimSpace(cfg.Key)
|
cfg.Key = strings.TrimSpace(cfg.Key)
|
||||||
@@ -63,17 +84,17 @@ func NewSpaces(cfg SpacesConfig) Uploader {
|
|||||||
|
|
||||||
func (s *spaces) Enabled() bool { return true }
|
func (s *spaces) Enabled() bool { return true }
|
||||||
|
|
||||||
func (s *spaces) Upload(ctx context.Context, key string, body io.Reader, contentType string, size int64) (string, error) {
|
func (s *spaces) Upload(ctx context.Context, obj Object) (string, error) {
|
||||||
key = strings.TrimPrefix(key, "/")
|
key := strings.TrimPrefix(obj.Key, "/")
|
||||||
input := &s3.PutObjectInput{
|
input := &s3.PutObjectInput{
|
||||||
Bucket: aws.String(s.cfg.Bucket),
|
Bucket: aws.String(s.cfg.Bucket),
|
||||||
Key: aws.String(key),
|
Key: aws.String(key),
|
||||||
Body: body,
|
Body: obj.Body,
|
||||||
ContentType: aws.String(contentType),
|
ContentType: aws.String(obj.ContentType),
|
||||||
ACL: types.ObjectCannedACLPublicRead,
|
ACL: types.ObjectCannedACLPublicRead,
|
||||||
}
|
}
|
||||||
if size > 0 {
|
if obj.Size > 0 {
|
||||||
input.ContentLength = aws.Int64(size)
|
input.ContentLength = aws.Int64(obj.Size)
|
||||||
}
|
}
|
||||||
if _, err := s.client.PutObject(ctx, input); err != nil {
|
if _, err := s.client.PutObject(ctx, input); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
+11
-5
@@ -8,18 +8,19 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"plumber/internal/blob"
|
||||||
"plumber/internal/geo"
|
"plumber/internal/geo"
|
||||||
"plumber/internal/store"
|
"plumber/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type profilePage struct {
|
type profilePage struct {
|
||||||
page
|
page
|
||||||
States []struct{ Code, Name string }
|
States []struct{ Code, Name string }
|
||||||
Questions []store.RankedQuestion
|
Questions []store.RankedQuestion
|
||||||
QuestionsLabel string
|
QuestionsLabel string
|
||||||
UploadsEnabled bool
|
UploadsEnabled bool
|
||||||
Error string
|
Error string
|
||||||
StateVal string
|
StateVal string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleProfileForm(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleProfileForm(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -74,7 +75,12 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
key := path.Join("avatars", u.ID, uuid.NewString()+ext)
|
key := path.Join("avatars", u.ID, uuid.NewString()+ext)
|
||||||
limited := io.LimitReader(file, (2<<20)+1)
|
limited := io.LimitReader(file, (2<<20)+1)
|
||||||
url, upErr := s.cfg.Blob.Upload(r.Context(), key, limited, contentType, hdr.Size)
|
url, upErr := s.cfg.Blob.Upload(r.Context(), blob.Object{
|
||||||
|
Key: key,
|
||||||
|
Body: limited,
|
||||||
|
ContentType: contentType,
|
||||||
|
Size: hdr.Size,
|
||||||
|
})
|
||||||
if upErr != nil {
|
if upErr != nil {
|
||||||
s.renderProfile(w, r, u, "Could not upload avatar. Try again later.", state)
|
s.renderProfile(w, r, u, "Could not upload avatar. Try again later.", state)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package web
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
"github.com/alexedwards/scs/v2"
|
"github.com/alexedwards/scs/v2"
|
||||||
|
|
||||||
"plumber"
|
"plumber"
|
||||||
|
"plumber/internal/blob"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestServer(t *testing.T) (*Server, *memDB, scs.Store) {
|
func newTestServer(t *testing.T) (*Server, *memDB, scs.Store) {
|
||||||
@@ -296,10 +296,10 @@ type fakeBlob struct {
|
|||||||
|
|
||||||
func (f *fakeBlob) Enabled() bool { return true }
|
func (f *fakeBlob) Enabled() bool { return true }
|
||||||
|
|
||||||
func (f *fakeBlob) Upload(_ context.Context, key string, _ io.Reader, _ string, _ int64) (string, error) {
|
func (f *fakeBlob) Upload(_ context.Context, obj blob.Object) (string, error) {
|
||||||
f.calls++
|
f.calls++
|
||||||
f.last = key
|
f.last = obj.Key
|
||||||
return "https://cdn.example.com/" + key, nil
|
return "https://cdn.example.com/" + obj.Key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProfilePageAndState(t *testing.T) {
|
func TestProfilePageAndState(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user