M gqlgen.yml => gqlgen.yml +3 -0
@@ 60,3 60,6 @@ models:
Filter:
model:
- git.sr.ht/~sircmpwn/core-go/model.Filter
+ SiteConfig:
+ model:
+ - "map[string]interface{}"
M graph/api/generated.go => graph/api/generated.go +70 -5
@@ 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)
M graph/model/site.go => graph/model/site.go +2 -0
@@ 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},
M graph/schema.graphqls => graph/schema.graphqls +8 -1
@@ 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.
M graph/schema.resolvers.go => graph/schema.resolvers.go +28 -6
@@ 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
}
A migrations/003-add-not-found.sql => migrations/003-add-not-found.sql +7 -0
@@ 0,0 1,7 @@
+BEGIN;
+
+ALTER TABLE sites ADD COLUMN not_found varchar;
+
+UPDATE schema_version SET id = 3;
+
+COMMIT;
M schema.sql => schema.sql +2 -1
@@ 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)
);
M server.go => server.go +8 -3
@@ 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)