package main import ( "context" "encoding/base64" "errors" "log/slog" "net/http" "os" "os/signal" "time" "github.com/glebarez/sqlite" "github.com/gorilla/mux" flag "github.com/spf13/pflag" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" "git.sr.ht/~gabrielgio/img/pkg/database/localfs" "git.sr.ht/~gabrielgio/img/pkg/database/repository" "git.sr.ht/~gabrielgio/img/pkg/database/sql" "git.sr.ht/~gabrielgio/img/pkg/ext" "git.sr.ht/~gabrielgio/img/pkg/service" "git.sr.ht/~gabrielgio/img/pkg/view" "git.sr.ht/~gabrielgio/img/pkg/worker" "git.sr.ht/~gabrielgio/img/pkg/worker/scanner" "git.sr.ht/~gabrielgio/img/static" ) func main() { var ( key = flag.String("aes-key", "", "AES key, either 16, 24, or 32 bytes string to select AES-128, AES-192, or AES-256") dbType = flag.String("db-type", "sqlite", "Database to be used. Choose either mysql, psql or sqlite") dbCon = flag.String("db-con", "main.db", "Database string connection for given database type. Ref: https://gorm.io/docs/connecting_to_the_database.html") logLevel = flag.String("log-level", "error", "Log level: Choose either debug, info, warning, error") schedulerCount = flag.Uint("scheduler-count", 10, "How many workers are created to process media files") cachePath = flag.String("cache-path", "", "Folder to store thumbnail image") ) flag.Parse() level := parseLogLevel(*logLevel) handler := slog.NewTextHandler( os.Stdout, &slog.HandlerOptions{Level: level}, ) logger := slog.New(handler) d, err := OpenDatabase(*dbType, *dbCon) if err != nil { panic("failed to parse database strings" + err.Error()) } db, err := gorm.Open(d, &gorm.Config{ Logger: ext.Wraplog(logger.With("context", "sql")), }) if err != nil { panic("failed to connect database: " + err.Error()) } if err = sql.Migrate(db); err != nil { panic("failed to migrate database: " + err.Error()) } if *dbType == "sqlite" { *schedulerCount = 1 } baseKey, err := base64.StdEncoding.DecodeString(*key) if err != nil { panic("failed to decode key database: " + err.Error()) } r := mux.NewRouter().StrictSlash(false) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static)))) // repository var ( userRepository = sql.NewUserRepository(db) settingsRepository = sql.NewSettingsRespository(db) fileSystemRepository = localfs.NewFileSystemRepository() mediaRepository = sql.NewMediaRepository(db) ) // middleware var ( authMiddleware = ext.NewAuthMiddleware(baseKey, logger.With("context", "auth"), userRepository) logMiddleware = ext.NewLogMiddleare(logger.With("context", "http")) initialMiddleware = ext.NewInitialSetupMiddleware(userRepository) ) extRouter := ext.NewRouter(r) extRouter.AddMiddleware(ext.HTML) extRouter.AddMiddleware(initialMiddleware.Check) extRouter.AddMiddleware(authMiddleware.LoggedIn) extRouter.AddMiddleware(logMiddleware.HTTP) scheduler := worker.NewScheduler(*schedulerCount) // controller var ( userController = service.NewAuthController(userRepository, userRepository, baseKey) fileSystemController = service.NewFileSystemController(fileSystemRepository, userRepository) ) // view for _, v := range []view.View{ view.NewAuthView(userController), view.NewFileSystemView(*fileSystemController, settingsRepository), view.NewSettingsView(settingsRepository, userController), view.NewMediaView(mediaRepository, userRepository, settingsRepository), view.NewAlbumView(mediaRepository, userRepository, settingsRepository), } { v.SetMyselfIn(extRouter) } // processors var ( fileScanner = scanner.NewFileScanner(mediaRepository, userRepository) exifScanner = scanner.NewEXIFScanner(mediaRepository) thumbnailScanner = scanner.NewThumbnailScanner(*cachePath, mediaRepository) albumScanner = scanner.NewAlbumScanner(mediaRepository) ) // tasks var ( serverTask = worker.NewServerTask(&http.Server{Handler: r, Addr: "0.0.0.0:8080"}) fileTask = worker.NewTaskFromChanProcessor[string]( fileScanner, scheduler, logger.With("context", "file scanner"), ) exifTask = worker.NewTaskFromBatchProcessor[*repository.Media]( exifScanner, scheduler, logger.With("context", "exif scanner"), ) thumbnailTask = worker.NewTaskFromBatchProcessor[*repository.Media]( thumbnailScanner, scheduler, logger.With("context", "thumbnail scanner"), ) albumTask = worker.NewTaskFromSerialProcessor[*repository.Media]( albumScanner, scheduler, logger.With("context", "thumbnail scanner"), ) ) pool := worker.NewTaskPool() pool.AddTask("http server", time.Minute, serverTask) pool.AddTask("exif scanner", 15*time.Minute, exifTask) pool.AddTask("file scanner", 2*time.Hour, fileTask) pool.AddTask("thumbnail scanner", 15*time.Minute, thumbnailTask) pool.AddTask("album scanner", 15*time.Minute, albumTask) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() pool.Start(ctx) } func OpenDatabase(dbType string, dbConn string) (gorm.Dialector, error) { switch dbType { case "sqlite": return sqlite.Open(dbConn), nil case "psql": return postgres.Open(dbConn), nil case "mysql": return mysql.Open(dbConn), nil default: return nil, errors.New("No valid db type given") } } func parseLogLevel(input string) slog.Level { switch input { case "debug": return slog.LevelDebug case "info": return slog.LevelInfo case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelWarn } }