diff --git a/db/storage.go b/db/storage.go index b216829..3e7abc2 100644 --- a/db/storage.go +++ b/db/storage.go @@ -1,6 +1,7 @@ package db import ( + "context" "log" "os" "path/filepath" @@ -13,11 +14,16 @@ import ( "github.com/glebarez/sqlite" "gorm.io/gorm" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" ) type Storage struct { Db *gorm.DB Authorization *ttlcache.Cache[string, *indieauth.AuthenticationRequest] + Media *s3.Client } type CollectionName string @@ -41,6 +47,17 @@ func NewStorage() *Storage { panic(err) } + sdkConfig, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + log.Printf("Couldn't load default configuration. Here's why: %v\n", err) + panic(err) + } + + svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) { + o.BaseEndpoint = aws.String("https://" + os.Getenv("AWS_S3_ENDPOINT")) + o.Region = os.Getenv("AWS_REGION") + }) + cache := ttlcache.New[string, *indieauth.AuthenticationRequest]( ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), ) @@ -48,9 +65,9 @@ func NewStorage() *Storage { go cache.Start() store := &Storage{ - // Docs: c, Db: db, Authorization: cache, + Media: svc, } return store diff --git a/go.mod b/go.mod index 0178883..423a273 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.22.5 require ( codeberg.org/gruf/go-ulid v1.1.0 github.com/aidarkhanov/nanoid v1.0.8 + github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/glebarez/sqlite v1.11.0 + github.com/go-chi/cors v1.2.1 + github.com/h2non/filetype v1.1.3 github.com/jellydator/ttlcache/v3 v3.2.0 github.com/joho/godotenv v1.5.1 go.hacdias.com/indielib v0.3.1 @@ -15,10 +18,24 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect + github.com/aws/smithy-go v1.20.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/go-chi/cors v1.2.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -37,6 +54,8 @@ require ( require ( github.com/a-h/templ v0.2.747 + github.com/aws/aws-sdk-go-v2/config v1.27.27 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/httprate v0.12.0 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index c7216e7..045a1ac 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,42 @@ github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2y github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA= github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= +github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= +github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -39,6 +75,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= diff --git a/main.go b/main.go index fd51cbd..bc5005f 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,18 @@ package main import ( "encoding/json" + "fmt" + "io" "os" + "strings" "time" "log" "net/http" "strconv" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/puregarlic/space/db" "github.com/puregarlic/space/models" "github.com/puregarlic/space/pages" @@ -76,6 +81,7 @@ func main() { // Pages r.Get("/", s.serveHomeTemplate) r.Get("/posts/{slug}", s.servePostTemplate) + r.Get("/media/*", s.serveMedia) // IndieAuth handlers r.Group(func(r chi.Router) { @@ -96,8 +102,31 @@ func main() { }, )) r.Use(s.mustAuth) - r.Get("/", s.serveMicropub) - r.Post("/", s.serveMicropub) + + mp := µpubImplementation{s} + mpHandler := micropub.NewHandler( + mp, + micropub.WithGetPostTypes(func() []micropub.PostType { + return []micropub.PostType{ + { + Name: "Post", + Type: string(microformats.TypeNote), + }, + { + Name: "Photo", + Type: string(microformats.TypePhoto), + }, + } + }), + micropub.WithMediaEndpoint(s.profileURL+"/micropub/media"), + ) + + r.Get("/", mpHandler.ServeHTTP) + r.Post("/", mpHandler.ServeHTTP) + r.Post("/media", micropub.NewMediaHandler( + mp.HandleMediaUpload, + mp.HasScope, + ).ServeHTTP) }) // Start it! @@ -138,18 +167,28 @@ func (s *server) servePostTemplate(w http.ResponseWriter, r *http.Request) { templ.Handler(pages.Post(post)).ServeHTTP(w, r) } -func (s *server) serveMicropub(w http.ResponseWriter, r *http.Request) { - micropub.NewHandler( - µpubImplementation{s}, - micropub.WithGetPostTypes(func() []micropub.PostType { - return []micropub.PostType{ - { - Name: "Post", - Type: string(microformats.TypeNote), - }, - } - }), - ).ServeHTTP(w, r) +func (s *server) serveMedia(w http.ResponseWriter, r *http.Request) { + key := strings.TrimPrefix(r.URL.Path, "/") + + res, err := s.db.Media.GetObject(r.Context(), &s3.GetObjectInput{ + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), + Key: &key, + }) + if err != nil { + fmt.Println("failed to get object", err) + httpError(w, http.StatusInternalServerError) + return + } + + defer res.Body.Close() + + w.Header().Set("Cache-Control", "604800") + + if _, err := io.Copy(w, res.Body); err != nil { + fmt.Println("failed to send object", err) + httpError(w, http.StatusInternalServerError) + return + } } func httpError(w http.ResponseWriter, status int) { diff --git a/micropub.go b/micropub.go index 20a88bb..b3a2dc6 100644 --- a/micropub.go +++ b/micropub.go @@ -1,15 +1,23 @@ package main import ( + "context" "encoding/json" "errors" "fmt" + "mime/multipart" "net/http" urlpkg "net/url" + "os" "reflect" "strings" "github.com/puregarlic/space/models" + + "github.com/aidarkhanov/nanoid" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/h2non/filetype" "github.com/samber/lo" "go.hacdias.com/indielib/micropub" @@ -62,6 +70,27 @@ func (s *micropubImplementation) SourceMany(limit, offset int) ([]map[string]any return nil, micropub.ErrNotImplemented } +func (s *micropubImplementation) HandleMediaUpload(file multipart.File, header *multipart.FileHeader) (string, error) { + defer file.Close() + + kind, err := filetype.MatchReader(file) + if err != nil { + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) + } + + key := fmt.Sprintf("media/%s.%s", nanoid.New(), kind.Extension) + _, err = s.db.Media.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), + Key: &key, + Body: file, + }) + if err != nil { + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) + } + + return s.profileURL + key, nil +} + func (s *micropubImplementation) Create(req *micropub.Request) (string, error) { props, err := json.Marshal(req.Properties) if err != nil {