~emersion/public-inbox

hut: pages: Add website publish v2 APPLIED

Thorben Günther: 2
 pages: Add website publish
 pages: Add unpublish

 11 files changed, 371 insertions(+), 0 deletions(-)
#654303 .build.yml success
hut/patches/.build.yml: SUCCESS in 52s

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

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

✓ #654303 SUCCESS hut/patches/.build.yml https://builds.sr.ht/~emersion/job/654303
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/27569/mbox | git am -3
Learn more about email & git

[PATCH hut v2 1/2] pages: Add website publish Export this patch

---
v1 -> v2:
- Some stylistic improvements
- Move protocol logic to function (for usage by unpublish)

 doc/hut.1.scd                     |  13 ++++
 main.go                           |   1 +
 pages.go                          |  81 ++++++++++++++++++++
 srht/generate.go                  |   1 +
 srht/pagessrht/gql.go             |  85 +++++++++++++++++++++
 srht/pagessrht/operations.graphql |   5 ++
 srht/pagessrht/schema.graphqls    | 121 ++++++++++++++++++++++++++++++
 7 files changed, 307 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)

# CONFIGURATION

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() {
	cmd.AddCommand(newBuildsCommand())
	cmd.AddCommand(newGitCommand())
	cmd.AddCommand(newPasteCommand())
	cmd.AddCommand(newPagesCommand())

	cmd.ExecuteContext(ctx)
}
diff --git a/pages.go b/pages.go
new file mode 100644
index 0000000..47e8f80
--- /dev/null
+++ b/pages.go
@@ -0,0 +1,81 @@
package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	"git.sr.ht/~emersion/gqlclient"
	"git.sr.ht/~emersion/hut/srht/pagessrht"
	"github.com/spf13/cobra"
)

func newPagesCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "pages",
		Short: "Use the pages API",
	}
	cmd.AddCommand(newPagesPublishCommand())
	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")
		}

		pagesProtocol, err := getProtocol(protocol)
		if err != nil {
			log.Fatal(err)
		}

		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\n", site.Domain)
	}

	cmd := &cobra.Command{
		Use:   "publish <archive>",
		Short: "Publish a website",
		Run:   run,
	}
	cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain name")
	cmd.Flags().StringVarP(&protocol, "protocol", "p", "HTTPS",
		"protocol (HTTPS or GEMINI)")
	return cmd
}

func getProtocol(protocol string) (pagessrht.Protocol, error) {
	switch strings.ToLower(protocol) {
	case "https":
		return pagessrht.ProtocolHttps, nil
	case "gemini":
		return pagessrht.ProtocolGemini, nil
	default:
		return "", fmt.Errorf("invalid protocol: %s", protocol)
	}
}
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 (
	"context"
	gqlclient "git.sr.ht/~emersion/gqlclient"
	"time"
)

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) {
        domain
    }
}
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
  GEMINI
}

# 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)
}
--
2.34.1

[PATCH hut v2 2/2] pages: Add unpublish Export this patch

---
 doc/hut.1.scd                     | 11 ++++++++++
 pages.go                          | 36 +++++++++++++++++++++++++++++++
 srht/pagessrht/gql.go             | 11 ++++++++++
 srht/pagessrht/operations.graphql |  6 ++++++
 4 files changed, 64 insertions(+)

diff --git a/doc/hut.1.scd b/doc/hut.1.scd
index e7e82b4..22b6361 100644
--- a/doc/hut.1.scd
+++ b/doc/hut.1.scd
@@ -64,6 +64,17 @@ hut is a CLI companion utility to interact with sr.ht.
	*-p, --protocol* <string>
		Protocol to use (either HTTPS or GEMINI; defaults to HTTPS)

*unpublish* [options...]
	Unpublish a website.

	Options are:

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

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

# CONFIGURATION

Generate a new OAuth2 access token on _meta.sr.ht_.
diff --git a/pages.go b/pages.go
index 47e8f80..2cda8c6 100644
--- a/pages.go
+++ b/pages.go
@@ -18,6 +18,7 @@ func newPagesCommand() *cobra.Command {
		Short: "Use the pages API",
	}
	cmd.AddCommand(newPagesPublishCommand())
	cmd.AddCommand(newPagesUnpublishCommand())
	return cmd
}

@@ -69,6 +70,41 @@ func newPagesPublishCommand() *cobra.Command {
	return cmd
}

func newPagesUnpublishCommand() *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")
		}

		pagesProtocol, err := getProtocol(protocol)
		if err != nil {
			log.Fatal(err)
		}

		c := createClient("pages")

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

		fmt.Printf("Unpublished site at %s\n", site.Domain)
	}

	cmd := &cobra.Command{
		Use:   "unpublish",
		Short: "Unpublish a website",
		Run:   run,
	}
	cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain name")
	cmd.Flags().StringVarP(&protocol, "protocol", "p", "HTTPS",
		"protocol (HTTPS or GEMINI)")
	return cmd
}

func getProtocol(protocol string) (pagessrht.Protocol, error) {
	switch strings.ToLower(protocol) {
	case "https":
diff --git a/srht/pagessrht/gql.go b/srht/pagessrht/gql.go
index 6f7d3a8..c136502 100644
--- a/srht/pagessrht/gql.go
+++ b/srht/pagessrht/gql.go
@@ -83,3 +83,14 @@ func Publish(client *gqlclient.Client, ctx context.Context, domain string, conte
	err = client.Execute(ctx, op, &respData)
	return respData.Publish, err
}

func Unpublish(client *gqlclient.Client, ctx context.Context, domain string, protocol *Protocol) (unpublish *Site, err error) {
	op := gqlclient.NewOperation("mutation unpublish ($domain: String!, $protocol: Protocol) {\n\tunpublish(domain: $domain, protocol: $protocol) {\n\t\tdomain\n\t}\n}\n")
	op.Var("domain", domain)
	op.Var("protocol", protocol)
	var respData struct {
		Unpublish *Site
	}
	err = client.Execute(ctx, op, &respData)
	return respData.Unpublish, err
}
diff --git a/srht/pagessrht/operations.graphql b/srht/pagessrht/operations.graphql
index 382aa18..2ab7ffd 100644
--- a/srht/pagessrht/operations.graphql
+++ b/srht/pagessrht/operations.graphql
@@ -3,3 +3,9 @@ mutation publish($domain: String!, $content: Upload!, $protocol: Protocol) {
        domain
    }
}

mutation unpublish($domain: String!, $protocol: Protocol) {
    unpublish(domain: $domain, protocol: $protocol) {
        domain
    }
}
-- 
2.34.1
hut/patches/.build.yml: SUCCESS in 52s

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

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

✓ #654303 SUCCESS hut/patches/.build.yml https://builds.sr.ht/~emersion/job/654303