~sircmpwn/pages.sr.ht

9cd00b9c6b3b28d2dca891eecd972371e5d84dcf — Dhruvin Gandhi 4 months ago e50e698 0.6.0
Add support for custom 404.html page
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, &notFound); 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)