package pocketbase import ( "io" "os" "os/signal" "path/filepath" "strings" "syscall" "github.com/fatih/color" "github.com/pocketbase/pocketbase/cmd" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/list" "github.com/spf13/cobra" ) var _ core.App = (*PocketBase)(nil) // Version of PocketBase var Version = "(untracked)" // appWrapper serves as a private core.App instance wrapper. type appWrapper struct { core.App } // PocketBase defines a PocketBase app launcher. // // It implements [core.App] via embedding and all of the app interface methods // could be accessed directly through the instance (eg. PocketBase.DataDir()). type PocketBase struct { *appWrapper dataDirFlag string encryptionEnvFlag string hideStartBanner bool // RootCmd is the main console command RootCmd *cobra.Command } // Config is the PocketBase initialization config struct. type Config struct { // optional default values for the console flags DefaultDataDir string // if not set, it will fallback to "./pb_data" DefaultEncryptionEnv string // hide the default console server info on app startup HideStartBanner bool // optional DB configurations DataMaxOpenConns int // default to core.DefaultDataMaxOpenConns DataMaxIdleConns int // default to core.DefaultDataMaxIdleConns LogsMaxOpenConns int // default to core.DefaultLogsMaxOpenConns LogsMaxIdleConns int // default to core.DefaultLogsMaxIdleConns } // New creates a new PocketBase instance with the default configuration. // Use [NewWithConfig()] if you want to provide a custom configuration. // // Note that the application will not be initialized/bootstrapped yet, // aka. DB connections, migrations, app settings, etc. will not be accessible. // Everything will be initialized when [Start()] is executed. // If you want to initialize the application before calling [Start()], // then you'll have to manually call [Bootstrap()]. func New() *PocketBase { return NewWithConfig(Config{}) } // NewWithConfig creates a new PocketBase instance with the provided config. // // Note that the application will not be initialized/bootstrapped yet, // aka. DB connections, migrations, app settings, etc. will not be accessible. // Everything will be initialized when [Start()] is executed. // If you want to initialize the application before calling [Start()], // then you'll have to manually call [Bootstrap()]. func NewWithConfig(config Config) *PocketBase { // initialize a default data directory based on the executable baseDir if config.DefaultDataDir == "" { baseDir, _ := inspectRuntime() config.DefaultDataDir = filepath.Join(baseDir, "pb_data") } pb := &PocketBase{ RootCmd: &cobra.Command{ Use: filepath.Base(os.Args[0]), Short: "PocketBase CLI", Version: Version, FParseErrWhitelist: cobra.FParseErrWhitelist{ UnknownFlags: true, }, // no need to provide the default cobra completion command CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, }, dataDirFlag: config.DefaultDataDir, encryptionEnvFlag: config.DefaultEncryptionEnv, hideStartBanner: config.HideStartBanner, } // replace with a colored stderr writer pb.RootCmd.SetErr(newErrWriter()) // parse base flags // (errors are ignored, since the full flags parsing happens on Execute()) pb.eagerParseFlags(&config) // initialize the app instance pb.appWrapper = &appWrapper{core.NewBaseApp(core.BaseAppConfig{ DataDir: pb.dataDirFlag, EncryptionEnv: pb.encryptionEnvFlag, DataMaxOpenConns: config.DataMaxOpenConns, DataMaxIdleConns: config.DataMaxIdleConns, LogsMaxOpenConns: config.LogsMaxOpenConns, LogsMaxIdleConns: config.LogsMaxIdleConns, })} // hide the default help command (allow only `--help` flag) pb.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) return pb } // Start starts the application, aka. registers the default system // commands (serve, migrate, version) and executes pb.RootCmd. func (pb *PocketBase) Start() error { // register system commands pb.RootCmd.AddCommand(cmd.NewAdminCommand(pb)) pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, !pb.hideStartBanner)) return pb.Execute() } // Execute initializes the application (if not already) and executes // the pb.RootCmd with graceful shutdown support. // // This method differs from pb.Start() by not registering the default // system commands! func (pb *PocketBase) Execute() error { if !pb.skipBootstrap() { if err := pb.Bootstrap(); err != nil { return err } } 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 pb.RootCmd.Execute() done <- true }() <-done // trigger cleanups return pb.OnTerminate().Trigger(&core.TerminateEvent{ App: pb, }, func(e *core.TerminateEvent) error { return e.App.ResetBootstrapState() }) } // eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute(). // so we can have all PocketBase flags ready for use on initialization. func (pb *PocketBase) eagerParseFlags(config *Config) error { pb.RootCmd.PersistentFlags().StringVar( &pb.dataDirFlag, "dir", config.DefaultDataDir, "the PocketBase data directory", ) pb.RootCmd.PersistentFlags().StringVar( &pb.encryptionEnvFlag, "encryptionEnv", config.DefaultEncryptionEnv, "the env variable whose value of 32 characters will be used \nas encryption key for the app settings (default none)", ) return pb.RootCmd.ParseFlags(os.Args[1:]) } // skipBootstrap eagerly checks if the app should skip the bootstrap process: // - already bootstrapped // - is unknown command // - is the default help command // - is the default version command // // https://github.com/pocketbase/pocketbase/issues/404 // https://github.com/pocketbase/pocketbase/discussions/1267 func (pb *PocketBase) skipBootstrap() bool { flags := []string{ "-h", "--help", "-v", "--version", } if pb.IsBootstrapped() { return true // already bootstrapped } cmd, _, err := pb.RootCmd.Find(os.Args[1:]) if err != nil { return true // unknown command } for _, arg := range os.Args { if !list.ExistInSlice(arg, flags) { continue } // ensure that there is no user defined flag with the same name/shorthand trimmed := strings.TrimLeft(arg, "-") if len(trimmed) > 1 && cmd.Flags().Lookup(trimmed) == nil { return true } if len(trimmed) == 1 && cmd.Flags().ShorthandLookup(trimmed) == nil { return true } } return false } // 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 } // newErrWriter returns a red colored stderr writter. 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)) }