aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/neko/main.go152
-rw-r--r--cmd/neko/main_test.go124
2 files changed, 276 insertions, 0 deletions
diff --git a/cmd/neko/main.go b/cmd/neko/main.go
new file mode 100644
index 0000000..47385b1
--- /dev/null
+++ b/cmd/neko/main.go
@@ -0,0 +1,152 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "adammathes.com/neko/config"
+ "adammathes.com/neko/internal/crawler"
+ "adammathes.com/neko/internal/exporter"
+ "adammathes.com/neko/models"
+ "adammathes.com/neko/models/feed"
+
+ "flag"
+
+ "adammathes.com/neko/internal/vlog"
+ "adammathes.com/neko/web"
+)
+
+var Version, Build string
+
+func main() {
+ if err := Run(os.Args[1:]); err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+}
+
+func Run(args []string) error {
+ var help, update, verbose, proxyImages bool
+ var configFile, dbfile, newFeed, export, password string
+ var port, minutes int
+
+ f := flag.NewFlagSet("neko", flag.ContinueOnError)
+
+ // config file
+ f.StringVar(&configFile, "config", "", "read configuration from file")
+ f.StringVar(&configFile, "c", "", "read configuration from file (short)")
+
+ // commands
+ f.BoolVar(&help, "help", false, "display help")
+ f.BoolVar(&help, "h", false, "display help (short)")
+
+ f.BoolVar(&update, "update", false, "fetch feeds and store new items")
+ f.BoolVar(&update, "u", false, "fetch feeds and store new items (short)")
+
+ f.StringVar(&newFeed, "add", "", "add the feed at URL")
+ f.StringVar(&newFeed, "a", "", "add the feed at URL (short)")
+
+ f.StringVar(&export, "export", "", "export feed: text, opml, html, json")
+ f.StringVar(&export, "x", "", "export feed (short)")
+
+ // options
+ f.StringVar(&dbfile, "database", "", "sqlite database file")
+ f.StringVar(&dbfile, "d", "", "sqlite database file (short)")
+
+ f.IntVar(&port, "http", 0, "HTTP port to serve on")
+ f.IntVar(&port, "s", 0, "HTTP port to serve on (short)")
+
+ f.IntVar(&minutes, "minutes", 0, "minutes between crawling feeds")
+ f.IntVar(&minutes, "m", 0, "minutes between crawling feeds (short)")
+
+ f.BoolVar(&proxyImages, "imageproxy", false, "rewrite and proxy all image requests")
+ f.BoolVar(&proxyImages, "i", false, "rewrite and proxy all image requests (short)")
+
+ f.BoolVar(&verbose, "verbose", false, "verbose output")
+ f.BoolVar(&verbose, "v", false, "verbose output (short)")
+
+ f.StringVar(&password, "password", "", "password for web interface")
+ f.StringVar(&password, "p", "", "password for web interface (short)")
+
+ f.Usage = func() {
+ fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
+ f.PrintDefaults()
+ }
+
+ if err := f.Parse(args); err != nil {
+ return err
+ }
+
+ if help {
+ fmt.Printf("neko v%s | build %s\n", Version, Build)
+ f.Usage()
+ return nil
+ }
+ // reads config if present and sets defaults
+ if err := config.Init(configFile); err != nil {
+ return fmt.Errorf("config error: %v", err)
+ }
+
+ // override config file with flags if present
+ vlog.VERBOSE = verbose
+ if dbfile != "" {
+ config.Config.DBFile = dbfile
+ }
+
+ if port != 0 {
+ config.Config.Port = port
+ }
+
+ if password != "" {
+ config.Config.DigestPassword = password
+ }
+
+ if minutes != 0 {
+ config.Config.CrawlMinutes = minutes
+ }
+
+ if proxyImages != false {
+ config.Config.ProxyImages = proxyImages
+ }
+
+ models.InitDB()
+
+ if update {
+ vlog.Printf("starting crawl\n")
+ crawler.Crawl()
+ return nil
+ }
+ if newFeed != "" {
+ vlog.Printf("creating new feed\n")
+ feed.NewFeed(newFeed)
+ return nil
+ }
+ if export != "" {
+ vlog.Printf("exporting feeds in format %s\n", export)
+ fmt.Printf("%s", exporter.ExportFeeds(export))
+ return nil
+ }
+
+ // For testing, we might want to avoid starting a web server
+ if config.Config.Port == -1 {
+ return nil
+ }
+
+ go backgroundCrawl(config.Config.CrawlMinutes)
+ vlog.Printf("starting web server at 127.0.0.1:%d\n",
+ config.Config.Port)
+ web.Serve(&config.Config)
+ return nil
+}
+
+func backgroundCrawl(minutes int) {
+ if minutes < 1 {
+ return
+ }
+ vlog.Printf("starting background crawl every %d minutes\n", minutes)
+ for {
+ time.Sleep(time.Minute * time.Duration(minutes))
+ crawler.Crawl()
+ }
+}
diff --git a/cmd/neko/main_test.go b/cmd/neko/main_test.go
new file mode 100644
index 0000000..fd36fdd
--- /dev/null
+++ b/cmd/neko/main_test.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "adammathes.com/neko/config"
+ "adammathes.com/neko/models"
+)
+
+func TestRunHelp(t *testing.T) {
+ err := Run([]string{"--help"})
+ if err != nil {
+ t.Errorf("Run(--help) should not error, got %v", err)
+ }
+}
+
+func TestRunInvalidFlag(t *testing.T) {
+ err := Run([]string{"--invalid"})
+ if err == nil {
+ t.Error("Run(--invalid) should error")
+ }
+}
+
+func TestRunCrawl(t *testing.T) {
+ // Setup test DB
+ config.Config.DBFile = filepath.Join(t.TempDir(), "test_main.db")
+ models.InitDB()
+ defer models.DB.Close()
+
+ // Use --update flag
+ err := Run([]string{"-u", "-d", config.Config.DBFile})
+ if err != nil {
+ t.Errorf("Run(-u) should not error, got %v", err)
+ }
+}
+
+func TestBackgroundCrawlZero(t *testing.T) {
+ backgroundCrawl(0) // Should return immediately
+}
+
+func TestRunServerConfig(t *testing.T) {
+ // Setup test DB
+ config.Config.DBFile = filepath.Join(t.TempDir(), "test_main_server.db")
+ models.InitDB()
+ defer models.DB.Close()
+
+ // Use config.Config.Port = -1 to signal Run to exit instead of starting server
+ config.Config.Port = -1
+ err := Run([]string{"-d", config.Config.DBFile})
+ if err != nil {
+ t.Errorf("Run should not error with Port=-1, got %v", err)
+ }
+}
+
+func TestRunAdd(t *testing.T) {
+ dbPath := filepath.Join(t.TempDir(), "test_add.db")
+ err := Run([]string{"-d", dbPath, "-a", "http://example.com/rss"})
+ if err != nil {
+ t.Errorf("Run -a failed: %v", err)
+ }
+}
+
+func TestRunExport(t *testing.T) {
+ dbPath := filepath.Join(t.TempDir(), "test_export.db")
+ err := Run([]string{"-d", dbPath, "-x", "text"})
+ if err != nil {
+ t.Errorf("Run -x failed: %v", err)
+ }
+}
+
+func TestRunOptions(t *testing.T) {
+ dbPath := filepath.Join(t.TempDir(), "test_options.db")
+ err := Run([]string{"-d", dbPath, "-v", "-i", "-s", "-1"})
+ if err != nil {
+ t.Errorf("Run with options failed: %v", err)
+ }
+}
+
+func TestRunSetPassword(t *testing.T) {
+ dbPath := filepath.Join(t.TempDir(), "test_pass.db")
+ err := Run([]string{"-d", dbPath, "-p", "newpassword"})
+ if err != nil {
+ t.Errorf("Run -p should succeed, got %v", err)
+ }
+ if config.Config.DigestPassword != "newpassword" {
+ t.Errorf("Expected password to be updated")
+ }
+}
+
+func TestRunConfigError(t *testing.T) {
+ err := Run([]string{"-c", "/nonexistent/config.yaml"})
+ if err == nil {
+ t.Error("Run should error for nonexistent config file")
+ }
+}
+
+func TestRunExportFormat(t *testing.T) {
+ dbPath := filepath.Join(t.TempDir(), "test_export_format.db")
+ err := Run([]string{"-d", dbPath, "-x", "json"})
+ if err != nil {
+ t.Errorf("Run -x json failed: %v", err)
+ }
+}
+
+func TestRunConfigInvalidContent(t *testing.T) {
+ tmpDir := t.TempDir()
+ confPath := filepath.Join(tmpDir, "bad.yaml")
+ os.WriteFile(confPath, []byte("invalid: : yaml"), 0644)
+ err := Run([]string{"-c", confPath})
+ if err == nil {
+ t.Error("Run should error for malformed config file")
+ }
+}
+
+func TestRunNoArgs(t *testing.T) {
+ dbPath := filepath.Join(t.TempDir(), "test_noargs.db")
+ config.Config.Port = -1
+ err := Run([]string{"-d", dbPath})
+ if err != nil {
+ t.Errorf("Run with no args failed: %v", err)
+ }
+}