Dhruvin Gandhi: 1 Add support for custom 404.html page 8 files changed, 128 insertions(+), 16 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/28884/mbox | git am -3Learn more about email & git
--- gqlgen.yml | 3 ++ graph/api/generated.go | 75 +++++++++++++++++++++++++++++--- graph/model/site.go | 2 + graph/schema.graphqls | 9 +++- graph/schema.resolvers.go | 34 ++++++++++++--- migrations/003-add-not-found.sql | 7 +++ schema.sql | 3 +- server.go | 11 +++-- 8 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 migrations/003-add-not-found.sql diff --git a/gqlgen.yml b/gqlgen.yml index 33d349a..28cd866 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -60,3 +60,6 @@ models: Filter: model: - git.sr.ht/~sircmpwn/core-go/model.Filter + SiteConfig: + model: + - "map[string]interface{}" diff --git a/graph/api/generated.go b/graph/api/generated.go index b3e870a..161fa86 100644 --- a/graph/api/generated.go +++ b/graph/api/generated.go @@ -49,7 +49,7 @@ type DirectiveRoot struct { type ComplexityRoot struct { Mutation struct { - Publish func(childComplexity int, domain string, content graphql.Upload, protocol *model.Protocol, subdirectory *string) int + Publish func(childComplexity int, domain string, content graphql.Upload, protocol *model.Protocol, subdirectory *string, siteConfig map[string]interface{}) int Unpublish func(childComplexity int, domain string, protocol *model.Protocol) int } @@ -63,6 +63,7 @@ type ComplexityRoot struct { Created func(childComplexity int) int Domain func(childComplexity int) int ID func(childComplexity int) int + NotFound func(childComplexity int) int Protocol func(childComplexity int) int Updated func(childComplexity int) int Version func(childComplexity int) int @@ -94,7 +95,7 @@ type ComplexityRoot struct { } type MutationResolver interface { - Publish(ctx context.Context, domain string, content graphql.Upload, protocol *model.Protocol, subdirectory *string) (*model.Site, error) + Publish(ctx context.Context, domain string, content graphql.Upload, protocol *model.Protocol, subdirectory *string, siteConfig map[string]interface{}) (*model.Site, error) Unpublish(ctx context.Context, domain string, protocol *model.Protocol) (*model.Site, error) } type QueryResolver interface { @@ -128,7 +129,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.Publish(childComplexity, args["domain"].(string), args["content"].(graphql.Upload), args["protocol"].(*model.Protocol), args["subdirectory"].(*string)), true + return e.complexity.Mutation.Publish(childComplexity, args["domain"].(string), args["content"].(graphql.Upload), args["protocol"].(*model.Protocol), args["subdirectory"].(*string), args["siteConfig"].(map[string]interface{})), true case "Mutation.unpublish": if e.complexity.Mutation.Unpublish == nil { @@ -189,6 +190,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Site.ID(childComplexity), true + case "Site.notFound": + if e.complexity.Site.NotFound == nil { + break + } + + return e.complexity.Site.NotFound(childComplexity), true + case "Site.protocol": if e.complexity.Site.Protocol == nil { break @@ -455,6 +463,8 @@ type Site { protocol: Protocol! "SHA-256 checksum of the source tarball (uncompressed)" version: String! + "Path to the file to serve for 404 Not Found responses" + notFound: String } """ @@ -469,6 +479,11 @@ type SiteCursor { cursor: Cursor } +input SiteConfig { + "Path to the file to serve for 404 Not Found responses" + notFound: String +} + type Query { "Returns API version information." version: Version! @@ -503,7 +518,7 @@ type Mutation { of the files are unchanged. """ publish(domain: String!, content: Upload!, protocol: Protocol, - subdirectory: String): Site! @access(scope: PAGES, kind: RW) + subdirectory: String, siteConfig: SiteConfig): Site! @access(scope: PAGES, kind: RW) """ Deletes a previously published website. @@ -598,6 +613,15 @@ func (ec *executionContext) field_Mutation_publish_args(ctx context.Context, raw } } args["subdirectory"] = arg3 + var arg4 map[string]interface{} + if tmp, ok := rawArgs["siteConfig"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("siteConfig")) + arg4, err = ec.unmarshalOSiteConfig2map(ctx, tmp) + if err != nil { + return nil, err + } + } + args["siteConfig"] = arg4 return args, nil } @@ -719,7 +743,7 @@ func (ec *executionContext) _Mutation_publish(ctx context.Context, field graphql resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().Publish(rctx, args["domain"].(string), args["content"].(graphql.Upload), args["protocol"].(*model.Protocol), args["subdirectory"].(*string)) + return ec.resolvers.Mutation().Publish(rctx, args["domain"].(string), args["content"].(graphql.Upload), args["protocol"].(*model.Protocol), args["subdirectory"].(*string), args["siteConfig"].(map[string]interface{})) } directive1 := func(ctx context.Context) (interface{}, error) { scope, err := ec.unmarshalNAccessScope2gitᚗsrᚗhtᚋאsircmpwnᚋpagesᚗsrᚗhtᚋgraphᚋmodelᚐAccessScope(ctx, "PAGES") @@ -1279,6 +1303,38 @@ func (ec *executionContext) _Site_version(ctx context.Context, field graphql.Col return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _Site_notFound(ctx context.Context, field graphql.CollectedField, obj *model.Site) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Site", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.NotFound, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _SiteCursor_results(ctx context.Context, field graphql.CollectedField, obj *model.SiteCursor) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3046,6 +3102,8 @@ func (ec *executionContext) _Site(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "notFound": + out.Values[i] = ec._Site_notFound(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3918,6 +3976,13 @@ func (ec *executionContext) marshalOSite2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋpages return ec._Site(ctx, sel, v) } +func (ec *executionContext) unmarshalOSiteConfig2map(ctx context.Context, v interface{}) (map[string]interface{}, error) { + if v == nil { + return nil, nil + } + return v.(map[string]interface{}), nil +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/graph/model/site.go b/graph/model/site.go index 5370447..3f3fd58 100644 --- a/graph/model/site.go +++ b/graph/model/site.go @@ -19,6 +19,7 @@ type Site struct { Domain string `json:"domain"` Protocol Protocol `json:"protocol"` Version string `json:"version"` + NotFound *string `json:"notFound"` alias string fields *database.ModelFields @@ -49,6 +50,7 @@ func (site *Site) Fields() *database.ModelFields { {"domain", "domain", &site.Domain}, {"protocol", "protocol", &site.Protocol}, {"version", "version", &site.Version}, + {"not_found", "notFound", &site.NotFound}, // Always fetch: {"id", "", &site.ID}, diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 393e33f..90935eb 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -74,6 +74,8 @@ type Site { protocol: Protocol! "SHA-256 checksum of the source tarball (uncompressed)" version: String! + "Path to the file to serve for 404 Not Found responses" + notFound: String } """ @@ -88,6 +90,11 @@ type SiteCursor { cursor: Cursor } +input SiteConfig { + "Path to the file to serve for 404 Not Found responses" + notFound: String +} + type Query { "Returns API version information." version: Version! @@ -122,7 +129,7 @@ type Mutation { of the files are unchanged. """ publish(domain: String!, content: Upload!, protocol: Protocol, - subdirectory: String): Site! @access(scope: PAGES, kind: RW) + subdirectory: String, siteConfig: SiteConfig): Site! @access(scope: PAGES, kind: RW) """ Deletes a previously published website. diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index f88e771..7e9162d 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -30,7 +30,7 @@ import ( minio "github.com/minio/minio-go/v7" ) -func (r *mutationResolver) Publish(ctx context.Context, domain string, content graphql.Upload, protocol *model.Protocol, subdirectory *string) (*model.Site, error) { +func (r *mutationResolver) Publish(ctx context.Context, domain string, content graphql.Upload, protocol *model.Protocol, subdirectory *string, siteConfig map[string]interface{}) (*model.Site, error) { conf := config.ForContext(ctx) bucket, _ := conf.Get("pages.sr.ht", "s3-bucket") prefix, _ := conf.Get("pages.sr.ht", "s3-prefix") @@ -62,6 +62,12 @@ func (r *mutationResolver) Publish(ctx context.Context, domain string, content g } } + if value, ok := siteConfig["notFound"].(string); ok { + if value == "" { + return nil, fmt.Errorf("Invalid path siteConfig.notFound") + } + } + if strings.HasSuffix(domain, "."+userDomain) { user := strings.TrimSuffix(domain, "."+userDomain) if user != auth.ForContext(ctx).Username { @@ -130,11 +136,11 @@ func (r *mutationResolver) Publish(ctx context.Context, domain string, content g ON CONFLICT ON CONSTRAINT sites_domain_protocol_key DO UPDATE SET updated = NOW() at time zone 'utc' WHERE sites.user_id = $1 - RETURNING id, created, updated, domain, protocol, version;`, + RETURNING id, created, updated, domain, protocol, version, not_found;`, auth.ForContext(ctx).UserID, domain, proto) if err := row.Scan(&site.ID, &site.Created, &site.Updated, &site.Domain, &site.Protocol, - &site.Version); err != nil { + &site.Version, &site.NotFound); err != nil { if err == sql.ErrNoRows { return fmt.Errorf("This domain is not available") } @@ -149,6 +155,22 @@ func (r *mutationResolver) Publish(ctx context.Context, domain string, content g io.WriteString(sha, site.Version+"\n") } + notFound := site.NotFound + if value, ok := siteConfig["notFound"]; ok { + switch value.(type) { + case nil: + notFound = nil + case string: + value := path.Join("/", value.(string)) + notFound = &value + default: + panic("GraphQL schema validation broken") + } + } + if notFound != nil { + io.WriteString(sha, *notFound+"\n") + } + inputReader := io.TeeReader(content.File, sha) gzipReader, err := gzip.NewReader(inputReader) if err != nil { @@ -183,9 +205,9 @@ func (r *mutationResolver) Publish(ctx context.Context, domain string, content g prevPath := path.Join(prefix, "sites", domain, site.Version) row = tx.QueryRowContext(ctx, ` - UPDATE sites SET version = $1 WHERE id = $2 RETURNING version;`, - shahex, site.ID) - if err := row.Scan(&site.Version); err != nil { + UPDATE sites SET version = $1, not_found = $2 WHERE id = $3 RETURNING version, not_found;`, + shahex, notFound, site.ID) + if err := row.Scan(&site.Version, &site.NotFound); err != nil { return err } diff --git a/migrations/003-add-not-found.sql b/migrations/003-add-not-found.sql new file mode 100644 index 0000000..dbc47b3 --- /dev/null +++ b/migrations/003-add-not-found.sql @@ -0,0 +1,7 @@ +BEGIN; + +ALTER TABLE sites ADD COLUMN not_found varchar; + +UPDATE schema_version SET id = 3; + +COMMIT; diff --git a/schema.sql b/schema.sql index 2e473ea..1a27f0c 100644 --- a/schema.sql +++ b/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE schema_version ( id integer NOT NULL ); -INSERT INTO schema_version VALUES (2); +INSERT INTO schema_version VALUES (3); CREATE TABLE "user" ( id serial PRIMARY KEY, @@ -29,6 +29,7 @@ CREATE TABLE sites ( domain varchar NOT NULL, protocol protocol NOT NULL, version varchar NOT NULL, + not_found varchar, UNIQUE (domain, protocol) ); diff --git a/server.go b/server.go index ef51d2f..841232f 100644 --- a/server.go +++ b/server.go @@ -141,7 +141,7 @@ func main() { File: f, Filename: h.Filename, Size: h.Size, - }, &protocol, &subdir) + }, &protocol, &subdir, nil) if err != nil { http.Error(w, err.Error(), 400) return @@ -176,17 +176,18 @@ func ServeHTTP(conf ini.File, db *sql.DB, mc *minio.Client) *http.Server { strings.Join(sandbox, " "))) var version string + var notFound *string ctx := database.Context(r.Context(), db) if err := database.WithTx(ctx, &sql.TxOptions{ ReadOnly: true, Isolation: 0, }, func(tx *sql.Tx) error { row := tx.QueryRowContext(ctx, ` - SELECT version + SELECT version, not_found FROM sites WHERE domain = $1 AND protocol = 'https'; `, r.Host) - if err := row.Scan(&version); err != nil { + if err := row.Scan(&version, ¬Found); err != nil { return err } return nil @@ -202,6 +203,10 @@ func ServeHTTP(conf ini.File, db *sql.DB, mc *minio.Client) *http.Server { path.Join(r.URL.Path, "index.html"), } + if notFound != nil { + paths = append(paths, *notFound) + } + var object *minio.Object for _, cand := range paths { s3path := path.Join(prefix, "sites", r.Host, version, cand) -- 2.35.1
builds.sr.ht <builds@sr.ht>pages.sr.ht/patches: SUCCESS in 1m28s [Add support for custom 404.html page][0] v6 from [Dhruvin Gandhi][1] [0]: https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/28884 [1]: mailto:contact@dhruvin.dev ✓ #684817 SUCCESS pages.sr.ht/patches/archlinux.yml https://builds.sr.ht/~sircmpwn/job/684817 ✓ #684816 SUCCESS pages.sr.ht/patches/alpine.yml https://builds.sr.ht/~sircmpwn/job/684816
Thanks! To git@git.sr.ht:~sircmpwn/pages.sr.ht e50e698..9cd00b9 master -> master