Repository navigation
Expand file tree
/
Copy pathserver.go
More file actions
358 lines (315 loc) · 9.59 KB
/
Copy pathserver.go
File metadata and controls
358 lines (315 loc) · 9.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
//nolint:funlen,nonamedreturns
package main
/*
Package main is the entry point for the Defacto2 server application.
Use the Task runner / build tool (https://taskfile.dev) to build or run the source code.
$ task --list
Repository: https://github.com/Defacto2/server
Website: https://defacto2.net
License: GPL-3.0
© Defacto2, 2024-26
*/
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"io"
"log"
"log/slog"
"os"
"os/signal"
"runtime"
"slices"
"strings"
"syscall"
"time"
"github.com/Defacto2/helper"
"github.com/Defacto2/server/flags"
"github.com/Defacto2/server/handler"
"github.com/Defacto2/server/handler/fulltext"
"github.com/Defacto2/server/handler/tidbit"
"github.com/Defacto2/server/internal/config"
"github.com/Defacto2/server/internal/dir"
"github.com/Defacto2/server/internal/logs"
"github.com/Defacto2/server/internal/postgres"
"github.com/caarlos0/env/v11"
_ "github.com/jackc/pgx/v5"
)
var (
//go:embed public/text/defacto2.txt
brand []byte
//go:embed public/**/*
public embed.FS
//go:embed view/**/*
view embed.FS
version string // version is generated by the GoReleaser ldflags.
)
var ErrLog = errors.New("server: cannot save one or more log files")
func main() {
const msg = "defacto2 startup"
// Initialize a temporary logger, and get then print the environment variable configurations.
l := logs.Default()
slog.SetDefault(l)
envConfig := environmentVars(context.Background(), l)
// Parse any application commands and flags, and if appropriate run the request and exit to
// the terminal.
const quit = 0
if exitCode := flagParser(l, os.Stdout, envConfig); exitCode >= quit {
code := int(exitCode)
os.Exit(code)
}
// Configure the sl application logger, the cl startup configuration logger and the logo writer.
// Two separate loggers are used to avoid the system and file logs being cluttered with server
// startup information.
const nothing = "" // replace the dname nothing argument with logs.NameDebug, to write debug log reports
root := string(envConfig.AbsLog)
ename, iname := logs.NameErr, logs.NameInfo
fileLog, err := logs.OpenFiles(root, ename, iname, nothing)
switch {
case err != nil:
// log file errors
log.Println(fmt.Errorf("%w: %w", ErrLog, err))
case envConfig.ProdMode.Bool():
// log file use in production mode
defer func() {
if err := fileLog.Close(); err != nil {
log.Println(err)
}
}()
default:
// log file in development mode
// simulate the creation and closing of the log file
_ = fileLog.Close()
fileLog = logs.NoFiles()
}
sl, cl, logo := setupWriters(*envConfig, fileLog)
logErr := func(msg string, err error) {
sl.Error(msg, slog.Any("error", err))
}
warnErr := func(msg string, err error) {
sl.Warn(msg, slog.Any("error", err))
}
// Print the configurations.
envConfig.Configurations(cl)
// Connect to the database and perform some record repairs and sanity checks. Even if the
// database cannot connect the web server will continue to run with limited functionality.
db, err := postgres.Open()
if err != nil {
logErr("cannot initialize the database", err)
}
if db == nil {
db, _ = sql.Open("pgx", "")
}
defer func() {
if err := db.Close(); err != nil {
warnErr("close database connection failure", err)
}
}()
var database postgres.Version
if err := database.Query(db); err != nil {
logErr("postgres cannot run the version query", err)
}
// Clean stale temporary directories created by this application.
const threeDays = 3 * 24 * time.Hour
if err := dir.CleanTemp(threeDays); err != nil {
logErr("abort clean stale temp", err)
}
config.TempInfo(sl)
// Start the web server.
parent := context.Background()
signals := []os.Signal{os.Interrupt, syscall.SIGTERM}
ctx, stop := signal.NotifyContext(parent, signals...)
defer stop()
serv := initialisation(ctx, db, *envConfig)
go func() {
fsys := serv.Public
root := tidbit.Dir
if err := serv.TidbitIndex.NewIndex(fsys, root); err != nil {
logErr("tidbit indexs", err)
}
stat := serv.TidbitIndex
slog.Info("Indexed Tidbits",
slog.Int("Documents", stat.TotalDocs),
slog.Int64("SearchTerms", stat.TotalTerms),
)
}()
go func() {
tidbit.Startup(ctx, sl, db)
}()
display(logo)
printOpening(sl, serv.RecordCount)
h := serv.Handler(ctx, sl, db)
if err := serv.Print(logo); err != nil {
warnErr("print server startup", err)
}
if err := serv.Start(ctx, sl, h, *envConfig); err != nil {
logErr("web server startup failure", err)
}
go func() {
printOwner(cl, msg)
printAddrs(cl, envConfig, msg)
}()
slog.Info("Shutdown", slog.String("Triggered", "Signal received"))
}
func setupWriters(envConfig config.Config, lf logs.Files) (
sl, cl *slog.Logger, logo io.Writer,
) {
// configure logo to stdout so it is ignored by systemd and the operating system
logo = os.Stdout
// general slog and level configuration used by the website
flag := logs.Defaults
stdmin := logs.LevelInfo
if quiet := bool(envConfig.Quiet); quiet {
logo = io.Discard
stdmin = logs.LevelError
flag = logs.Quiets
}
// configure the server logger and make it the default
sl = lf.New(stdmin, flag)
slog.SetDefault(sl)
// configuration command line logger flags
flag = logs.Configurations
stdmin = logs.LevelInfo
if quiet := bool(envConfig.Quiet); quiet {
stdmin = logs.LevelError
}
// print the server configuration and command line flag output to stdout
cf := logs.NoFiles()
cl = cf.New(stdmin, flag)
return sl, cl, logo
}
func display(w io.Writer) {
_, _ = fmt.Fprintln(w)
}
// printOwner returns the owner and group of the current process and
// writes them to the w io.writer.
func printOwner(cl *slog.Logger, msg string) {
groups, usr, err := helper.Owner()
if err != nil {
cl.Error(msg, slog.String("own_and_group", "could not obtain the current user of this process"),
slog.Any("error", err))
}
clean := slices.DeleteFunc(groups, func(e string) bool {
return e == ""
})
cl.Info(msg, slog.String("permissions", "Server running as the following user and groups"),
slog.String("User", usr), slog.String("Groups", strings.Join(clean, ",")))
}
// printAddrs returns the local IP addresses in use by the application and
// writes them to the w io.writer.
func printAddrs(cl *slog.Logger, envConfig *config.Config, msg string) {
err := envConfig.Addresses(cl)
if err != nil {
cl.Error(msg, slog.String("local_address", "could not obtain the usable addresses"),
slog.Any("error", err))
}
}
// environmentVars is used to parse the environment variables and set the Go runtime.
// Defaults are used if the environment variables are not set.
//
// The configuration uses reference types to make the values immutable.
func environmentVars(ctx context.Context, l *slog.Logger) *config.Config {
const msg = "environment variables"
configs := config.Config{
// paths
AbsLog: "",
AbsDownload: "",
AbsPreview: "",
AbsThumbnail: "",
AbsExtra: "",
AbsOrphaned: "",
// database
DatabaseURL: postgres.DefaultURL,
SessionKey: "",
// 3rd party
GoogleClientID: "",
GoogleIDs: "",
DemozooAuth: "",
// server
MatchHost: "",
TLSCert: "",
TLSKey: "",
GoogleAccounts: config.OAuth2s{},
HTTPPort: config.StdCustom,
MaxProcs: 0,
SessionMaxAge: config.SessionHours,
TLSPort: 0,
Quiet: false,
Compression: true,
ProdMode: true,
ReadOnly: true,
NoCrawl: false,
LogAll: false,
}
if err := env.Parse(&configs); err != nil {
logs.Fatal(ctx, l, msg,
slog.String("parsing_error", "does the variable contain an invalid value?"), slog.Any("error", err))
}
configs.Override()
if i := configs.MaxProcs; i > 0 {
runtime.GOMAXPROCS(int(i))
}
return &configs
}
// initialisation is used to create the server controller instance.
//
// The configuration returns a reference type to make the values immutable.
func initialisation(ctx context.Context, db *sql.DB, envConfig config.Config) *handler.Server {
c := handler.Server{
Public: public,
View: view,
Version: version,
Brand: brand,
Environment: envConfig,
RecordCount: 0,
TidbitIndex: fulltext.Tidbits{
TotalDocs: 0,
TotalTerms: 0,
},
}
if c.Version == "" {
c.Version = flags.VersionCommit("")
}
if ctx != nil && db != nil {
c.RecordCount = config.RecordCount(ctx, db)
}
return &c
}
// flagParser is used to parse the command line arguments.
// If an error is returned, the application will exit with the error code.
// Otherwise, a negative value is returned to indicate the application should continue.
func flagParser(l *slog.Logger, w io.Writer, envConfig *config.Config) flags.ExitCode {
const msg = "server flag parser, there was a problem parsing the command arguments"
if l == nil || envConfig == nil {
return flags.GenericErr
}
exitCode, err := flags.Run(w, version, envConfig)
if err != nil {
l.Error(msg, slog.Int("exit_code", int(exitCode)), slog.Any("error", err))
return exitCode
}
if use := exitCode >= flags.ExitOK; use {
return exitCode
}
return flags.Continue
}
// printOpening prints the welcome to message and returns the number
// of artifacts kept in the database. It customizes the log level based
// on the number of records vs the expected number.
func printOpening(sl *slog.Logger, count int64) {
const msg = "Welcome to the Defacto2 web application"
switch {
case count == 0:
s := ", but with no access to the database records"
sl.Error(msg + s)
case config.MinimumFiles > count:
s := " with too few records"
sl.Warn(msg+s,
slog.Int64("record_count", count),
slog.Int("minimum_requirement", config.MinimumFiles))
default:
sl.Info(msg, slog.Int64("Artifacts", count))
}
}