An experimental IndieWeb site built in Go.

add initial media endpoint configurations

Untested because so many micropub clients are broken...

Changed files
+158 -16
db
+18 -1
db/storage.go
··· 1 1 package db 2 2 3 3 import ( 4 + "context" 4 5 "log" 5 6 "os" 6 7 "path/filepath" ··· 13 14 14 15 "github.com/glebarez/sqlite" 15 16 "gorm.io/gorm" 17 + 18 + "github.com/aws/aws-sdk-go-v2/aws" 19 + "github.com/aws/aws-sdk-go-v2/config" 20 + "github.com/aws/aws-sdk-go-v2/service/s3" 16 21 ) 17 22 18 23 type Storage struct { 19 24 Db *gorm.DB 20 25 Authorization *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 26 + Media *s3.Client 21 27 } 22 28 23 29 type CollectionName string ··· 41 47 panic(err) 42 48 } 43 49 50 + sdkConfig, err := config.LoadDefaultConfig(context.TODO()) 51 + if err != nil { 52 + log.Printf("Couldn't load default configuration. Here's why: %v\n", err) 53 + panic(err) 54 + } 55 + 56 + svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) { 57 + o.BaseEndpoint = aws.String("https://" + os.Getenv("AWS_S3_ENDPOINT")) 58 + o.Region = os.Getenv("AWS_REGION") 59 + }) 60 + 44 61 cache := ttlcache.New[string, *indieauth.AuthenticationRequest]( 45 62 ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 46 63 ) ··· 48 65 go cache.Start() 49 66 50 67 store := &Storage{ 51 - // Docs: c, 52 68 Db: db, 53 69 Authorization: cache, 70 + Media: svc, 54 71 } 55 72 56 73 return store
+20 -1
go.mod
··· 5 5 require ( 6 6 codeberg.org/gruf/go-ulid v1.1.0 7 7 github.com/aidarkhanov/nanoid v1.0.8 8 + github.com/aws/aws-sdk-go-v2 v1.30.3 8 9 github.com/glebarez/sqlite v1.11.0 10 + github.com/go-chi/cors v1.2.1 11 + github.com/h2non/filetype v1.1.3 9 12 github.com/jellydator/ttlcache/v3 v3.2.0 10 13 github.com/joho/godotenv v1.5.1 11 14 go.hacdias.com/indielib v0.3.1 ··· 15 18 16 19 require ( 17 20 filippo.io/edwards25519 v1.1.0 // indirect 21 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 22 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect 23 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 24 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect 25 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect 26 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 27 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect 28 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 29 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect 30 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 31 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect 32 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 33 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 34 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect 35 + github.com/aws/smithy-go v1.20.3 // indirect 18 36 github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 37 github.com/dustin/go-humanize v1.0.1 // indirect 20 38 github.com/glebarez/go-sqlite v1.21.2 // indirect 21 - github.com/go-chi/cors v1.2.1 // indirect 22 39 github.com/go-sql-driver/mysql v1.8.1 // indirect 23 40 github.com/google/uuid v1.3.0 // indirect 24 41 github.com/jinzhu/inflection v1.0.0 // indirect ··· 37 54 38 55 require ( 39 56 github.com/a-h/templ v0.2.747 57 + github.com/aws/aws-sdk-go-v2/config v1.27.27 58 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 40 59 github.com/go-chi/chi/v5 v5.1.0 41 60 github.com/go-chi/httprate v0.12.0 42 61 github.com/golang-jwt/jwt/v5 v5.2.1
+38
go.sum
··· 7 7 github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA= 8 8 github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= 9 9 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 10 + github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= 11 + github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= 12 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= 13 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= 14 + github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= 15 + github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= 16 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= 17 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= 18 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= 19 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= 20 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= 21 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= 22 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= 23 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= 24 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 25 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 26 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= 27 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= 28 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= 29 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= 30 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= 31 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= 32 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= 33 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= 34 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= 35 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= 36 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= 37 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= 38 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= 39 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= 40 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= 41 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= 42 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= 43 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= 44 + github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= 45 + github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 10 46 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 11 47 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 48 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= ··· 39 75 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 40 76 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 41 77 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 78 + github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 79 + github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 42 80 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 43 81 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 44 82 github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+53 -14
main.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "fmt" 6 + "io" 5 7 "os" 8 + "strings" 6 9 "time" 7 10 8 11 "log" 9 12 "net/http" 10 13 "strconv" 11 14 15 + "github.com/aws/aws-sdk-go-v2/aws" 16 + "github.com/aws/aws-sdk-go-v2/service/s3" 12 17 "github.com/puregarlic/space/db" 13 18 "github.com/puregarlic/space/models" 14 19 "github.com/puregarlic/space/pages" ··· 76 81 // Pages 77 82 r.Get("/", s.serveHomeTemplate) 78 83 r.Get("/posts/{slug}", s.servePostTemplate) 84 + r.Get("/media/*", s.serveMedia) 79 85 80 86 // IndieAuth handlers 81 87 r.Group(func(r chi.Router) { ··· 96 102 }, 97 103 )) 98 104 r.Use(s.mustAuth) 99 - r.Get("/", s.serveMicropub) 100 - r.Post("/", s.serveMicropub) 105 + 106 + mp := &micropubImplementation{s} 107 + mpHandler := micropub.NewHandler( 108 + mp, 109 + micropub.WithGetPostTypes(func() []micropub.PostType { 110 + return []micropub.PostType{ 111 + { 112 + Name: "Post", 113 + Type: string(microformats.TypeNote), 114 + }, 115 + { 116 + Name: "Photo", 117 + Type: string(microformats.TypePhoto), 118 + }, 119 + } 120 + }), 121 + micropub.WithMediaEndpoint(s.profileURL+"/micropub/media"), 122 + ) 123 + 124 + r.Get("/", mpHandler.ServeHTTP) 125 + r.Post("/", mpHandler.ServeHTTP) 126 + r.Post("/media", micropub.NewMediaHandler( 127 + mp.HandleMediaUpload, 128 + mp.HasScope, 129 + ).ServeHTTP) 101 130 }) 102 131 103 132 // Start it! ··· 138 167 templ.Handler(pages.Post(post)).ServeHTTP(w, r) 139 168 } 140 169 141 - func (s *server) serveMicropub(w http.ResponseWriter, r *http.Request) { 142 - micropub.NewHandler( 143 - &micropubImplementation{s}, 144 - micropub.WithGetPostTypes(func() []micropub.PostType { 145 - return []micropub.PostType{ 146 - { 147 - Name: "Post", 148 - Type: string(microformats.TypeNote), 149 - }, 150 - } 151 - }), 152 - ).ServeHTTP(w, r) 170 + func (s *server) serveMedia(w http.ResponseWriter, r *http.Request) { 171 + key := strings.TrimPrefix(r.URL.Path, "/") 172 + 173 + res, err := s.db.Media.GetObject(r.Context(), &s3.GetObjectInput{ 174 + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 175 + Key: &key, 176 + }) 177 + if err != nil { 178 + fmt.Println("failed to get object", err) 179 + httpError(w, http.StatusInternalServerError) 180 + return 181 + } 182 + 183 + defer res.Body.Close() 184 + 185 + w.Header().Set("Cache-Control", "604800") 186 + 187 + if _, err := io.Copy(w, res.Body); err != nil { 188 + fmt.Println("failed to send object", err) 189 + httpError(w, http.StatusInternalServerError) 190 + return 191 + } 153 192 } 154 193 155 194 func httpError(w http.ResponseWriter, status int) {
+29
micropub.go
··· 1 1 package main 2 2 3 3 import ( 4 + "context" 4 5 "encoding/json" 5 6 "errors" 6 7 "fmt" 8 + "mime/multipart" 7 9 "net/http" 8 10 urlpkg "net/url" 11 + "os" 9 12 "reflect" 10 13 "strings" 11 14 12 15 "github.com/puregarlic/space/models" 16 + 17 + "github.com/aidarkhanov/nanoid" 18 + "github.com/aws/aws-sdk-go-v2/aws" 19 + "github.com/aws/aws-sdk-go-v2/service/s3" 20 + "github.com/h2non/filetype" 13 21 "github.com/samber/lo" 14 22 15 23 "go.hacdias.com/indielib/micropub" ··· 60 68 61 69 func (s *micropubImplementation) SourceMany(limit, offset int) ([]map[string]any, error) { 62 70 return nil, micropub.ErrNotImplemented 71 + } 72 + 73 + func (s *micropubImplementation) HandleMediaUpload(file multipart.File, header *multipart.FileHeader) (string, error) { 74 + defer file.Close() 75 + 76 + kind, err := filetype.MatchReader(file) 77 + if err != nil { 78 + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 79 + } 80 + 81 + key := fmt.Sprintf("media/%s.%s", nanoid.New(), kind.Extension) 82 + _, err = s.db.Media.PutObject(context.TODO(), &s3.PutObjectInput{ 83 + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 84 + Key: &key, 85 + Body: file, 86 + }) 87 + if err != nil { 88 + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 89 + } 90 + 91 + return s.profileURL + key, nil 63 92 } 64 93 65 94 func (s *micropubImplementation) Create(req *micropub.Request) (string, error) {