Files
jiggablend/cmd/jiggablend/cmd/manager.go

178 lines
5.2 KiB
Go

package cmd
import (
"fmt"
"net/http"
"os/exec"
"strings"
"jiggablend/internal/auth"
"jiggablend/internal/config"
"jiggablend/internal/database"
"jiggablend/internal/logger"
manager "jiggablend/internal/manager"
"jiggablend/internal/storage"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var managerCmd = &cobra.Command{
Use: "manager",
Short: "Start the Jiggablend manager server",
Long: `Start the Jiggablend manager server to coordinate render jobs.`,
Run: runManager,
}
func init() {
rootCmd.AddCommand(managerCmd)
// Flags with env binding via viper
managerCmd.Flags().StringP("port", "p", "8080", "Server port")
managerCmd.Flags().String("db", "jiggablend.db", "Database path")
managerCmd.Flags().String("storage", "./jiggablend-storage", "Storage path")
managerCmd.Flags().StringP("log-file", "l", "", "Log file path (truncated on start, if not set logs only to stdout)")
managerCmd.Flags().String("log-level", "info", "Log level (debug, info, warn, error)")
managerCmd.Flags().BoolP("verbose", "v", false, "Enable verbose logging (same as --log-level=debug)")
// Bind flags to viper with JIGGABLEND_ prefix
viper.SetEnvPrefix("JIGGABLEND")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
viper.BindPFlag("port", managerCmd.Flags().Lookup("port"))
viper.BindPFlag("db", managerCmd.Flags().Lookup("db"))
viper.BindPFlag("storage", managerCmd.Flags().Lookup("storage"))
viper.BindPFlag("log_file", managerCmd.Flags().Lookup("log-file"))
viper.BindPFlag("log_level", managerCmd.Flags().Lookup("log-level"))
viper.BindPFlag("verbose", managerCmd.Flags().Lookup("verbose"))
}
func runManager(cmd *cobra.Command, args []string) {
// Get config values (flags take precedence over env vars)
port := viper.GetString("port")
dbPath := viper.GetString("db")
storagePath := viper.GetString("storage")
logFile := viper.GetString("log_file")
logLevel := viper.GetString("log_level")
verbose := viper.GetBool("verbose")
// Initialize logger
if logFile != "" {
if err := logger.InitWithFile(logFile); err != nil {
logger.Fatalf("Failed to initialize logger: %v", err)
}
defer func() {
if l := logger.GetDefault(); l != nil {
l.Close()
}
}()
} else {
logger.InitStdout()
}
// Set log level
if verbose {
logger.SetLevel(logger.LevelDebug)
} else {
logger.SetLevel(logger.ParseLevel(logLevel))
}
if logFile != "" {
logger.Infof("Logging to file: %s", logFile)
}
logger.Debugf("Log level: %s", logLevel)
// Initialize database
db, err := database.NewDB(dbPath)
if err != nil {
logger.Fatalf("Failed to initialize database: %v", err)
}
defer db.Close()
// Initialize config from database
cfg := config.NewConfig(db)
if err := cfg.InitializeFromEnv(); err != nil {
logger.Fatalf("Failed to initialize config: %v", err)
}
logger.Info("Configuration loaded from database")
// Initialize auth
authHandler, err := auth.NewAuth(db, cfg)
if err != nil {
logger.Fatalf("Failed to initialize auth: %v", err)
}
// Initialize storage
storageHandler, err := storage.NewStorage(storagePath)
if err != nil {
logger.Fatalf("Failed to initialize storage: %v", err)
}
// Check if Blender is available
if err := checkBlenderAvailable(); err != nil {
logger.Fatalf("Blender is not available: %v\n"+
"The manager requires Blender to be installed and in PATH for metadata extraction.\n"+
"Please install Blender and ensure it's accessible via the 'blender' command.", err)
}
logger.Info("Blender is available")
// Check if ImageMagick is available
if err := checkImageMagickAvailable(); err != nil {
logger.Fatalf("ImageMagick is not available: %v\n"+
"The manager requires ImageMagick to be installed and in PATH for EXR preview conversion.\n"+
"Please install ImageMagick and ensure 'magick' or 'convert' command is accessible.", err)
}
logger.Info("ImageMagick is available")
// Create manager server
server, err := manager.NewManager(db, cfg, authHandler, storageHandler)
if err != nil {
logger.Fatalf("Failed to create server: %v", err)
}
// Start server
addr := fmt.Sprintf(":%s", port)
logger.Infof("Starting manager server on %s", addr)
logger.Infof("Database: %s", dbPath)
logger.Infof("Storage: %s", storagePath)
httpServer := &http.Server{
Addr: addr,
Handler: server,
MaxHeaderBytes: 1 << 20,
ReadTimeout: 0,
WriteTimeout: 0,
}
if err := httpServer.ListenAndServe(); err != nil {
logger.Fatalf("Server failed: %v", err)
}
}
func checkBlenderAvailable() error {
cmd := exec.Command("blender", "--version")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to run 'blender --version': %w (output: %s)", err, string(output))
}
return nil
}
func checkImageMagickAvailable() error {
// Try 'magick' first (ImageMagick 7+)
cmd := exec.Command("magick", "--version")
output, err := cmd.CombinedOutput()
if err == nil {
return nil
}
// Fall back to 'convert' (ImageMagick 6 or legacy mode)
cmd = exec.Command("convert", "--version")
output, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to run 'magick --version' or 'convert --version': %w (output: %s)", err, string(output))
}
return nil
}