package backend import ( "datamanager/backend/cmd" "datamanager/backend/core" "github.com/fatih/color" "github.com/spf13/cobra" "io" "os" "os/signal" "path/filepath" "strings" "syscall" ) var _ core.App = (*Backend)(nil) // Version of Backend var Version = "(untracked)" type appWrapper struct { core.App } type Backend struct { *appWrapper devFlag bool dataDirFlag string encryptionEnvFlag string hideStartBanner bool // RootCmd is the main console command RootCmd *cobra.Command } func New() *Backend { _, isUsingGoRun := inspectRuntime() return NewWithConfig(Config{ DefaultDev: isUsingGoRun, DBUsername: "neo4j", DBPassword: "htwkalender-db", }) } // inspectRuntime tries to find the base executable directory and how it was run. func inspectRuntime() (baseDir string, withGoRun bool) { if strings.HasPrefix(os.Args[0], os.TempDir()) { // probably ran with go run withGoRun = true baseDir, _ = os.Getwd() } else { // probably ran with go build withGoRun = false baseDir = filepath.Dir(os.Args[0]) } return } type Config struct { // optional default values for the console flags DefaultDev bool DefaultDataDir string // if not set, it will fallback to "./pb_data" DBUsername string DBPassword string } func NewWithConfig(config Config) *Backend { // initialize a default data directory based on the executable baseDir if config.DefaultDataDir == "" { baseDir, _ := inspectRuntime() config.DefaultDataDir = filepath.Join(baseDir, "data") } backend := &Backend{ RootCmd: &cobra.Command{ Use: filepath.Base(os.Args[0]), Short: "Backend CLI", Version: Version, FParseErrWhitelist: cobra.FParseErrWhitelist{ UnknownFlags: true, }, // no need to provide the default cobra completion command CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, }, devFlag: config.DefaultDev, dataDirFlag: config.DefaultDataDir, } // replace with a colored stderr writer backend.RootCmd.SetErr(newErrWriter()) // parse base flags // (errors are ignored, since the full flags parsing happens on Execute()) _ = backend.eagerParseFlags(&config) // initialize the app instance backend.appWrapper = &appWrapper{core.NewBaseApp(core.BaseAppConfig{ IsDev: backend.devFlag, DataDir: backend.dataDirFlag, DBUsername: config.DBUsername, DBPassword: config.DBPassword, })} // hide the default help command (allow only `--help` flag) backend.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) return backend } func newErrWriter() *coloredWriter { return &coloredWriter{ w: os.Stderr, c: color.New(color.FgRed), } } // coloredWriter is a small wrapper struct to construct a [color.Color] writter. type coloredWriter struct { w io.Writer c *color.Color } // Write writes the p bytes using the colored writer. func (colored *coloredWriter) Write(p []byte) (n int, err error) { colored.c.SetWriter(colored.w) defer colored.c.UnsetWriter(colored.w) return colored.c.Print(string(p)) } func (backend *Backend) eagerParseFlags(config *Config) error { backend.RootCmd.PersistentFlags().StringVar( &backend.dataDirFlag, "dir", config.DefaultDataDir, "the PocketBase data directory", ) backend.RootCmd.PersistentFlags().BoolVar( &backend.devFlag, "dev", config.DefaultDev, "enable dev mode, aka. printing logs and cypher/sql statements to the console", ) return backend.RootCmd.ParseFlags(os.Args[1:]) } func (backend *Backend) Start() error { // register system commands backend.RootCmd.AddCommand(cmd.NewServeCommand(backend, !backend.hideStartBanner)) return backend.Execute() } func (backend *Backend) Execute() error { done := make(chan bool, 1) // listen for interrupt signal to gracefully shutdown the application go func() { sigch := make(chan os.Signal, 1) signal.Notify(sigch, os.Interrupt, syscall.SIGTERM) <-sigch done <- true }() // execute the root command go func() { // note: leave to the commands to decide whether to print their error _ = backend.RootCmd.Execute() done <- true }() <-done // trigger cleanups return backend.OnTerminate().Trigger(&core.TerminateEvent{ App: backend, }, func(e *core.TerminateEvent) error { return e.App.ResetBootstrapState() }) }