hut: pages: Add website publish v1 SUPERSEDED

Thorben Günther: 1
 pages: Add website publish

 7 files changed, 301 insertions(+), 0 deletions(-)
#654288 .build.yml success
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/public-inbox/patches/27564/mbox | git am -3
Learn more about email & git

[PATCH hut] pages: Add website publish Export this patch

 doc/hut.1.scd                     |  13 ++++
 main.go                           |   1 +
 pages.go                          |  75 ++++++++++++++++++
 srht/generate.go                  |   1 +
 srht/pagessrht/gql.go             |  85 +++++++++++++++++++++
 srht/pagessrht/operations.graphql |   5 ++
 srht/pagessrht/schema.graphqls    | 121 ++++++++++++++++++++++++++++++
 7 files changed, 301 insertions(+)
 create mode 100644 pages.go
 create mode 100644 srht/pagessrht/gql.go
 create mode 100644 srht/pagessrht/operations.graphql
 create mode 100644 srht/pagessrht/schema.graphqls

diff --git a/doc/hut.1.scd b/doc/hut.1.scd
index de1fb60..e7e82b4 100644
--- a/doc/hut.1.scd
+++ b/doc/hut.1.scd
@@ -51,6 +51,19 @@ hut is a CLI companion utility to interact with sr.ht.
*create* <filenames...>
	Create a new paste.

## pages

*publish* <tarball> [options...]
	Publish a website.

	Options are:

	*-d, --domain* <string>
		Fully qualified domain name.

	*-p, --protocol* <string>
		Protocol to use (either HTTPS or GEMINI; defaults to HTTPS)


Generate a new OAuth2 access token on _meta.sr.ht_.
diff --git a/main.go b/main.go
index 1ddac62..96768ed 100644
--- a/main.go
+++ b/main.go
@@ -18,6 +18,7 @@ func main() {

diff --git a/pages.go b/pages.go
new file mode 100644
index 0000000..235f582
--- /dev/null
+++ b/pages.go
@@ -0,0 +1,75 @@
package main

import (


func newPagesCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "pages",
		Short: "Use the pages API",
	return cmd

func newPagesPublishCommand() *cobra.Command {
	var domain, protocol string
	run := func(cmd *cobra.Command, args []string) {
		ctx := cmd.Context()

		if domain == "" {
			log.Fatal("enter a domain with --domain")

		var pagesProtocol pagessrht.Protocol
		switch strings.ToLower(protocol) {
		case "https":
			pagesProtocol = pagessrht.ProtocolHttps
		case "gemini":
			pagesProtocol = pagessrht.ProtocolGemini
			log.Fatalf("invalid protocol: %s", protocol)

		c := createClient("pages")

		if len(args) != 1 {
			log.Fatal("enter a tarball to upload")
		filename := args[0]

		f, err := os.Open(filename)
		if err != nil {
			log.Fatalf("failed to open input file: %v", err)
		defer f.Close()

		file := gqlclient.Upload{Body: f, Filename: filepath.Base(filename)}

		site, err := pagessrht.Publish(c.Client, ctx, domain, file, &pagesProtocol)
		if err != nil {
			log.Fatalf("failed to publish site: %v", err)

		fmt.Printf("Published site at %s", site.Domain)

	cmd := &cobra.Command{
		Use:   "publish <archive>",
		Short: "",
		Run:   run,
	cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain name")
	cmd.Flags().StringVarP(&protocol, "protocol", "p", "HTTPS",
		"protocol (HTTPS or GEMINI)")
	return cmd
diff --git a/srht/generate.go b/srht/generate.go
index 3418f53..b598242 100644
--- a/srht/generate.go
+++ b/srht/generate.go
@@ -10,3 +10,4 @@ import (
//go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s pastesrht/schema.graphqls -q pastesrht/operations.graphql -n pastesrht -o pastesrht/gql.go
//go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s buildssrht/schema.graphqls -q buildssrht/operations.graphql -n buildssrht -o buildssrht/gql.go
//go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s gitsrht/schema.graphqls -q gitsrht/operations.graphql -n gitsrht -o gitsrht/gql.go
//go:generate go run git.sr.ht/~emersion/gqlclient/cmd/gqlclientgen -s pagessrht/schema.graphqls -q pagessrht/operations.graphql -n pagessrht -o pagessrht/gql.go
diff --git a/srht/pagessrht/gql.go b/srht/pagessrht/gql.go
new file mode 100644
index 0000000..6f7d3a8
--- /dev/null
+++ b/srht/pagessrht/gql.go
@@ -0,0 +1,85 @@
// Code generated by gqlclientgen - DO NOT EDIT

package pagessrht

import (
	gqlclient "git.sr.ht/~emersion/gqlclient"

type AccessKind string

const (
	AccessKindRo AccessKind = "RO"
	AccessKindRw AccessKind = "RW"

type AccessScope string

const (
	AccessScopeProfile AccessScope = "PROFILE"
	AccessScopeSites   AccessScope = "SITES"
	AccessScopePages   AccessScope = "PAGES"

type Cursor string

type Entity struct {
	Id            int32     `json:"id"`
	Created       time.Time `json:"created"`
	Updated       time.Time `json:"updated"`
	CanonicalName string    `json:"canonicalName"`

type Protocol string

const (
	ProtocolHttps  Protocol = "HTTPS"
	ProtocolGemini Protocol = "GEMINI"

type Site struct {
	Id       int32     `json:"id"`
	Created  time.Time `json:"created"`
	Updated  time.Time `json:"updated"`
	Domain   string    `json:"domain"`
	Protocol Protocol  `json:"protocol"`
	Version  string    `json:"version"`

type SiteCursor struct {
	Results []*Site `json:"results"`
	Cursor  *Cursor `json:"cursor,omitempty"`

type User struct {
	Id            int32     `json:"id"`
	Created       time.Time `json:"created"`
	Updated       time.Time `json:"updated"`
	CanonicalName string    `json:"canonicalName"`
	Username      string    `json:"username"`
	Email         string    `json:"email"`
	Url           *string   `json:"url,omitempty"`
	Location      *string   `json:"location,omitempty"`
	Bio           *string   `json:"bio,omitempty"`

type Version struct {
	Major           int32     `json:"major"`
	Minor           int32     `json:"minor"`
	Patch           int32     `json:"patch"`
	DeprecationDate time.Time `json:"deprecationDate,omitempty"`

func Publish(client *gqlclient.Client, ctx context.Context, domain string, content gqlclient.Upload, protocol *Protocol) (publish Site, err error) {
	op := gqlclient.NewOperation("mutation publish ($domain: String!, $content: Upload!, $protocol: Protocol) {\n\tpublish(domain: $domain, content: $content, protocol: $protocol) {\n\t\tdomain\n\t}\n}\n")
	op.Var("domain", domain)
	op.Var("content", content)
	op.Var("protocol", protocol)
	var respData struct {
		Publish Site
	err = client.Execute(ctx, op, &respData)
	return respData.Publish, err
diff --git a/srht/pagessrht/operations.graphql b/srht/pagessrht/operations.graphql
new file mode 100644
index 0000000..382aa18
--- /dev/null
+++ b/srht/pagessrht/operations.graphql
@@ -0,0 +1,5 @@
mutation publish($domain: String!, $content: Upload!, $protocol: Protocol) {
    publish(domain: $domain, content: $content, protocol: $protocol) {
diff --git a/srht/pagessrht/schema.graphqls b/srht/pagessrht/schema.graphqls
new file mode 100644
index 0000000..d2c03c4
--- /dev/null
+++ b/srht/pagessrht/schema.graphqls
@@ -0,0 +1,121 @@
scalar Cursor
scalar Time
scalar Upload

# Used to provide a human-friendly description of an access scope
directive @scopehelp(details: String!) on ENUM_VALUE

enum AccessScope {
  PROFILE @scopehelp(details: "profile information")
  SITES   @scopehelp(details: "registered sites")
  PAGES   @scopehelp(details: "contents of registered sites")

enum AccessKind {
  RO @scopehelp(details: "read")
  RW @scopehelp(details: "read and write")

# Decorates fields for which access requires a particular OAuth 2.0 scope with
# read or write access.
directive @access(scope: AccessScope!, kind: AccessKind!) on FIELD_DEFINITION

enum Protocol {

# https://semver.org
type Version {
  major: Int!
  minor: Int!
  patch: Int!
  # If this API version is scheduled for deprecation, this is the date on which
  # it will stop working; or null if this API version is not scheduled for
  # deprecation.
  deprecationDate: Time

interface Entity {
  id: Int!
  created: Time!
  updated: Time!
  # The canonical name of this entity. For users, this is their username
  # prefixed with '~'. Additional entity types will be supported in the future.
  canonicalName: String!

type User implements Entity {
  id: Int!
  created: Time!
  updated: Time!
  canonicalName: String!
  username: String!
  email: String!
  url: String
  location: String
  bio: String

# A published website
type Site {
  id: Int!
  created: Time!
  updated: Time!
  # Domain name the site services
  domain: String!
  # The site protocol
  protocol: Protocol!
  # SHA-256 checksum of the source tarball (uncompressed)
  version: String!

# A cursor for enumerating site entries
# If there are additional results available, the cursor object may be passed
# back into the same endpoint to retrieve another page. If the cursor is null,
# there are no remaining results to return.
type SiteCursor {
  results: [Site]!
  cursor: Cursor

type Query {
  # Returns API version information.
  version: Version!

  # Returns the authenticated user.
  me: User! @access(scope: PROFILE, kind: RO)

  # Returns a list of registered sites on your account.
  sites(cursor: Cursor): SiteCursor! @access(scope: SITES, kind: RO)

type Mutation {
  # Publishes a website. If the domain already exists on your account, it is
  # updated to a new version. If the domain already exists under someone else's
  # account, the request is rejected. If the domain does not exist, a new site
  # is created.
  # Every user is given exclusive use over the 'username.srht.site' domain, and
  # it requires no special configuration to use. Users may also bring their own
  # domain name, in which case they should consult the configuration docs:
  # https://man.sr.ht/pages.sr.ht
  # 'content' must be a .tar.gz file. It must contain only directories and
  # regular files of mode 644. Symlinks are not supported. No error is returned
  # for an invalid tarball; the invalid data is simply discarded.
  # If protocol is unset, HTTPS is presumed.
  # If subdirectory is set, only the specified directory is updated. The rest
  # of the files are unchanged.
  publish(domain: String!, content: Upload!, protocol: Protocol,
    subdirectory: String): Site! @access(scope: PAGES, kind: RW)

  # Deletes a previously published website.
  # If protocol is unset, HTTPS is presumed.
  unpublish(domain: String!, protocol: Protocol): Site @access(scope: SITES, kind: RW)
hut/patches/.build.yml: SUCCESS in 32s

[pages: Add website publish][0] from [Thorben Günther][1]

[0]: https://lists.sr.ht/~emersion/public-inbox/patches/27564
[1]: mailto:admin@xenrox.net

✓ #654288 SUCCESS hut/patches/.build.yml https://builds.sr.ht/~emersion/job/654288
Looks good and pushed with a minor edit, thanks!

On Tuesday, December 21st, 2021 at 20:25, Thorben Günther <admin@xenrox.net> wrote: