From a36bc723cc3386f5695cf3661c1ef3363c223182 Mon Sep 17 00:00:00 2001 From: codegirl-007 Date: Fri, 21 Aug 2026 23:48:18 -0700 Subject: [PATCH] Move Spaces FromEnv into blob; Upload takes Object; drop logSpaces. --- cmd/server/main.go | 23 +---------------------- internal/blob/spaces.go | 37 +++++++++++++++++++++++++++++-------- internal/web/profile.go | 16 +++++++++++----- internal/web/server_test.go | 8 ++++---- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index a41407e..b09be88 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -25,9 +25,7 @@ func main() { st := openStore() defer st.Close() - uploader := spacesUploader() - logSpaces(uploader) - + uploader := blob.FromEnv() handler := newHandler(st, uploader) run(&http.Server{Addr: listenAddr(), Handler: handler}) } @@ -45,14 +43,6 @@ func openStore() *store.Store { 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 { srv, err := web.New(st, st.SessionStore(), plumber.TemplateFS, plumber.StaticFS, web.Config{ 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. func listenAddr() string { if p := strings.TrimSpace(os.Getenv("PORT")); p != "" { diff --git a/internal/blob/spaces.go b/internal/blob/spaces.go index 4e0d8cd..979e7d9 100644 --- a/internal/blob/spaces.go +++ b/internal/blob/spaces.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "os" "strings" "github.com/aws/aws-sdk-go-v2/aws" @@ -15,7 +16,15 @@ import ( // Uploader stores public avatar objects. type Uploader interface { 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. @@ -38,10 +47,22 @@ type spaces struct { 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") } +// 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. func NewSpaces(cfg SpacesConfig) Uploader { 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) Upload(ctx context.Context, key string, body io.Reader, contentType string, size int64) (string, error) { - key = strings.TrimPrefix(key, "/") +func (s *spaces) Upload(ctx context.Context, obj Object) (string, error) { + key := strings.TrimPrefix(obj.Key, "/") input := &s3.PutObjectInput{ Bucket: aws.String(s.cfg.Bucket), Key: aws.String(key), - Body: body, - ContentType: aws.String(contentType), + Body: obj.Body, + ContentType: aws.String(obj.ContentType), ACL: types.ObjectCannedACLPublicRead, } - if size > 0 { - input.ContentLength = aws.Int64(size) + if obj.Size > 0 { + input.ContentLength = aws.Int64(obj.Size) } if _, err := s.client.PutObject(ctx, input); err != nil { return "", err diff --git a/internal/web/profile.go b/internal/web/profile.go index 1c214ec..c603885 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -8,18 +8,19 @@ import ( "github.com/google/uuid" + "plumber/internal/blob" "plumber/internal/geo" "plumber/internal/store" ) type profilePage struct { page - States []struct{ Code, Name string } - Questions []store.RankedQuestion + States []struct{ Code, Name string } + Questions []store.RankedQuestion QuestionsLabel string UploadsEnabled bool - Error string - StateVal string + Error string + StateVal string } 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) 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 { s.renderProfile(w, r, u, "Could not upload avatar. Try again later.", state) return diff --git a/internal/web/server_test.go b/internal/web/server_test.go index aa6f849..57a1aed 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -3,7 +3,6 @@ package web import ( "bytes" "context" - "io" "mime/multipart" "net/http" "net/http/httptest" @@ -13,6 +12,7 @@ import ( "github.com/alexedwards/scs/v2" "plumber" + "plumber/internal/blob" ) 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) 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.last = key - return "https://cdn.example.com/" + key, nil + f.last = obj.Key + return "https://cdn.example.com/" + obj.Key, nil } func TestProfilePageAndState(t *testing.T) {