init
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
package clicmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/iceBear67/simplepages/api"
|
||||
"github.com/iceBear67/simplepages/internal/cliutil"
|
||||
)
|
||||
|
||||
// projectFlags are the settings shared by "project create" and "project
|
||||
// update". They map one-to-one onto api.ProjectPatch, whose fields are pointers
|
||||
// so an absent flag and an explicitly emptied one stay distinguishable.
|
||||
type projectFlags struct {
|
||||
displayName cliutil.OptString
|
||||
indexFile cliutil.OptString
|
||||
notFoundFile cliutil.OptString
|
||||
spa cliutil.OptBool
|
||||
cacheControl cliutil.OptString
|
||||
retention cliutil.OptInt
|
||||
grace cliutil.OptDuration
|
||||
maxFiles cliutil.OptInt
|
||||
maxFileBytes cliutil.OptBytes
|
||||
maxTotalBytes cliutil.OptBytes
|
||||
}
|
||||
|
||||
func (p *projectFlags) register(fs *flag.FlagSet) {
|
||||
fs.Var(&p.displayName, "display-name", "human-readable `name` shown in listings")
|
||||
fs.Var(&p.indexFile, "index-file", "document served for a directory, e.g. `index.html`")
|
||||
fs.Var(&p.notFoundFile, "not-found-file", "document served with 404, e.g. `404.html`; empty clears it")
|
||||
fs.Var(&p.spa, "spa", "serve the index document for unknown paths that accept HTML")
|
||||
fs.Var(&p.cacheControl, "cache-control", "Cache-Control `header` sent with every file")
|
||||
fs.Var(&p.retention, "retention", "`count` of finished deployments to keep per project")
|
||||
fs.Var(&p.grace, "retention-grace", "`duration` a deployment stays after being replaced, e.g. 1h")
|
||||
fs.Var(&p.maxFiles, "max-files", "`count` of files allowed in one deployment")
|
||||
fs.Var(&p.maxFileBytes, "max-file-bytes", "largest single file, e.g. `256MiB`")
|
||||
fs.Var(&p.maxTotalBytes, "max-total-bytes", "largest total deployment, e.g. `2GiB`")
|
||||
}
|
||||
|
||||
func (p *projectFlags) patch() api.ProjectPatch {
|
||||
return api.ProjectPatch{
|
||||
DisplayName: p.displayName.Ptr(),
|
||||
IndexFile: p.indexFile.Ptr(),
|
||||
NotFoundFile: p.notFoundFile.Ptr(),
|
||||
SPAFallback: p.spa.Ptr(),
|
||||
CacheControl: p.cacheControl.Ptr(),
|
||||
RetentionCount: p.retention.Ptr(),
|
||||
RetentionGrace: p.grace.SecondsPtr(),
|
||||
MaxFiles: p.maxFiles.Ptr(),
|
||||
MaxFileBytes: p.maxFileBytes.Ptr(),
|
||||
MaxTotalBytes: p.maxTotalBytes.Ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
func projectCmd(g *Globals) *cliutil.Command {
|
||||
return &cliutil.Command{
|
||||
Name: "project",
|
||||
Short: "Manage projects",
|
||||
Sub: []*cliutil.Command{
|
||||
projectCreateCmd(g),
|
||||
projectListCmd(g),
|
||||
projectShowCmd(g),
|
||||
projectUpdateCmd(g),
|
||||
projectDeleteCmd(g),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func projectCreateCmd(g *Globals) *cliutil.Command {
|
||||
var pf projectFlags
|
||||
return &cliutil.Command{
|
||||
Name: "create",
|
||||
Args: "<name>",
|
||||
Short: "Create a project",
|
||||
Long: "The name becomes the URL prefix, so it is restricted to lowercase\n" +
|
||||
"letters, digits, dot, dash and underscore, and cannot be changed later.\n" +
|
||||
"Settings left unset take the server's defaults. Requires an admin key.",
|
||||
Flags: pf.register,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 1, "one project name"); err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patch := pf.patch()
|
||||
p, err := c.CreateProject(ctx, api.CreateProjectRequest{Name: args[0], Patch: &patch})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.printProject(p)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func projectListCmd(g *Globals) *cliutil.Command {
|
||||
return &cliutil.Command{
|
||||
Name: "list",
|
||||
Short: "List projects",
|
||||
Long: "Requires an admin key. Follows paging to the end.",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps, err := c.ListAllProjects(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// api.ProjectList rather than the bare slice, so -o json produces the
|
||||
// same shape the API returns and a nil slice still prints as [].
|
||||
out := api.ProjectList{Projects: ps}
|
||||
if out.Projects == nil {
|
||||
out.Projects = []api.Project{}
|
||||
}
|
||||
return p.Print(out, func() *cliutil.Table {
|
||||
t := cliutil.NewTable("NAME", "DISPLAY NAME", "INDEX", "SPA", "KEEP", "UPDATED")
|
||||
for _, pr := range ps {
|
||||
t.Row(pr.Name, cliutil.Str(cliutil.Truncate(pr.DisplayName, 32)),
|
||||
pr.IndexFile, cliutil.Bool(pr.SPAFallback),
|
||||
strconv.Itoa(pr.RetentionCount), cliutil.Time(pr.UpdatedAt))
|
||||
}
|
||||
return t
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func projectShowCmd(g *Globals) *cliutil.Command {
|
||||
return &cliutil.Command{
|
||||
Name: "show",
|
||||
Args: "[name]",
|
||||
Short: "Show one project",
|
||||
Long: "Defaults to --project. A project key may read only its own project.",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
name, err := g.oneProject(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := c.GetProject(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.printProject(p)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func projectUpdateCmd(g *Globals) *cliutil.Command {
|
||||
var pf projectFlags
|
||||
return &cliutil.Command{
|
||||
Name: "update",
|
||||
Args: "[name]",
|
||||
Short: "Change a project's settings",
|
||||
Long: "Only the settings named by flags are changed. Requires an admin key.\n" +
|
||||
"--not-found-file= with an empty value clears the custom 404 document.",
|
||||
Flags: pf.register,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
name, err := g.oneProject(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patch := pf.patch()
|
||||
if patch == (api.ProjectPatch{}) {
|
||||
return fmt.Errorf("nothing to change: pass at least one setting flag")
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := c.PatchProject(ctx, name, patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.printProject(p)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func projectDeleteCmd(g *Globals) *cliutil.Command {
|
||||
var yes bool
|
||||
return &cliutil.Command{
|
||||
Name: "delete",
|
||||
Args: "[name]",
|
||||
Short: "Delete a project and everything in it",
|
||||
Long: "Removes the project's keys and deployments and unpublishes the site.\n" +
|
||||
"The uploaded content is reclaimed by the next garbage collection.\n" +
|
||||
"Requires an admin key.",
|
||||
Flags: func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&yes, "yes", false, "do not ask for confirmation")
|
||||
},
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
name, err := g.oneProject(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !yes {
|
||||
if err := cliutil.Confirm(g.In, g.Err,
|
||||
fmt.Sprintf("Delete project %q, its keys and all its deployments?", name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c, err := g.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.DeleteProject(ctx, name); err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Printf("deleted project %s\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// oneProject takes the project from the positional argument, falling back to
|
||||
// --project. Both are accepted because "pages project show demo" reads better
|
||||
// at a prompt while "--project" is what a CI job already has set.
|
||||
func (g *Globals) oneProject(args []string) (string, error) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return g.ProjectName()
|
||||
case 1:
|
||||
return args[0], nil
|
||||
default:
|
||||
return "", fmt.Errorf("expected at most one project name")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Globals) printProject(pr api.Project) error {
|
||||
p, err := g.Printer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Print(pr, func() *cliutil.Table {
|
||||
t := cliutil.NewTable()
|
||||
t.Row("name", pr.Name)
|
||||
t.Row("display_name", cliutil.Str(pr.DisplayName))
|
||||
t.Row("url", cliutil.Str(pr.URL))
|
||||
t.Row("index_file", pr.IndexFile)
|
||||
t.Row("not_found_file", cliutil.Str(pr.NotFoundFile))
|
||||
t.Row("spa_fallback", cliutil.Bool(pr.SPAFallback))
|
||||
t.Row("cache_control", pr.CacheControl)
|
||||
t.Row("retention_count", strconv.Itoa(pr.RetentionCount))
|
||||
t.Row("retention_grace", (time.Duration(pr.RetentionGrace) * time.Second).String())
|
||||
t.Row("max_files", strconv.Itoa(pr.MaxFiles))
|
||||
t.Row("max_file_bytes", cliutil.Bytes(pr.MaxFileBytes))
|
||||
t.Row("max_total_bytes", cliutil.Bytes(pr.MaxTotalBytes))
|
||||
t.Row("created_at", cliutil.Time(pr.CreatedAt))
|
||||
t.Row("updated_at", cliutil.Time(pr.UpdatedAt))
|
||||
if d := pr.ActiveDeployment; d != nil {
|
||||
t.Row("active_deployment", d.ID)
|
||||
t.Row("active_files", strconv.Itoa(d.FileCount))
|
||||
t.Row("active_bytes", cliutil.Bytes(d.TotalBytes))
|
||||
t.Row("activated_at", cliutil.TimePtr(d.ActivatedAt))
|
||||
}
|
||||
return t
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user