Vendor the front-end assets and drop the third-party CDN dependencies (BootstrapCDN is being sunset): the bootstrap 4.0.0 css/js, jquery 3.2.1 slim, and popper 1.12.9 now live under static/ and are served from the app, byte-for-byte identical to the previous SRI-pinned files. Migrate CI from Drone to a Gitea Actions workflow that runs docker build . on push, with the checkout action pinned by SHA. Bring the repo up to standard: - add REPO_POLICIES.md, .editorconfig, .dockerignore, .golangci.yml, and a comprehensive root-anchored .gitignore - rewrite the Makefile with the required test/lint/fmt/fmt-check/check/ docker/hooks targets (golangci-lint, 30s test timeout, verbose rerun on failure, check modifies nothing) - rewrite the Dockerfile as a hash-pinned multistage build: a lint stage (golangci-lint), a glibc build+test stage (the legacy sqlite driver needs cgo+glibc), and a debian-slim runtime carrying the binary, templates, and static assets - add real tests for the hn package - bring the code into golangci-lint (default: all) compliance: fix the malformed gorm struct tags, check previously-ignored errors, dispatch the zerolog error event, avoid a uint->Duration overflow, split long functions, and add doc comments — all behaviour-preserving - expand the README with the required sections
146 lines
3.0 KiB
Go
146 lines
3.0 KiB
Go
package hn
|
|
|
|
import (
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/jinzhu/gorm"
|
|
// sqlite3 dialect registered with gorm via import side effects.
|
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
"github.com/labstack/echo"
|
|
"github.com/labstack/echo/middleware"
|
|
gl "github.com/labstack/gommon/log"
|
|
"github.com/mattn/go-isatty"
|
|
ep2 "github.com/mayowa/echo-pongo2"
|
|
"github.com/ziflex/lecho/v2"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// App holds the running server's dependencies: the echo instance, the
|
|
// logger, the database handle, the startup time, and the background fetcher.
|
|
type App struct {
|
|
version string
|
|
buildarch string
|
|
e *echo.Echo
|
|
log *zerolog.Logger
|
|
db *gorm.DB
|
|
startup time.Time
|
|
fetcher *Fetcher
|
|
}
|
|
|
|
// RunServer boots the application, starts the background fetcher, and blocks
|
|
// serving HTTP until the server exits, returning a process exit code.
|
|
func RunServer(version string, buildarch string) int {
|
|
a := new(App)
|
|
a.version = version
|
|
a.buildarch = buildarch
|
|
a.startup = time.Now()
|
|
|
|
a.init()
|
|
defer func() { _ = a.db.Close() }()
|
|
|
|
a.fetcher = NewFetcher(a.db)
|
|
a.fetcher.AddLogger(a.log)
|
|
|
|
go a.fetcher.run()
|
|
|
|
return a.runForever()
|
|
}
|
|
|
|
func (a *App) init() {
|
|
// setup logging
|
|
l := log.With().Caller().Logger()
|
|
log.Logger = l
|
|
|
|
tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
|
|
if tty {
|
|
out := zerolog.NewConsoleWriter(
|
|
func(w *zerolog.ConsoleWriter) {
|
|
// Customize time format
|
|
w.TimeFormat = time.RFC3339
|
|
},
|
|
)
|
|
log.Logger = log.Output(out)
|
|
}
|
|
|
|
// always log in UTC
|
|
zerolog.TimestampFunc = func() time.Time {
|
|
return time.Now().UTC()
|
|
}
|
|
|
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
|
|
if os.Getenv("DEBUG") != "" {
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
}
|
|
|
|
a.log = &log.Logger
|
|
|
|
a.identify()
|
|
|
|
// database path defaults to the container data volume, overridable
|
|
// with the DATABASE_PATH environment variable
|
|
dbPath := "/data/storage.sqlite"
|
|
if os.Getenv("DATABASE_PATH") != "" {
|
|
dbPath = os.Getenv("DATABASE_PATH")
|
|
}
|
|
|
|
db, err := gorm.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
panic("failed to open database: " + err.Error())
|
|
}
|
|
|
|
a.db = db
|
|
}
|
|
|
|
func (a *App) identify() {
|
|
log.Info().
|
|
Str("version", a.version).
|
|
Str("buildarch", a.buildarch).
|
|
Msg("starting")
|
|
}
|
|
|
|
func (a *App) runForever() int {
|
|
// Echo instance
|
|
a.e = echo.New()
|
|
|
|
lev := gl.INFO
|
|
if os.Getenv("DEBUG") != "" {
|
|
lev = gl.DEBUG
|
|
}
|
|
|
|
logger := lecho.New(
|
|
os.Stdout,
|
|
lecho.WithLevel(lev),
|
|
lecho.WithTimestamp(),
|
|
lecho.WithCaller(),
|
|
)
|
|
a.e.Logger = logger
|
|
a.e.Use(middleware.RequestID())
|
|
|
|
// Middleware
|
|
a.e.Use(middleware.Logger())
|
|
a.e.Use(middleware.Recover())
|
|
|
|
r, err := ep2.NewRenderer("view")
|
|
if err != nil {
|
|
a.e.Logger.Fatal(err)
|
|
}
|
|
|
|
a.e.Renderer = r
|
|
|
|
rhs := NewRequestHandlerSet(a.version, a.db)
|
|
|
|
// Routes
|
|
a.e.Static("/static", "static")
|
|
a.e.GET("/", rhs.indexHandler)
|
|
a.e.GET("/about", rhs.aboutHandler)
|
|
|
|
// Start server (blocks; Fatal exits the process on error)
|
|
a.e.Logger.Fatal(a.e.Start(":8080"))
|
|
|
|
return 0
|
|
}
|