Files
zoe-shaldon/server/cli.go
T
warkanum f96465b798 feat(cli): add CLI for managing RSVPs and photo uploads
* implement runCLI function to handle subcommands
* add runRSVPCLI and runPhotosCLI for RSVP and photo management
* include usage instructions for CLI commands
* integrate CLI execution in main function
2026-08-23 11:45:48 +02:00

172 lines
4.1 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"wedding-server/internal/store"
)
// runCLI handles admin subcommands (listing/removing bad RSVPs or photo
// uploads) so fixing a mistake doesn't require touching the database by hand.
// Returns the process exit code.
func runCLI(dataDir string, st *store.Store, args []string) int {
if len(args) == 0 {
printCLIUsage()
return 1
}
switch args[0] {
case "rsvp":
return runRSVPCLI(st, args[1:])
case "photos":
return runPhotosCLI(dataDir, st, args[1:])
case "help", "-h", "--help":
printCLIUsage()
return 0
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", args[0])
printCLIUsage()
return 1
}
}
func printCLIUsage() {
fmt.Println(`Usage:
wedding-server start the web server
wedding-server rsvp list list all RSVPs
wedding-server rsvp delete <id> delete an RSVP and its guests
wedding-server photos list list all photo uploads
wedding-server photos delete <id> delete a photo upload and its files`)
}
func runRSVPCLI(st *store.Store, args []string) int {
if len(args) == 0 {
printCLIUsage()
return 1
}
switch args[0] {
case "list":
rsvps, err := st.ListRSVPs()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}
if len(rsvps) == 0 {
fmt.Println("no RSVPs yet")
return 0
}
for _, r := range rsvps {
names := make([]string, len(r.Guests))
for i, g := range r.Guests {
names[i] = g.Name
if g.IsChild {
names[i] += " (child)"
}
}
fmt.Printf("#%d %s %s\n", r.ID, r.CreatedAt.Local().Format("2006-01-02 15:04"), strings.Join(names, ", "))
if r.Message != "" {
fmt.Printf(" message: %s\n", r.Message)
}
}
return 0
case "delete":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "usage: wedding-server rsvp delete <id>")
return 1
}
id, err := strconv.ParseInt(args[1], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid id %q\n", args[1])
return 1
}
if !confirm(fmt.Sprintf("Delete RSVP #%d? This cannot be undone.", id)) {
fmt.Println("cancelled")
return 0
}
if err := st.DeleteRSVP(id); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}
fmt.Printf("deleted RSVP #%d\n", id)
return 0
default:
printCLIUsage()
return 1
}
}
func runPhotosCLI(dataDir string, st *store.Store, args []string) int {
if len(args) == 0 {
printCLIUsage()
return 1
}
switch args[0] {
case "list":
uploads, err := st.ListPhotoUploads()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}
if len(uploads) == 0 {
fmt.Println("no photo uploads yet")
return 0
}
for _, u := range uploads {
fmt.Printf("#%d %s %s (%d photo(s))\n", u.ID, u.CreatedAt.Local().Format("2006-01-02 15:04"), u.Name, len(u.Files))
for _, f := range u.Files {
fmt.Printf(" %s\n", f.URL)
}
}
return 0
case "delete":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "usage: wedding-server photos delete <id>")
return 1
}
id, err := strconv.ParseInt(args[1], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid id %q\n", args[1])
return 1
}
if !confirm(fmt.Sprintf("Delete photo upload #%d and its files? This cannot be undone.", id)) {
fmt.Println("cancelled")
return 0
}
files, err := st.DeletePhotoUpload(id)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}
for _, f := range files {
if err := os.Remove(f.Path); err != nil && !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "warning: could not remove %s: %v\n", f.Path, err)
}
}
// Best-effort: only removes the per-upload directory if now empty.
_ = os.Remove(filepath.Join(dataDir, "photos", strconv.FormatInt(id, 10)))
fmt.Printf("deleted photo upload #%d (%d file(s))\n", id, len(files))
return 0
default:
printCLIUsage()
return 1
}
}
func confirm(prompt string) bool {
fmt.Printf("%s [y/N]: ", prompt)
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
line = strings.ToLower(strings.TrimSpace(line))
return line == "y" || line == "yes"
}