1package main
2
3import (
4 "context"
5 "encoding/base64"
6 "errors"
7 "net/http"
8 "os"
9 "os/signal"
10 "time"
11
12 "github.com/gorilla/mux"
13 "github.com/sirupsen/logrus"
14 flag "github.com/spf13/pflag"
15 "gorm.io/driver/mysql"
16 "gorm.io/driver/postgres"
17 "gorm.io/driver/sqlite"
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 trace, debug, info, warning, error, fatal or panic")
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 l, err := logrus.ParseLevel(*logLevel)
44 if err != nil {
45 panic("failed to parse log level" + err.Error())
46 }
47 logger := logrus.New()
48 logger.SetLevel(l)
49
50 d, err := OpenDatabase(*dbType, *dbCon)
51 if err != nil {
52 panic("failed to parse database strings" + err.Error())
53 }
54
55 db, err := gorm.Open(d, &gorm.Config{
56 Logger: ext.Wraplog(logger.WithField("context", "sql")),
57 })
58 if err != nil {
59 panic("failed to connect database: " + err.Error())
60 }
61
62 if err = sql.Migrate(db); err != nil {
63 panic("failed to migrate database: " + err.Error())
64 }
65
66 if *dbType == "sqlite" {
67 *schedulerCount = 1
68 }
69
70 baseKey, err := base64.StdEncoding.DecodeString(*key)
71 if err != nil {
72 panic("failed to decode key database: " + err.Error())
73 }
74
75 r := mux.NewRouter()
76 r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static))))
77
78 // repository
79 var (
80 userRepository = sql.NewUserRepository(db)
81 settingsRepository = sql.NewSettingsRespository(db)
82 fileSystemRepository = localfs.NewFileSystemRepository()
83 mediaRepository = sql.NewMediaRepository(db)
84 )
85
86 // middleware
87 var (
88 authMiddleware = ext.NewAuthMiddleware(baseKey, logger.WithField("context", "auth"))
89 logMiddleware = ext.NewLogMiddleare(logger.WithField("context", "http"))
90 initialMiddleware = ext.NewInitialSetupMiddleware(userRepository)
91 )
92
93 extRouter := ext.NewRouter(r)
94 extRouter.AddMiddleware(ext.HTML)
95 extRouter.AddMiddleware(initialMiddleware.Check)
96 extRouter.AddMiddleware(authMiddleware.LoggedIn)
97 extRouter.AddMiddleware(logMiddleware.HTTP)
98
99 scheduler := worker.NewScheduler(*schedulerCount)
100
101 // controller
102 var (
103 userController = service.NewAuthController(userRepository, userRepository, baseKey)
104 fileSystemController = service.NewFileSystemController(fileSystemRepository, userRepository)
105 )
106
107 // view
108 for _, v := range []view.View{
109 view.NewAuthView(userController),
110 view.NewFileSystemView(*fileSystemController, settingsRepository),
111 view.NewSettingsView(settingsRepository, userController),
112 view.NewMediaView(mediaRepository, userRepository, settingsRepository),
113 view.NewAlbumView(mediaRepository, userRepository, settingsRepository),
114 } {
115 v.SetMyselfIn(extRouter)
116 }
117
118 // processors
119 var (
120 fileScanner = scanner.NewFileScanner(mediaRepository, userRepository)
121 exifScanner = scanner.NewEXIFScanner(mediaRepository)
122 thumbnailScanner = scanner.NewThumbnailScanner(*cachePath, mediaRepository)
123 albumScanner = scanner.NewAlbumScanner(mediaRepository)
124 )
125
126 // tasks
127 var (
128 serverTask = worker.NewServerTask(&http.Server{Handler: r, Addr: "0.0.0.0:8080"})
129 fileTask = worker.NewTaskFromChanProcessor[string](
130 fileScanner,
131 scheduler,
132 logrus.WithField("context", "file scanner"),
133 )
134 exifTask = worker.NewTaskFromBatchProcessor[*repository.Media](
135 exifScanner,
136 scheduler,
137 logrus.WithField("context", "exif scanner"),
138 )
139 thumbnailTask = worker.NewTaskFromBatchProcessor[*repository.Media](
140 thumbnailScanner,
141 scheduler,
142 logrus.WithField("context", "thumbnail scanner"),
143 )
144 albumTask = worker.NewTaskFromSerialProcessor[*repository.Media](
145 albumScanner,
146 scheduler,
147 logrus.WithField("context", "thumbnail scanner"),
148 )
149 )
150
151 pool := worker.NewTaskPool()
152 pool.AddTask("http server", time.Minute, serverTask)
153 pool.AddTask("exif scanner", 15*time.Minute, exifTask)
154 pool.AddTask("file scanner", 2*time.Hour, fileTask)
155 pool.AddTask("thumbnail scanner", 15*time.Minute, thumbnailTask)
156 pool.AddTask("album scanner", 15*time.Minute, albumTask)
157
158 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
159 defer stop()
160
161 pool.Start(ctx)
162}
163
164func OpenDatabase(dbType string, dbConn string) (gorm.Dialector, error) {
165 switch dbType {
166 case "sqlite":
167 return sqlite.Open(dbConn), nil
168 case "psql":
169 return postgres.Open(dbConn), nil
170 case "mysql":
171 return mysql.Open(dbConn), nil
172 default:
173 return nil, errors.New("No valid db type given")
174 }
175}