mirror of
https://github.com/therootcompany/golib.git
synced 2026-02-20 10:48:04 +00:00
feat: add cmd/api-example as server boilerplate
This commit is contained in:
parent
3700770144
commit
614af86976
53
cmd/api-example/args.go
Normal file
53
cmd/api-example/args.go
Normal file
@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func peekOption(args []string, flags []string, defaultOpt string) string {
|
||||
n := len(args)
|
||||
for i := range n {
|
||||
if slices.Contains(flags, args[i]) {
|
||||
if i+1 < n {
|
||||
return args[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return defaultOpt
|
||||
}
|
||||
|
||||
func parseEnvs(opts *MainConfig) error {
|
||||
if envPort := os.Getenv("PORT"); envPort != "" {
|
||||
if p, err := strconv.Atoi(envPort); err == nil && p > 0 {
|
||||
opts.defaultPort = p
|
||||
} else {
|
||||
return fmt.Errorf("invalid PORT environment variable value: %q", envPort)
|
||||
}
|
||||
}
|
||||
if envAddress := os.Getenv("ADDRESS"); envAddress != "" {
|
||||
if _, err := netip.ParseAddr(envAddress); err != nil {
|
||||
return fmt.Errorf("invalid ADDRESS environment variable value: %q", envAddress)
|
||||
}
|
||||
opts.defaultAddress = envAddress
|
||||
}
|
||||
|
||||
if opts.pgURL = os.Getenv("PG_URL"); opts.pgURL != "" {
|
||||
if _, err := url.Parse(opts.pgURL); err != nil {
|
||||
return fmt.Errorf("invalid PG_URL environment variable value: %q", opts.pgURL)
|
||||
}
|
||||
}
|
||||
if opts.jwtInspectURL = os.Getenv("JWT_INSPECT_URL"); opts.jwtInspectURL != "" {
|
||||
if _, err := url.Parse(opts.jwtInspectURL); err != nil {
|
||||
return fmt.Errorf("invalid JWT_INSPECT_URL environment variable value: %q", opts.jwtInspectURL)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
cmd/api-example/git.go
Normal file
42
cmd/api-example/git.go
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func maybeGetVersion() string {
|
||||
// Try git describe for tag + commits since tag
|
||||
args := []string{"describe", "--tags", "--abbrev=7", "--dirty=+local", "--always"}
|
||||
if out, err := exec.Command("git", args...).Output(); err == nil {
|
||||
return strings.TrimSpace(strings.TrimPrefix(string(out), "v"))
|
||||
}
|
||||
|
||||
return "0.0.0-dev"
|
||||
}
|
||||
|
||||
func maybeGetCommit() string {
|
||||
// Try git rev-parse for short commit hash
|
||||
if out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output(); err == nil {
|
||||
if out, err := exec.Command("git", "status", "--porcelain").Output(); err == nil && len(out) == 0 {
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
return strings.TrimSpace(string(out)) + "+dev"
|
||||
}
|
||||
return "0000000"
|
||||
}
|
||||
|
||||
func maybeGetDate() string {
|
||||
// Get timestamp of most recent commit, if clean
|
||||
if out, err := exec.Command("git", "status", "--porcelain").Output(); err == nil && len(out) == 0 {
|
||||
if out, err := exec.Command("git", "log", "-1", "--format=%ci").Output(); err == nil {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05 -0700", strings.TrimSpace(string(out))); err == nil {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return current day with 0s for hour, minute, second
|
||||
return time.Now().UTC().Truncate(24 * time.Hour).Format(time.RFC3339)
|
||||
}
|
||||
17
cmd/api-example/go.mod
Normal file
17
cmd/api-example/go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module github.com/therootcompany/golib/cmd/api-example
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/therootcompany/golib v0.0.0-20260215001229-3700770144be
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
30
cmd/api-example/go.sum
Normal file
30
cmd/api-example/go.sum
Normal file
@ -0,0 +1,30 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/therootcompany/golib v0.0.0-20260215001229-3700770144be h1:RpIFiKHN7yM2yj1kSjJh1SMTIBGEE7yxR7CRVTQgZYY=
|
||||
github.com/therootcompany/golib v0.0.0-20260215001229-3700770144be/go.mod h1:BWCvTz1AYl4S+vZD/I/gt7i7yJYy3yU9sLX/3HqqcNA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
41
cmd/api-example/internal/duration.go
Normal file
41
cmd/api-example/internal/duration.go
Normal file
@ -0,0 +1,41 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FormatDuration(d time.Duration) string {
|
||||
if d < 0 {
|
||||
d = -d
|
||||
}
|
||||
days := int(d / (24 * time.Hour))
|
||||
d -= time.Duration(days) * 24 * time.Hour
|
||||
hours := int(d / time.Hour)
|
||||
d -= time.Duration(hours) * time.Hour
|
||||
minutes := int(d / time.Minute)
|
||||
d -= time.Duration(minutes) * time.Minute
|
||||
seconds := int(d / time.Second)
|
||||
|
||||
var parts []string
|
||||
if days > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%dd", days))
|
||||
}
|
||||
if hours > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%dh", hours))
|
||||
}
|
||||
if minutes > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%dm", minutes))
|
||||
}
|
||||
if seconds > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%ds", seconds))
|
||||
}
|
||||
if seconds == 0 || len(parts) == 0 {
|
||||
d -= time.Duration(seconds) * time.Second
|
||||
millis := int(d / time.Millisecond)
|
||||
parts = append(parts, fmt.Sprintf("%dms", millis))
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
19
cmd/api-example/internal/proxy.go
Normal file
19
cmd/api-example/internal/proxy.go
Normal file
@ -0,0 +1,19 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func ProxyToOtherAPI(proxyTarget string) http.Handler {
|
||||
target, _ := url.Parse(proxyTarget)
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
// r.SetXForwarded() // these are trusted from the tls-terminating proxy
|
||||
r.SetURL(target)
|
||||
r.Out.Host = r.In.Host
|
||||
},
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
60
cmd/api-example/internal/routes.go
Normal file
60
cmd/api-example/internal/routes.go
Normal file
@ -0,0 +1,60 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/therootcompany/golib/cmd/api-example/db"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
BootTime time.Time
|
||||
StartTime time.Time
|
||||
PG *pgxpool.Pool
|
||||
Queries *db.Queries
|
||||
}
|
||||
|
||||
type APIStatus struct {
|
||||
SystemSeconds float64 `json:"system_seconds"`
|
||||
SystemUptime string `json:"system_uptime"`
|
||||
APISeconds float64 `json:"api_seconds"`
|
||||
APIUptime string `json:"api_uptime"`
|
||||
}
|
||||
|
||||
type Greeting struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (a *API) HandleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
systemUptime := time.Since(a.BootTime)
|
||||
apiUptime := time.Since(a.StartTime)
|
||||
apiStatus := APIStatus{
|
||||
SystemSeconds: systemUptime.Seconds(),
|
||||
SystemUptime: FormatDuration(systemUptime),
|
||||
APISeconds: apiUptime.Seconds(),
|
||||
APIUptime: FormatDuration(apiUptime),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
_ = enc.Encode(apiStatus)
|
||||
}
|
||||
|
||||
func (a *API) HandleGreet(w http.ResponseWriter, r *http.Request) {
|
||||
subject := r.PathValue("subject")
|
||||
if subject == "" {
|
||||
subject = "World"
|
||||
}
|
||||
|
||||
msg := Greeting{
|
||||
Message: fmt.Sprintf("Hello, %s!", subject),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
_ = enc.Encode(msg)
|
||||
}
|
||||
248
cmd/api-example/main.go
Normal file
248
cmd/api-example/main.go
Normal file
@ -0,0 +1,248 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/therootcompany/golib/cmd/api-example/db"
|
||||
"github.com/therootcompany/golib/cmd/api-example/internal"
|
||||
"github.com/therootcompany/golib/crypto/passphrase"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "CHANGE_ME"
|
||||
licenseYear = "2025"
|
||||
licenseOwner = "AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)"
|
||||
licenseType = "LICENSE in LICENSE"
|
||||
)
|
||||
|
||||
// for goreleaser
|
||||
var (
|
||||
version = ""
|
||||
commit = ""
|
||||
date = ""
|
||||
)
|
||||
|
||||
var (
|
||||
newHTTPServer func(context.Context, string) (*http.Server, error)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// workaround for `tinygo` ldflag replacement handling not allowing default values
|
||||
// See <https://github.com/tinygo-org/tinygo/issues/2976>
|
||||
if len(version) == 0 {
|
||||
version = maybeGetVersion() // defaults to "0.0.0-dev"
|
||||
}
|
||||
if len(date) == 0 {
|
||||
date = maybeGetDate() // defaults to date-only "20xx-01-00 00:00:00"
|
||||
}
|
||||
if len(commit) == 0 {
|
||||
commit = maybeGetCommit() // defaults to "0000000"
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion displays the version, commit, and build date.
|
||||
func printVersion(w io.Writer) {
|
||||
_, _ = fmt.Fprintf(w, "%s v%s %s (%s)\n", name, version, commit[:7], date)
|
||||
_, _ = fmt.Fprintf(w, "Copyright (C) %s %s\n", licenseYear, licenseOwner)
|
||||
_, _ = fmt.Fprintf(w, "Licensed under the %s license\n", licenseType)
|
||||
}
|
||||
|
||||
type MainConfig struct {
|
||||
defaultAddress string
|
||||
defaultPort int
|
||||
defaultProxyTarget string
|
||||
address string
|
||||
port int
|
||||
proxyTarget string
|
||||
showVersion bool
|
||||
pgURL string
|
||||
encryptionPassphrase string
|
||||
encryptionSalt string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := MainConfig{
|
||||
defaultAddress: "0.0.0.0",
|
||||
defaultPort: 3080,
|
||||
defaultProxyTarget: "127.0.0.1:3081",
|
||||
}
|
||||
|
||||
var envErr error
|
||||
{
|
||||
envPath := peekOption(os.Args[1:], []string{"-envfile", "--envfile"}, ".env")
|
||||
|
||||
if err := godotenv.Load(envPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
envErr = err
|
||||
}
|
||||
} else if err := parseEnvs(&cfg); err != nil {
|
||||
printVersion(os.Stderr)
|
||||
fmt.Fprintf(os.Stderr, "\nerror: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// note: --help is implicit, but handled specially below
|
||||
mainFlags := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
mainFlags.BoolVar(&cfg.showVersion, "version", false, "Show version and exit")
|
||||
mainFlags.IntVar(&cfg.port, "port", cfg.defaultPort, "Port to listen on")
|
||||
_ = mainFlags.String("envfile", ".env", "Load ENVs from this file")
|
||||
mainFlags.StringVar(&cfg.pgURL, "pg-url", cfg.pgURL, "Postgres URL such as postgres://postgres@localhost:5432/postgres")
|
||||
mainFlags.StringVar(&cfg.proxyTarget, "proxy-target", cfg.defaultProxyTarget, "Proxy unhandled requests to this target")
|
||||
mainFlags.StringVar(&cfg.address, "address", cfg.defaultAddress, "Address to bind to")
|
||||
|
||||
flagOut := os.Stderr
|
||||
mainFlags.Usage = func() {
|
||||
_, _ = fmt.Fprintf(flagOut, "USAGE\n")
|
||||
_, _ = fmt.Fprintf(flagOut, " CHANGE_ME [options]\n")
|
||||
_, _ = fmt.Fprintf(flagOut, "\n")
|
||||
_, _ = fmt.Fprintf(flagOut, "EXAMPLES\n")
|
||||
_, _ = fmt.Fprintf(flagOut, " CHANGE_ME --address 0.0.0.0 --port 443\n")
|
||||
_, _ = fmt.Fprintf(flagOut, "\n")
|
||||
_, _ = fmt.Fprintf(flagOut, "OPTIONS\n")
|
||||
mainFlags.PrintDefaults()
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "-V", "version", "-version", "--version":
|
||||
printVersion(os.Stdout)
|
||||
os.Exit(0)
|
||||
return
|
||||
case "help", "-help", "--help":
|
||||
printVersion(os.Stdout)
|
||||
_, _ = fmt.Fprintf(os.Stdout, "\n")
|
||||
|
||||
flagOut = os.Stdout
|
||||
mainFlags.SetOutput(flagOut)
|
||||
mainFlags.Usage()
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
}
|
||||
printVersion(os.Stderr)
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
|
||||
if err := mainFlags.Parse(os.Args[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
|
||||
mainFlags.Usage()
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if envErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not read .env: %s", envErr)
|
||||
}
|
||||
|
||||
cfg.encryptionPassphrase = os.Getenv("APP_BIP39_PASSPHRASE")
|
||||
cfg.encryptionSalt = os.Getenv("APP_BIP39_SALT")
|
||||
run(&cfg)
|
||||
}
|
||||
|
||||
func run(cfg *MainConfig) {
|
||||
var err error
|
||||
boottime, err := maybeGetUptime()
|
||||
if err != nil {
|
||||
log.Printf("could not get server uptime: %s", err)
|
||||
}
|
||||
|
||||
// TODO: add signal handling for graceful shutdowns and restarts
|
||||
newHTTPServer = func(ctx context.Context, addr string) (*http.Server, error) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
seed, _ := passphrase.SeedFrom(cfg.encryptionPassphrase, cfg.encryptionSalt)
|
||||
sensitiveKeyHex := hex.EncodeToString(seed)
|
||||
pgPool, err := configDB(cfg, ctx, sensitiveKeyHex)
|
||||
if err != nil {
|
||||
log.Fatalf("Error while creating new pgxpool: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
pgPool.Close()
|
||||
}()
|
||||
|
||||
api := &internal.API{
|
||||
BootTime: boottime,
|
||||
StartTime: time.Now(),
|
||||
PG: pgPool,
|
||||
Queries: db.New(pgPool),
|
||||
}
|
||||
mux.HandleFunc("GET /api/status", api.HandleStatus)
|
||||
mux.HandleFunc("GET /api/hello", api.HandleGreet)
|
||||
mux.HandleFunc("GET /api/hello/{subject}", api.HandleGreet)
|
||||
|
||||
proxy := internal.ProxyToOtherAPI(cfg.proxyTarget)
|
||||
mux.HandleFunc("OPTIONS /api/", proxy.ServeHTTP)
|
||||
mux.HandleFunc("GET /api/", proxy.ServeHTTP)
|
||||
mux.HandleFunc("POST /api/", proxy.ServeHTTP)
|
||||
mux.HandleFunc("PUT /api/", proxy.ServeHTTP)
|
||||
mux.HandleFunc("DELETE /api/", proxy.ServeHTTP)
|
||||
|
||||
// allow http/1.1 and h2 from tls-terminating proxy
|
||||
protocols := &http.Protocols{}
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 10 * time.Second, // still needs per-request ReadTimeout
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: (30 + 1) * time.Second,
|
||||
MaxHeaderBytes: 1 << 14, // 2^14 = 16k
|
||||
Protocols: protocols,
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", cfg.address, cfg.port)
|
||||
server, err := newHTTPServer(context.TODO(), addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Listening for http on %s\n", addr)
|
||||
err = server.ListenAndServe()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
func configDB(cfg *MainConfig, ctx context.Context, sensitiveKeyHex string) (*pgxpool.Pool, error) {
|
||||
pgConfig, err := pgxpool.ParseConfig(cfg.pgURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Error while parsing PG_URL: %s", err)
|
||||
}
|
||||
|
||||
// This runs for every **new** connection created by the pool
|
||||
pgConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||
// Escape the hex string properly for PostgreSQL bytea literal
|
||||
// \\x + hex digits is the standard way
|
||||
query := fmt.Sprintf("SET my.sensitive_data_key = '\\x%s';", sensitiveKeyHex)
|
||||
|
||||
// Use Exec (no parameters needed here)
|
||||
_, err := conn.Exec(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set my.sensitive_data_key: %w", err)
|
||||
}
|
||||
|
||||
// Optional: verify it (for debugging)
|
||||
// var val string
|
||||
// err = conn.QueryRow(ctx, "SHOW my.sensitive_data_key").Scan(&val)
|
||||
// log.Printf("After set: %s", val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return pgxpool.NewWithConfig(ctx, pgConfig)
|
||||
}
|
||||
1
cmd/api-example/sql/db.go
Normal file
1
cmd/api-example/sql/db.go
Normal file
@ -0,0 +1 @@
|
||||
package db
|
||||
@ -0,0 +1,3 @@
|
||||
DELETE FROM _migrations WHERE id = '00000001';
|
||||
|
||||
DROP TABLE IF EXISTS _migrations;
|
||||
@ -0,0 +1,14 @@
|
||||
-- Config variables for sql-migrate (do not delete)
|
||||
-- sql_command: psql "$PG_URL" -v ON_ERROR_STOP=on -A -t --file %s
|
||||
-- migrations_log: ./db/paperdb-migrations.log
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id CHAR(8) PRIMARY KEY,
|
||||
name VARCHAR(80) UNIQUE NOT NULL,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- note: to enable text-based tools to grep and sort we put 'name' before 'id'
|
||||
-- grep -r 'INSERT INTO _migrations' ./sql/migrations/ | cut -d':' -f2 | sort
|
||||
INSERT INTO _migrations (name, id) VALUES ('0001-01-01-01000_init-migrations', '00000001');
|
||||
@ -0,0 +1,4 @@
|
||||
-- add-blobs-table (down)
|
||||
DROP TABLE IF EXISTS "blobs";
|
||||
|
||||
DELETE FROM _migrations WHERE id = '45249baf';
|
||||
@ -0,0 +1,10 @@
|
||||
INSERT INTO _migrations (name, id) VALUES ('2026-01-01-001000_add-ai-blobs-table', '45249baf');
|
||||
|
||||
-- add-blobs-table (up)
|
||||
CREATE TABLE "blobs" (
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"key" VARCHAR(100) NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
PRIMARY KEY ("key")
|
||||
)
|
||||
4
cmd/api-example/sql/queries/blobs.sql
Normal file
4
cmd/api-example/sql/queries/blobs.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- name: BlobsAll :many
|
||||
SELECT *
|
||||
FROM blobs
|
||||
ORDER BY key;
|
||||
17
cmd/api-example/sqlc.yaml
Normal file
17
cmd/api-example/sqlc.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
schema: "./sql/migrations/"
|
||||
queries:
|
||||
- "./sql/queries/"
|
||||
gen:
|
||||
go:
|
||||
package: "db"
|
||||
out: "./db"
|
||||
sql_package: "pgx/v5"
|
||||
emit_json_tags: true
|
||||
overrides:
|
||||
- db_type: jsonb
|
||||
go_type: "encoding/json.RawMessage"
|
||||
- column: "blobs.value"
|
||||
go_type: "encoding/json.RawMessage"
|
||||
36
cmd/api-example/uptime.go
Normal file
36
cmd/api-example/uptime.go
Normal file
@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func maybeGetUptime() (time.Time, error) {
|
||||
out, err := exec.Command("uptime").Output()
|
||||
if err != nil {
|
||||
return time.Now(), fmt.Errorf("uptime command failed: %s\n%w", out, err)
|
||||
}
|
||||
// Parse uptime output (e.g., "up 1 day, 2:34" or "up 2:34")
|
||||
re := regexp.MustCompile(`up\s+(?:(\d+)\s+days?,?\s+)?(?:(\d+):)?(\d+)`)
|
||||
matches := re.FindStringSubmatch(string(out))
|
||||
if len(matches) < 2 {
|
||||
return time.Now(), fmt.Errorf("invalid uptime format")
|
||||
}
|
||||
|
||||
var seconds int64
|
||||
if days, err := strconv.Atoi(matches[1]); err == nil && matches[1] != "" {
|
||||
seconds += int64(days) * 24 * 3600
|
||||
}
|
||||
if hours, err := strconv.Atoi(matches[2]); err == nil && matches[2] != "" {
|
||||
seconds += int64(hours) * 3600
|
||||
}
|
||||
if minutes, err := strconv.Atoi(matches[3]); err == nil {
|
||||
seconds += int64(minutes) * 60
|
||||
}
|
||||
|
||||
duration := time.Duration(seconds) * time.Second
|
||||
return time.Now().Add(-duration), nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user