init command
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
## Generated binaries
|
||||
pb
|
||||
|
||||
## Build artifacts and caches
|
||||
.cache/
|
||||
# bin/
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
|
||||
## Go build specific
|
||||
/vendor/
|
||||
|
||||
## Editor temporary files
|
||||
*.swp
|
||||
*.swo
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
## macOS
|
||||
.DS_Store
|
||||
|
||||
13
README.md
Normal file
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# pb
|
||||
|
||||
A simple, rsync-based PocketBase deployment tool.
|
||||
|
||||
## init
|
||||
|
||||
- start a new PocketBase project
|
||||
|
||||
## deploy
|
||||
|
||||
## logs
|
||||
|
||||
## secrets
|
||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module pb
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require github.com/urfave/cli/v2 v2.27.7
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
)
|
||||
8
go.sum
Normal file
8
go.sum
Normal file
@@ -0,0 +1,8 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
269
main.go
Normal file
269
main.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "pb",
|
||||
Usage: "PocketBase deployment helper",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "init",
|
||||
Usage: "start a new PocketBase project",
|
||||
Action: initAction(),
|
||||
},
|
||||
{
|
||||
Name: "deploy",
|
||||
Usage: "deploy the PocketBase project",
|
||||
Action: placeholderAction("deploy"),
|
||||
},
|
||||
{
|
||||
Name: "logs",
|
||||
Usage: "show PocketBase logs",
|
||||
Action: placeholderAction("logs"),
|
||||
},
|
||||
{
|
||||
Name: "secrets",
|
||||
Usage: "manage deployment secrets",
|
||||
Action: placeholderAction("secrets"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func placeholderAction(name string) func(*cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
fmt.Printf("TODO: implement %s command\n", name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func initAction() func(*cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pbPath := filepath.Join(cwd, "pb.toml")
|
||||
if _, err := os.Stat(pbPath); err == nil {
|
||||
return fmt.Errorf("pb.toml already exists, aborting")
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceName := generateServiceName()
|
||||
|
||||
if err := writePBConfig(pbPath, serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(cwd, "bin")
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(targetDir, pocketbaseBinaryName(runtime.GOOS))
|
||||
if err := downloadPocketbase(defaultPocketbaseVersion, runtime.GOOS, runtime.GOARCH, binaryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureGitignoreEntries(filepath.Join(cwd, ".gitignore"), []string{"bin/pocketbase", "pb.toml"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Initialized PocketBase project %q\n", serviceName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPocketbaseVersion = "0.35.1"
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func writePBConfig(path, serviceName string) error {
|
||||
const tmpl = `[server]
|
||||
ip = "127.0.0.1"
|
||||
port = 8090
|
||||
domain = "example.com"
|
||||
|
||||
[pocketbase]
|
||||
version = "%s"
|
||||
service_name = "%s"
|
||||
`
|
||||
return os.WriteFile(path, []byte(fmt.Sprintf(tmpl, defaultPocketbaseVersion, serviceName)), 0o644)
|
||||
}
|
||||
|
||||
func generateServiceName() string {
|
||||
adjectives := []string{"sunny", "silent", "cobalt", "rusty", "plush", "brisk", "sage", "glow"}
|
||||
nouns := []string{"harbor", "delta", "bloom", "atlas", "ripple", "ember", "lumen", "orbit"}
|
||||
adjective := adjectives[rand.Intn(len(adjectives))]
|
||||
noun := nouns[rand.Intn(len(nouns))]
|
||||
return fmt.Sprintf("%s-%s", adjective, noun)
|
||||
}
|
||||
|
||||
func pocketbaseBinaryName(goos string) string {
|
||||
if goos == "windows" {
|
||||
return "pocketbase.exe"
|
||||
}
|
||||
return "pocketbase"
|
||||
}
|
||||
|
||||
func downloadPocketbase(version, goos, goarch, dest string) error {
|
||||
url, binaryName, err := releaseAssetURL(version, goos, goarch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status %s while downloading %s", resp.Status, url)
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "pocketbase-*.zip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close()
|
||||
|
||||
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := extractBinary(tmpFile.Name(), binaryName, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Chmod(dest, 0o755)
|
||||
}
|
||||
|
||||
func releaseAssetURL(version, goos, goarch string) (string, string, error) {
|
||||
osName := map[string]string{
|
||||
"darwin": "darwin",
|
||||
"linux": "linux",
|
||||
"windows": "windows",
|
||||
}
|
||||
archName := map[string]string{
|
||||
"amd64": "amd64",
|
||||
"386": "386",
|
||||
"arm64": "arm64",
|
||||
"arm": "armv7",
|
||||
}
|
||||
|
||||
osPart, ok := osName[goos]
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("unsupported GOOS: %s", goos)
|
||||
}
|
||||
archPart, ok := archName[goarch]
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("unsupported GOARCH: %s", goarch)
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("pocketbase_%s_%s_%s.zip", version, osPart, archPart)
|
||||
url := fmt.Sprintf("https://github.com/pocketbase/pocketbase/releases/download/v%s/%s", version, filename)
|
||||
return url, pocketbaseBinaryName(goos), nil
|
||||
}
|
||||
|
||||
func extractBinary(zipPath, binaryName, dest string) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
if filepath.Base(f.Name) != binaryName {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("binary %s not found in archive", binaryName)
|
||||
}
|
||||
|
||||
func ensureGitignoreEntries(path string, entries []string) error {
|
||||
var existing = make(map[string]struct{})
|
||||
var original []byte
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
original = data
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" {
|
||||
existing[trimmed] = struct{}{}
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
builder := &strings.Builder{}
|
||||
if len(original) > 0 {
|
||||
builder.Write(original)
|
||||
if !strings.HasSuffix(string(original), "\n") {
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
var added bool
|
||||
for _, entry := range entries {
|
||||
if _, seen := existing[entry]; seen {
|
||||
continue
|
||||
}
|
||||
builder.WriteString(entry)
|
||||
builder.WriteByte('\n')
|
||||
added = true
|
||||
}
|
||||
|
||||
if !added {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(builder.String()), 0o644)
|
||||
}
|
||||
Reference in New Issue
Block a user