Ronbun/cmd/web/main.go

239 lines
5.3 KiB
Go

package main
import (
"crypto/tls"
"database/sql"
"flag"
"html/template"
"log/slog"
"net/http"
"os"
"sync"
"time"
"github.com/Thomasorus/Ronbun-CMS/internal/models"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
_ "github.com/ncruces/go-sqlite3/driver"
)
// Create a struct for dependencies injection
type application struct {
debug bool
devMode bool
logger *slog.Logger
pages *models.PageModel
users *models.UserModel
templateCache map[string]*template.Template
formDecoder *form.Decoder
sessionManager *scs.SessionManager
ogCache sync.Map
}
func main() {
// Parse command line arguments
addr := flag.String("addr", ":4000", "HTTP network address")
dsn := flag.String("dsn", "file:ronbun.db", "BDD data source name")
debug := flag.Bool("debug", false, "Enable debug mode")
devMode := flag.Bool("dev", false, "Enable dev mode")
// Create shared logger
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true,
}))
db, err := openDB(*dsn)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
// Close the DB connection when main() is terminated
defer db.Close()
// Check if database has tables
tables, err := checkTables(db)
if err != nil {
logger.Error("failed to check tables", "err", err)
os.Exit(1)
}
// If no tables that's the first launch, create the tables
if !tables {
if err := createTables(db); err != nil {
logger.Error("failed to create tables", "err", err)
os.Exit(1)
}
}
err = os.MkdirAll("uploaded", 0755)
if err != nil {
logger.Error(err.Error())
}
err = os.MkdirAll("processed", 0755)
if err != nil {
logger.Error(err.Error())
}
filesIsEmpty, err := isFolderEmpty("./uploaded")
if err != nil {
logger.Error("failed to check uploaded folder", "err", err)
os.Exit(1)
}
processedIsEmpty, err := isFolderEmpty("./processed")
if err != nil {
logger.Error("failed to check processed folder", "err", err)
os.Exit(1)
}
if !filesIsEmpty && processedIsEmpty {
imgProc := NewImageProcessor()
err = imgProc.ProcessBatch("uploaded")
if err != nil {
logger.Error(err.Error())
}
}
// Initialize Template cache
templateCache, err := newTemplateCache()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
formDecoder := form.NewDecoder()
sessionManager := scs.New()
sessionManager.Store = sqlite3store.New(db)
sessionManager.Lifetime = 730 * time.Hour
sessionManager.Cookie.Secure = true
// Define application and its dependencies
app := &application{
debug: *debug,
devMode: *devMode,
logger: logger,
pages: &models.PageModel{DB: db},
users: &models.UserModel{DB: db},
templateCache: templateCache,
formDecoder: formDecoder,
sessionManager: sessionManager,
}
tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}
// Serve the routes from app.routes
srv := &http.Server{
Addr: *addr,
Handler: app.routes(),
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
TLSConfig: tlsConfig,
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
logger.Info("Starting server", "addr", srv.Addr)
if !app.devMode {
err = srv.ListenAndServe()
} else {
err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem")
}
logger.Error(err.Error())
os.Exit(1)
}
// Initializes the DB Pool, creates a connection and pings to see if db is working
func openDB(dsn string) (*sql.DB, error) {
// Does not create a connection but initializes the pool
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, err
}
// Creates a connection to ping and check if the DB is working
err = db.Ping()
if err != nil {
db.Close()
return nil, err
}
return db, nil
}
func checkTables(db *sql.DB) (bool, error) {
stmt := "SELECT name FROM sqlite_master WHERE type='table' AND name='pages'"
var tableName string
err := db.QueryRow(stmt).Scan(&tableName)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func createTables(db *sql.DB) error {
stmt := `CREATE TABLE pages (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL,
host INTEGER NOT NULL DEFAULT 0,
category VARCHAR(100) NOT NULL,
summary TEXT NOT NULL,
body TEXT NOT NULL,
created DATETIME NOT NULL,
updated DATETIME NOT NULL,
private INT NOT NULL DEFAULT 0,
menu INT NOT NULL DEFAULT 0,
feed INT NOT NULL DEFAULT 0
);
CREATE TABLE sessions (
token TEXT PRIMARY KEY,
data BLOB NOT NULL,
expiry REAL NOT NULL
);
CREATE INDEX sessions_expiry_idx ON sessions(expiry);
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
theme VARCHAR(255) NOT NULL,
hashed_password CHAR(60) NOT NULL,
created DATETIME NOT NULL
);
CREATE UNIQUE INDEX users_uc_email ON users (email);`
_, err := db.Exec(stmt)
if err != nil {
return err
}
print("\nTables Created!\n\n")
return nil
}
func isFolderEmpty(path string) (bool, error) {
entries, err := os.ReadDir(path)
if err != nil {
return false, err
}
return len(entries) == 0, nil
}