diff --git a/main.go b/main.go index 47da465..3545547 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,9 @@ var ( userFromEnv = os.Getenv("USERNAME") passFromEnv = os.Getenv("PASSWORD") + logFile string + enableFileLogging bool + //go:embed frontend/dist/index.html //go:embed frontend/dist/assets/* frontend embed.FS @@ -47,6 +50,9 @@ func init() { flag.StringVar(&sessionFilePath, "session", ".", "session file path") flag.StringVar(&localDatabasePath, "db", "local.db", "local database path") + flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file") + flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location") + flag.BoolVar(&requireAuth, "auth", false, "Enable RPC authentication") flag.StringVar(&username, "user", userFromEnv, "Username required for auth") flag.StringVar(&password, "pass", passFromEnv, "Password required for auth") @@ -79,5 +85,12 @@ func main() { log.Println(cli.BgRed, "config", cli.Reset, err) } - server.RunBlocking(c.Host, c.Port, frontend, localDatabasePath) + server.RunBlocking(&server.RunConfig{ + Host: c.Host, + Port: c.Port, + App: frontend, + DBPath: localDatabasePath, + FileLogging: enableFileLogging, + LogFile: logFile, + }) } diff --git a/server/logging/file_logger.go b/server/logging/file_logger.go new file mode 100644 index 0000000..b99174e --- /dev/null +++ b/server/logging/file_logger.go @@ -0,0 +1,80 @@ +package logging + +import ( + "compress/gzip" + "io" + "os" + "sync" + "time" +) + +// implements io.Writer interface +type LogRotateWriter struct { + mu sync.Mutex + fd *os.File + filename string +} + +func NewRotableLogger(filename string) (*LogRotateWriter, error) { + fd, err := os.Create(filename) + if err != nil { + return nil, err + } + w := &LogRotateWriter{filename: filename, fd: fd} + return w, nil +} + +func (w *LogRotateWriter) Write(b []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.fd.Write(b) +} + +func (w *LogRotateWriter) Rotate() error { + var err error + w.mu.Lock() + + gzFile, err := os.Create(w.filename + "." + time.Now().Format(time.RFC3339) + ".gz") + if err != nil { + return err + } + + data, err := io.ReadAll(w.fd) + if err != nil { + return err + } + + defer func() { + w.mu.Unlock() + w.gzipLog(gzFile, &data) + }() + + _, err = os.Stat(w.filename) + if err != nil { + return err + } + + if w.fd != nil { + err = w.fd.Close() + w.fd = nil + if err != nil { + return err + } + } + + err = os.Remove(w.filename) + if err != nil { + return err + } + + w.fd, err = os.Create(w.filename) + return err +} + +func (w *LogRotateWriter) gzipLog(wr io.Writer, data *[]byte) error { + if _, err := gzip.NewWriter(wr).Write(*data); err != nil { + return err + } + + return nil +} diff --git a/server/server.go b/server/server.go index d355284..78f4bb9 100644 --- a/server/server.go +++ b/server/server.go @@ -30,6 +30,15 @@ import ( _ "modernc.org/sqlite" ) +type RunConfig struct { + Host string + Port int + App fs.FS + DBPath string + LogFile string + FileLogging bool +} + type serverConfig struct { frontend fs.FS logger *slog.Logger @@ -40,19 +49,37 @@ type serverConfig struct { mq *internal.MessageQueue } -func RunBlocking(host string, port int, frontend fs.FS, dbPath string) { +func RunBlocking(cfg *RunConfig) { var mdb internal.MemoryDB + logWriters := []io.Writer{ + os.Stdout, + logging.NewObservableLogger(), + } + + if cfg.FileLogging { + logger, err := logging.NewRotableLogger(cfg.LogFile) + if err != nil { + panic(err) + } + + go func() { + for { + time.Sleep(time.Hour * 24) + logger.Rotate() + } + }() + + logWriters = append(logWriters, logger) + } + logger := slog.New( - slog.NewTextHandler( - io.MultiWriter(os.Stdout, logging.NewObservableLogger()), - &slog.HandlerOptions{}, - ), + slog.NewTextHandler(io.MultiWriter(logWriters...), &slog.HandlerOptions{}), ) mdb.Restore(logger) - db, err := sql.Open("sqlite", dbPath) + db, err := sql.Open("sqlite", cfg.DBPath) if err != nil { logger.Error("failed to open database", slog.String("err", err.Error())) } @@ -66,10 +93,10 @@ func RunBlocking(host string, port int, frontend fs.FS, dbPath string) { go mq.Subscriber() srv := newServer(serverConfig{ - frontend: frontend, + frontend: cfg.App, logger: logger, - host: host, - port: port, + host: cfg.Host, + port: cfg.Port, mdb: &mdb, mq: mq, db: db, @@ -79,11 +106,13 @@ func RunBlocking(host string, port int, frontend fs.FS, dbPath string) { go autoPersist(time.Minute*5, &mdb, logger) network := "tcp" - address := fmt.Sprintf("%s:%d", host, port) - if strings.HasPrefix(host, "/") { + address := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + + if strings.HasPrefix(cfg.Host, "/") { network = "unix" - address = host + address = cfg.Host } + listener, err := net.Listen(network, address) if err != nil { logger.Error("failed to listen", slog.String("err", err.Error())) diff --git a/server/utils/logrotate.go b/server/utils/logrotate.go deleted file mode 100644 index 684f1a9..0000000 --- a/server/utils/logrotate.go +++ /dev/null @@ -1,55 +0,0 @@ -package utils - -import ( - "io" - "io/fs" - "os" - "path/filepath" - "time" - - "github.com/marcopeocchi/yt-dlp-web-ui/server/config" -) - -func LogRotate() (*os.File, error) { - logs := findLogs() - - for _, log := range logs { - logfd, err := os.Open(log) - if err != nil { - return nil, err - } - - gzWriter, err := os.Create(log + ".gz") - if err != nil { - return nil, err - } - - _, err = io.Copy(gzWriter, logfd) - if err != nil { - return nil, err - } - } - - logfile := time.Now().String() + ".log" - config.Instance().CurrentLogFile = logfile - - return os.Create(logfile) -} - -func findLogs() []string { - var ( - logfiles []string - root = config.Instance().LogPath - ) - - filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if filepath.Ext(d.Name()) == ".log" { - logfiles = append(logfiles, path) - } - return nil - }) - return logfiles -}