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}