diff --git a/.gitignore b/.gitignore index a5f69e7..e03d5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ out/ temp/ dist/ .task/ -athenaeum +/athenaeum diff --git a/Taskfile.yaml b/Taskfile.yaml index 47e1b69..809ecb0 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -28,7 +28,12 @@ tasks: compile: desc: "Compiles for the current OS and architecture" cmds: - - go build -ldflags "-s -w" -o . ./... + - go build -ldflags "-s -w -X main.Version={{.VERSION}} -X main.Commit={{.COMMIT}} -X main.Date={{now | date "2006-01-02T15:04:05-0700"}}" -o . ./... + vars: + COMMIT: + sh: git rev-parse HEAD + VERSION: + sh: echo "$(git describe --tags)-dev+$(git rev-parse --short HEAD)" linux-compile: desc: "Compiles for Linux" diff --git a/cmd/athenaeum/cmd.go b/cmd/athenaeum/cmd.go new file mode 100644 index 0000000..f2ea635 --- /dev/null +++ b/cmd/athenaeum/cmd.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/CallumKerson/Athenaeum/internal/adapters/bolt" + "github.com/CallumKerson/Athenaeum/internal/adapters/logrus" + audiobooksService "github.com/CallumKerson/Athenaeum/internal/audiobooks/service" + mediaService "github.com/CallumKerson/Athenaeum/internal/media/service" + podcastService "github.com/CallumKerson/Athenaeum/internal/podcasts/service" + transportHttp "github.com/CallumKerson/Athenaeum/internal/transport/http" +) + +const ( + shortHelp = "an audiobook server that provides a podcast feed" +) + +func main() { + cmd := NewRootCommand() + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} + +// Build the cobra command that handles our command line tool. +func NewRootCommand() *cobra.Command { + pathToConfig := "" + var cfg Config + + // Define our command + rootCmd := &cobra.Command{ + Use: "athenaeum", + Short: shortHelp, + PreRunE: func(cmd *cobra.Command, args []string) error { + return InitConfig(&cfg, pathToConfig, cmd.OutOrStderr()) + }, + RunE: func(cmd *cobra.Command, args []string) error { + logger := logrus.NewLogger() + setLogLevel(logger, cfg.GetLogLevel()) + mediaSvc := mediaService.New(logger, cfg.GetMediaServiceOpts()...) + boltAudiobookStore, err := bolt.NewAudiobookStore(logger, true, cfg.GetBoltDBOps()...) + if err != nil { + return err + } + audiobookSvc := audiobooksService.New(mediaSvc, boltAudiobookStore, logger) + if errScan := audiobookSvc.UpdateAudiobooks(context.Background()); errScan != nil { + return errScan + } + podcastSvc := podcastService.New(audiobookSvc, logger, cfg.GetPodcastServiceOpts()...) + httpHandler := transportHttp.NewHandler(podcastSvc, audiobookSvc, logger, cfg.GetHTTPHandlerOpts()...) + + return transportHttp.Serve(httpHandler, cfg.Port, logger) + }, + } + + // Define cobra flags, the default value has the lowest (least significant) precedence + rootCmd.PersistentFlags().StringVarP(&pathToConfig, "config", "c", pathToConfig, "path to config file") + rootCmd.SilenceUsage = true + + rootCmd.Version = Version + + rootCmd.AddCommand(NewVersionCommand()) + return rootCmd +} + +func setLogLevel(logger *logrus.Logger, level string) { + level = strings.ToLower(level) + switch level { + case "debug": + logger.SetLevelDebug() + case "warn": + logger.SetLevelWarn() + case "error": + logger.SetLevelError() + default: + logger.SetLevelInfo() + } +} diff --git a/cmd/athenaeum/config.go b/cmd/athenaeum/config.go index 9fc5baa..12ad458 100644 --- a/cmd/athenaeum/config.go +++ b/cmd/athenaeum/config.go @@ -2,21 +2,21 @@ package main import ( "fmt" - "path/filepath" + "io" "strings" "github.com/spf13/viper" - "github.com/CallumKerson/loggerrific" - - "github.com/CallumKerson/Athenaeum/internal/adapters/bolt" - mediaService "github.com/CallumKerson/Athenaeum/internal/media/service" - podcastService "github.com/CallumKerson/Athenaeum/internal/podcasts/service" transportHttp "github.com/CallumKerson/Athenaeum/internal/transport/http" ) +const ( + defaultPort = 8080 +) + type Config struct { Host string + Port int DB DB Media Media Podcast Podcast @@ -37,7 +37,6 @@ type Media struct { } type Podcast struct { - Root string Copyright string Explicit bool Language string @@ -45,24 +44,6 @@ type Podcast struct { Email string } -func (c *Config) GetMediaHost() string { - return fmt.Sprintf("%s/%s", c.Host, c.Media.HostPath) -} - -func (c *Config) GetMediaServiceOpts() []mediaService.Option { - return []mediaService.Option{mediaService.WithPathToMediaRoot(c.Media.Root)} -} - -func (c *Config) GetBoltDBOps() []bolt.Option { - return []bolt.Option{bolt.WithDBDefaults(), bolt.WithPathToDBDirectory(c.DB.Root)} -} - -func (c *Config) GetPodcastServiceOpts() []podcastService.Option { - return []podcastService.Option{podcastService.WithHost(c.Host), - podcastService.WithMediaPath(c.Media.HostPath), - podcastService.WithPodcastFeedInfo(c.Podcast.Explicit, c.Podcast.Language, c.Podcast.Author, c.Podcast.Email, c.Podcast.Copyright)} -} - func (c *Config) GetLogLevel() string { return c.Log.Level } @@ -71,15 +52,15 @@ func (c *Config) GetHTTPHandlerOpts() []transportHttp.HandlerOption { return []transportHttp.HandlerOption{transportHttp.WithMediaConfig(c.Media.Root, c.Media.HostPath), transportHttp.WithVersion(Version)} } -func NewConfig(port int, logger loggerrific.Logger) (*Config, error) { +func InitConfig(cfg *Config, pathToConfigFile string, out io.Writer) error { viper.SetDefault("Podcast.Copyright", "None") viper.SetDefault("Podcast.Explicit", true) viper.SetDefault("Podcast.Language", "EN") - viper.SetDefault("Podcast.Root", "/srv/podcasts") viper.SetDefault("Media.HostPath", "/media") viper.SetDefault("Media.Root", "/srv/media") viper.SetDefault("DB.Root", "/usr/local/athenaeum") - viper.SetDefault("Host", fmt.Sprintf("http://localhost:%d", port)) + viper.SetDefault("Port", defaultPort) + viper.SetDefault("Host", fmt.Sprintf("http://localhost:%d", defaultPort)) viper.SetDefault("Log.Level", "INFO") replacer := strings.NewReplacer(".", "_") @@ -100,21 +81,21 @@ func NewConfig(port int, logger loggerrific.Logger) (*Config, error) { viper.AutomaticEnv() - pathToConfig := viper.GetString("Config.Path") + if pathToConfigFile == "" { + pathToConfigFile = viper.GetString("Config.Path") + } - if !filepath.IsAbs(pathToConfig) { - logger.Infoln("No valid config path found from environment variable ATHENAEUM_CONFIG_PATH,", + if pathToConfigFile == "" { + fmt.Fprintln(out, "No valid config path provided by flag or found from environment variable ATHENAEUM_CONFIG_PATH,", "reading config from environment variables only") } else { - viper.SetConfigFile(pathToConfig) + viper.SetConfigFile(pathToConfigFile) err := viper.ReadInConfig() if err != nil { - logger.WithError(err).Errorln("Cannot read config from file") - return nil, err + return fmt.Errorf("config error: %w", err) } } - var cfg Config - err := viper.Unmarshal(&cfg) - return &cfg, err + err := viper.Unmarshal(cfg) + return err } diff --git a/cmd/athenaeum/config_opts.go b/cmd/athenaeum/config_opts.go new file mode 100644 index 0000000..9825860 --- /dev/null +++ b/cmd/athenaeum/config_opts.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + + "github.com/CallumKerson/Athenaeum/internal/adapters/bolt" + mediaService "github.com/CallumKerson/Athenaeum/internal/media/service" + podcastService "github.com/CallumKerson/Athenaeum/internal/podcasts/service" +) + +func (c *Config) GetMediaHost() string { + return fmt.Sprintf("%s/%s", c.Host, c.Media.HostPath) +} + +func (c *Config) GetMediaServiceOpts() []mediaService.Option { + return []mediaService.Option{mediaService.WithPathToMediaRoot(c.Media.Root)} +} + +func (c *Config) GetBoltDBOps() []bolt.Option { + return []bolt.Option{bolt.WithDBDefaults(), bolt.WithPathToDBDirectory(c.DB.Root)} +} + +func (c *Config) GetPodcastServiceOpts() []podcastService.Option { + return []podcastService.Option{podcastService.WithHost(c.Host), + podcastService.WithMediaPath(c.Media.HostPath), + podcastService.WithPodcastFeedInfo(c.Podcast.Explicit, c.Podcast.Language, c.Podcast.Author, c.Podcast.Email, c.Podcast.Copyright)} +} diff --git a/cmd/athenaeum/config_test.go b/cmd/athenaeum/config_test.go index 96c2491..6a1011c 100644 --- a/cmd/athenaeum/config_test.go +++ b/cmd/athenaeum/config_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "io/fs" "os" "path/filepath" @@ -8,8 +9,6 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" - - "github.com/CallumKerson/loggerrific/tlogger" ) func TestConfig_FromEnvironment(t *testing.T) { @@ -20,9 +19,10 @@ func TestConfig_FromEnvironment(t *testing.T) { }) t.Cleanup(envVarCleanup) viper.Reset() + var config Config // when - config, err := NewConfig(defaultPort, tlogger.NewTLogger(t)) + err := InitConfig(&config, "", &bytes.Buffer{}) // then assert.NoError(t, err) @@ -31,6 +31,29 @@ func TestConfig_FromEnvironment(t *testing.T) { } func TestConfig_FromFile(t *testing.T) { + // given + configFilePath := filepath.Join(t.TempDir(), "config.yaml") + viper.Reset() + + configYAML := `--- +Host: "http://localhost:8088" +Podcast: + Language: FR +` + err := os.WriteFile(configFilePath, []byte(configYAML), 0644) + assert.NoError(t, err) + var config Config + + // when + err = InitConfig(&config, configFilePath, &bytes.Buffer{}) + + // then + assert.NoError(t, err) + assert.Equal(t, "http://localhost:8088", config.Host) + assert.Equal(t, "FR", config.Podcast.Language) +} + +func TestConfig_FromFile_DefiniedInEnvironment(t *testing.T) { // given configFilePath := filepath.Join(t.TempDir(), "config.yaml") envVarCleanup := envVarSetter(t, map[string]string{ @@ -46,9 +69,10 @@ Podcast: ` err := os.WriteFile(configFilePath, []byte(configYAML), 0644) assert.NoError(t, err) + var config Config // when - config, err := NewConfig(defaultPort, tlogger.NewTLogger(t)) + err = InitConfig(&config, "", &bytes.Buffer{}) // then assert.NoError(t, err) @@ -71,9 +95,10 @@ Host: "http://localhost:8083" ` err := os.WriteFile(configFilePath, []byte(configYAML), 0644) assert.NoError(t, err) + var config Config // when - config, err := NewConfig(defaultPort, tlogger.NewTLogger(t)) + err = InitConfig(&config, "", &bytes.Buffer{}) // then assert.NoError(t, err) @@ -83,9 +108,10 @@ Host: "http://localhost:8083" func TestConfig_DefaultsOnly(t *testing.T) { // given viper.Reset() + var config Config // when - config, err := NewConfig(defaultPort, tlogger.NewTLogger(t)) + err := InitConfig(&config, "", &bytes.Buffer{}) // then assert.NoError(t, err) @@ -106,9 +132,10 @@ func TestConfig_BadFile(t *testing.T) { }) t.Cleanup(envVarCleanup) viper.Reset() + var config Config // when - _, err := NewConfig(defaultPort, tlogger.NewTLogger(t)) + err := InitConfig(&config, "", &bytes.Buffer{}) // then if assert.Error(t, err) { diff --git a/cmd/athenaeum/main.go b/cmd/athenaeum/main.go deleted file mode 100644 index 4d193cf..0000000 --- a/cmd/athenaeum/main.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "strings" - - flag "github.com/spf13/pflag" - - "github.com/CallumKerson/loggerrific" - - "github.com/CallumKerson/Athenaeum/internal/adapters/bolt" - "github.com/CallumKerson/Athenaeum/internal/adapters/logrus" - audiobooksService "github.com/CallumKerson/Athenaeum/internal/audiobooks/service" - mediaService "github.com/CallumKerson/Athenaeum/internal/media/service" - podcastService "github.com/CallumKerson/Athenaeum/internal/podcasts/service" - transportHttp "github.com/CallumKerson/Athenaeum/internal/transport/http" -) - -const ( - defaultPort = 8080 -) - -var ( - Version = "development" - Commit = "development" - Date = "development" -) - -func Run(cfg *Config, port int, logger loggerrific.Logger) error { - mediaSvc := mediaService.New(logger, cfg.GetMediaServiceOpts()...) - boltAudiobookStore, err := bolt.NewAudiobookStore(logger, true, cfg.GetBoltDBOps()...) - if err != nil { - return err - } - audiobookSvc := audiobooksService.New(mediaSvc, boltAudiobookStore, logger) - if errScan := audiobookSvc.UpdateAudiobooks(context.Background()); errScan != nil { - return errScan - } - podcastSvc := podcastService.New(audiobookSvc, logger, cfg.GetPodcastServiceOpts()...) - httpHandler := transportHttp.NewHandler(podcastSvc, audiobookSvc, logger, cfg.GetHTTPHandlerOpts()...) - - return Serve(httpHandler, port, logger) -} - -func main() { - showVersion := flag.BoolP("version", "v", false, "prints version and exits") - flag.Parse() - if *showVersion { - fmt.Println("version: ", Version) - fmt.Println("commit: ", Commit) - fmt.Println("built at:", Date) - return - } - - logger := logrus.NewLogger() - cfg, err := NewConfig(defaultPort, logger) - if err != nil { - logger.WithError(err).Errorln("Error getting config") - os.Exit(1) - } - setLogLevel(logger, cfg.GetLogLevel()) - - if err := Run(cfg, defaultPort, logger); err != nil { - logger.WithError(err).Errorln("Error starting up server") - os.Exit(1) - } -} - -func setLogLevel(logger *logrus.Logger, level string) { - level = strings.ToLower(level) - switch level { - case "debug": - logger.SetLevelDebug() - case "info": - logger.SetLevelInfo() - case "warn": - logger.SetLevelWarn() - case "error": - logger.SetLevelError() - } -} diff --git a/cmd/athenaeum/version.go b/cmd/athenaeum/version.go new file mode 100644 index 0000000..251c3a1 --- /dev/null +++ b/cmd/athenaeum/version.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + Version = "development" + Commit = "development" + Date = "development" +) + +func NewVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Prints version and build information", + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(cmd.OutOrStdout(), "version: ", Version) + fmt.Fprintln(cmd.OutOrStdout(), "commit: ", Commit) + fmt.Fprintln(cmd.OutOrStdout(), "built at:", Date) + }, + } +} diff --git a/go.mod b/go.mod index 56d9057..e873add 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.7 github.com/shopspring/decimal v1.3.1 github.com/sirupsen/logrus v1.9.0 - github.com/spf13/pflag v1.0.5 + github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.2 go.etcd.io/bbolt v1.3.7 @@ -22,12 +22,14 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.5.0 // indirect diff --git a/go.sum b/go.sum index a7b4782..880a0f1 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= @@ -132,6 +133,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -154,6 +157,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= @@ -162,6 +166,8 @@ github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/cmd/athenaeum/serve.go b/internal/transport/http/serve.go similarity index 98% rename from cmd/athenaeum/serve.go rename to internal/transport/http/serve.go index 4d6c617..0e38259 100644 --- a/cmd/athenaeum/serve.go +++ b/internal/transport/http/serve.go @@ -1,4 +1,4 @@ -package main +package http import ( "context"