270 lines
5.8 KiB
Go
270 lines
5.8 KiB
Go
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)
|
|
}
|