lens @ 417041797674319485bea96cb38489670ff3b2ef

  1package main
  2
  3import (
  4	"context"
  5	"encoding/base64"
  6	"errors"
  7	"log/slog"
  8	"net/http"
  9	"os"
 10	"os/signal"
 11	"time"
 12
 13	"github.com/glebarez/sqlite"
 14	"github.com/gorilla/mux"
 15	flag "github.com/spf13/pflag"
 16	"gorm.io/driver/mysql"
 17	"gorm.io/driver/postgres"
 18	"gorm.io/gorm"
 19
 20	"git.sr.ht/~gabrielgio/img/pkg/database/localfs"
 21	"git.sr.ht/~gabrielgio/img/pkg/database/repository"
 22	"git.sr.ht/~gabrielgio/img/pkg/database/sql"
 23	"git.sr.ht/~gabrielgio/img/pkg/ext"
 24	"git.sr.ht/~gabrielgio/img/pkg/service"
 25	"git.sr.ht/~gabrielgio/img/pkg/view"
 26	"git.sr.ht/~gabrielgio/img/pkg/worker"
 27	"git.sr.ht/~gabrielgio/img/pkg/worker/scanner"
 28	"git.sr.ht/~gabrielgio/img/static"
 29)
 30
 31func main() {
 32	var (
 33		key            = flag.String("aes-key", "", "AES key, either 16, 24, or 32 bytes string to select AES-128, AES-192, or AES-256")
 34		dbType         = flag.String("db-type", "sqlite", "Database to be used. Choose either mysql, psql or sqlite")
 35		dbCon          = flag.String("db-con", "main.db", "Database string connection for given database type. Ref: https://gorm.io/docs/connecting_to_the_database.html")
 36		logLevel       = flag.String("log-level", "error", "Log level: Choose either debug, info, warning, error")
 37		schedulerCount = flag.Uint("scheduler-count", 10, "How many workers are created to process media files")
 38		cachePath      = flag.String("cache-path", "", "Folder to store thumbnail image")
 39	)
 40
 41	flag.Parse()
 42
 43	level := parseLogLevel(*logLevel)
 44
 45	handler := slog.NewTextHandler(
 46		os.Stdout,
 47		&slog.HandlerOptions{Level: level},
 48	)
 49	logger := slog.New(handler)
 50
 51	d, err := OpenDatabase(*dbType, *dbCon)
 52	if err != nil {
 53		panic("failed to parse database strings" + err.Error())
 54	}
 55
 56	db, err := gorm.Open(d, &gorm.Config{
 57		Logger: ext.Wraplog(logger.With("context", "sql")),
 58	})
 59	if err != nil {
 60		panic("failed to connect database: " + err.Error())
 61	}
 62
 63	if err = sql.Migrate(db); err != nil {
 64		panic("failed to migrate database: " + err.Error())
 65	}
 66
 67	if *dbType == "sqlite" {
 68		*schedulerCount = 1
 69	}
 70
 71	baseKey, err := base64.StdEncoding.DecodeString(*key)
 72	if err != nil {
 73		panic("failed to decode key database: " + err.Error())
 74	}
 75
 76	r := mux.NewRouter().StrictSlash(false)
 77	r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static))))
 78
 79	// repository
 80	var (
 81		userRepository       = sql.NewUserRepository(db)
 82		settingsRepository   = sql.NewSettingsRespository(db)
 83		fileSystemRepository = localfs.NewFileSystemRepository()
 84		mediaRepository      = sql.NewMediaRepository(db)
 85	)
 86
 87	// middleware
 88	var (
 89		authMiddleware    = ext.NewAuthMiddleware(baseKey, logger.With("context", "auth"), userRepository)
 90		logMiddleware     = ext.NewLogMiddleare(logger.With("context", "http"))
 91		initialMiddleware = ext.NewInitialSetupMiddleware(userRepository)
 92	)
 93
 94	extRouter := ext.NewRouter(r)
 95	extRouter.AddMiddleware(ext.HTML)
 96	extRouter.AddMiddleware(initialMiddleware.Check)
 97	extRouter.AddMiddleware(authMiddleware.LoggedIn)
 98	extRouter.AddMiddleware(logMiddleware.HTTP)
 99
100	scheduler := worker.NewScheduler(*schedulerCount)
101
102	// controller
103	var (
104		userController       = service.NewAuthController(userRepository, userRepository, baseKey)
105		fileSystemController = service.NewFileSystemController(fileSystemRepository, userRepository)
106	)
107
108	// view
109	for _, v := range []view.View{
110		view.NewAuthView(userController),
111		view.NewFileSystemView(*fileSystemController, settingsRepository),
112		view.NewSettingsView(settingsRepository, userController),
113		view.NewMediaView(mediaRepository, userRepository, settingsRepository),
114		view.NewAlbumView(mediaRepository, userRepository, settingsRepository),
115	} {
116		v.SetMyselfIn(extRouter)
117	}
118
119	// processors
120	var (
121		fileScanner      = scanner.NewFileScanner(mediaRepository, userRepository)
122		exifScanner      = scanner.NewEXIFScanner(mediaRepository)
123		thumbnailScanner = scanner.NewThumbnailScanner(*cachePath, mediaRepository)
124		albumScanner     = scanner.NewAlbumScanner(mediaRepository)
125	)
126
127	// tasks
128	var (
129		serverTask = worker.NewServerTask(&http.Server{Handler: r, Addr: "0.0.0.0:8080"})
130		fileTask   = worker.NewTaskFromChanProcessor[string](
131			fileScanner,
132			scheduler,
133			logger.With("context", "file scanner"),
134		)
135		exifTask = worker.NewTaskFromBatchProcessor[*repository.Media](
136			exifScanner,
137			scheduler,
138			logger.With("context", "exif scanner"),
139		)
140		thumbnailTask = worker.NewTaskFromBatchProcessor[*repository.Media](
141			thumbnailScanner,
142			scheduler,
143			logger.With("context", "thumbnail scanner"),
144		)
145		albumTask = worker.NewTaskFromSerialProcessor[*repository.Media](
146			albumScanner,
147			scheduler,
148			logger.With("context", "thumbnail scanner"),
149		)
150	)
151
152	pool := worker.NewTaskPool()
153	pool.AddTask("http server", time.Minute, serverTask)
154	pool.AddTask("exif scanner", 15*time.Minute, exifTask)
155	pool.AddTask("file scanner", 2*time.Hour, fileTask)
156	pool.AddTask("thumbnail scanner", 15*time.Minute, thumbnailTask)
157	pool.AddTask("album scanner", 15*time.Minute, albumTask)
158
159	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
160	defer stop()
161
162	pool.Start(ctx)
163}
164
165func OpenDatabase(dbType string, dbConn string) (gorm.Dialector, error) {
166	switch dbType {
167	case "sqlite":
168		return sqlite.Open(dbConn), nil
169	case "psql":
170		return postgres.Open(dbConn), nil
171	case "mysql":
172		return mysql.Open(dbConn), nil
173	default:
174		return nil, errors.New("No valid db type given")
175	}
176}
177
178func parseLogLevel(input string) slog.Level {
179	switch input {
180	case "debug":
181		return slog.LevelDebug
182	case "info":
183		return slog.LevelInfo
184	case "warn":
185		return slog.LevelWarn
186	case "error":
187		return slog.LevelError
188	default:
189		return slog.LevelWarn
190	}
191}