1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-30 09:56:23 -07:00

Implement database configuration and validation

This commit is contained in:
Eggbertx 2025-05-16 14:19:52 -07:00
parent 772bd265f9
commit 6a87d74258
8 changed files with 246 additions and 30 deletions

View file

@ -2,7 +2,10 @@ package main
import (
"bytes"
"context"
"database/sql"
"flag"
"fmt"
"html/template"
"net"
"net/http"
@ -15,8 +18,10 @@ import (
_ "embed"
"github.com/Eggbertx/go-forms"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
_ "github.com/gochan-org/gochan/pkg/gcsql/initsql"
"github.com/gochan-org/gochan/pkg/gctemplates"
"github.com/gochan-org/gochan/pkg/gcutil"
@ -45,11 +50,11 @@ func main() {
workingConfig := config.GetDefaultConfig()
flag.StringVar(&workingConfig.SiteHost, "host", "127.0.0.1", "Host to listen on")
flag.IntVar(&workingConfig.Port, "port", 8080, "Port to bind to")
flag.IntVar(&workingConfig.Port, "port", 0, "Port to bind to (REQUIRED)")
flag.BoolVar(&workingConfig.UseFastCGI, "fastcgi", false, "Use FastCGI instead of HTTP")
flag.StringVar(&workingConfig.WebRoot, "webroot", "/", "Web root path")
flag.StringVar(&workingConfig.TemplateDir, "template-dir", "", "Template directory")
flag.StringVar(&workingConfig.DocumentRoot, "document-root", "", "Document root directory")
flag.StringVar(&workingConfig.TemplateDir, "template-dir", "", "Template directory (REQUIRED)")
flag.StringVar(&workingConfig.DocumentRoot, "document-root", "", "Document root directory (REQUIRED)")
flag.Parse()
if jsonPath := config.GetGochanJSONPath(); jsonPath != "" {
@ -61,6 +66,13 @@ func main() {
config.SetSiteConfig(&workingConfig.SiteConfig)
config.SetSystemCriticalConfig(&workingConfig.SystemCriticalConfig)
systemCriticalConfig := config.GetSystemCriticalConfig()
if systemCriticalConfig.TemplateDir == "" {
flag.Usage()
fatalEv.Msg("-template-dir command line argument is required")
}
if err = initTemplates(); err != nil {
os.Exit(1)
}
@ -70,7 +82,6 @@ func main() {
router := server.GetRouter()
router.GET(path.Join(workingConfig.WebRoot, "/install"), installHandler)
router.POST(path.Join(workingConfig.WebRoot, "/install/:page"), installHandler)
// router.GET(path.Join(workingConfig.WebRoot, "/install/:page"), installHandler)
if workingConfig.DocumentRoot == "" {
fatalEv.Msg("-document-root command line argument is required")
@ -87,6 +98,7 @@ func main() {
}
}
}()
infoEv.Str("listenAddr", listenAddr).Msg("Starting installer server")
if workingConfig.UseFastCGI {
listener, err = net.Listen("tcp", listenAddr)
if err != nil {
@ -119,11 +131,6 @@ func initTemplates() error {
systemCriticalConfig := config.GetSystemCriticalConfig()
if systemCriticalConfig.TemplateDir == "" {
fatalEv.Msg("-template-dir command line argument is required")
return nil
}
if err = gctemplates.InitTemplates(); err != nil {
fatalEv.Err(err).Caller().Msg("Failed to initialize templates")
return err
@ -141,6 +148,63 @@ func initTemplates() error {
return nil
}
type dbForm struct {
DBType string `form:"dbtype,required,notempty" method:"POST"`
DBHost string `form:"dbhost,required,notempty" method:"POST"`
DBName string `form:"dbname,required,notempty" method:"POST"`
DBUser string `form:"dbuser,required,notempty" method:"POST"`
DBPass string `form:"dbpass,required" method:"POST"`
DBPrefix string `form:"dbprefix,required" method:"POST"`
}
func testDB(form *dbForm) (err error) {
var connStr string
var query string
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
switch form.DBType {
case "mysql":
connStr = fmt.Sprintf(gcsql.MySQLConnStr, form.DBUser, form.DBPass, form.DBHost, form.DBName)
query = `SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`
case "postgres":
connStr = fmt.Sprintf(gcsql.PostgresConnStr, form.DBUser, form.DBPass, form.DBHost, form.DBName)
query = `SELECT COUNT(*) FROM information_schema.TABLES WHERE table_catalog = CURRENT_DATABASE() AND table_name = ?`
case "sqlite3":
connStr = fmt.Sprintf(gcsql.SQLite3ConnStr, form.DBHost, form.DBUser, form.DBPass)
query = `SELECT COUNT(*) FROM sqlite_master WHERE name = ? AND type = 'table'`
default:
return gcsql.ErrUnsupportedDB
}
db, err := sql.Open(form.DBType, connStr)
if err != nil {
return err
}
defer db.Close()
var count int
stmt, err := db.PrepareContext(ctx, query)
if err != nil {
return err
}
defer stmt.Close()
if err = stmt.QueryRowContext(ctx, form.DBPrefix+"database_version").Scan(&count); err != nil {
return err
}
if count > 0 {
return fmt.Errorf("database already appears to have a Gochan installation (%sdatabase_version table already exists)", form.DBName)
}
if err = stmt.Close(); err != nil {
return err
}
if err = db.Close(); err != nil {
return err
}
return nil
}
func installHandler(writer http.ResponseWriter, req bunrouter.Request) (err error) {
infoEv, warnEv, errEv := gcutil.LogRequest(req.Request)
var buf bytes.Buffer
@ -156,21 +220,53 @@ func installHandler(writer http.ResponseWriter, req bunrouter.Request) (err erro
}()
var pageTitle string
page := req.Param("page")
systemCriticalConfig := config.GetSystemCriticalConfig()
data := map[string]any{
"page": page,
"page": page,
"systemCriticalConfig": systemCriticalConfig,
"siteConfig": config.GetSiteConfig(),
}
var stopServer bool
switch page {
case "":
pageTitle = "Gochan Installation"
data["nextPage"] = "license"
case "license":
pageTitle = "License"
data["license"] = licenseTxt
data["nextPage"] = "paths"
case "paths":
pageTitle = "Paths"
data["nextPage"] = "database"
case "database":
pageTitle = "Database Setup"
data["nextPage"] = "dbtest"
case "dbtest":
pageTitle = "Database Test"
var dbFormData dbForm
if err = forms.FillStructFromForm(req.Request, &dbFormData); err != nil {
httpStatus = http.StatusBadRequest
errEv.Err(err).Msg("Failed to fill form data")
return
}
if err = testDB(&dbFormData); err != nil {
httpStatus = http.StatusBadRequest
errEv.Err(err).Msg("Database test failed")
return err
}
systemCriticalConfig.DBtype = dbFormData.DBType
systemCriticalConfig.DBhost = dbFormData.DBHost
systemCriticalConfig.DBname = dbFormData.DBName
systemCriticalConfig.DBusername = dbFormData.DBUser
systemCriticalConfig.DBpassword = dbFormData.DBPass
systemCriticalConfig.DBprefix = dbFormData.DBPrefix
config.SetSystemCriticalConfig(systemCriticalConfig)
data["nextPage"] = "install"
case "stop":
writer.Write([]byte("Stopping server..."))
installServerStopper <- 1 // Stop the server
return nil
stopServer = true
default:
httpStatus = http.StatusNotFound
pageTitle = "Page Not Found"
}
if err = building.BuildPageHeader(&buf, pageTitle, "", data); err != nil {
@ -188,6 +284,9 @@ func installHandler(writer http.ResponseWriter, req bunrouter.Request) (err erro
errEv.Err(err).Msg("Failed to build page footer")
return
}
if stopServer {
installServerStopper <- 1
}
return nil
}

View file

@ -6,6 +6,7 @@
@use "global/watcher";
@use 'global/bans';
@use 'global/animations';
@use 'global/installer';
.increase-line-height {
header, .post, .reply {

View file

@ -0,0 +1,22 @@
.install-container {
margin: 0 auto;
max-width: 80%;
padding: 1em 0;
textarea.license {
width: 50%;
height: 16em;
margin: 1em auto;
display: block;
}
.buttons {
display: block;
text-align: center;
margin-top: 0.5em;
}
table {
margin: 0 auto;
}
}

View file

@ -686,6 +686,26 @@ img#banpage-image {
margin: 4px 8px 8px 4px;
}
.install-container {
margin: 0 auto;
max-width: 80%;
padding: 1em 0;
}
.install-container textarea.license {
width: 50%;
height: 16em;
margin: 1em auto;
display: block;
}
.install-container .buttons {
display: block;
text-align: center;
margin-top: 0.5em;
}
.install-container table {
margin: 0 auto;
}
.increase-line-height header, .increase-line-height .post, .increase-line-height .reply {
line-height: 1.5;
}

View file

@ -0,0 +1,19 @@
.install-container {
margin: 0 auto;
max-width: 80%;
padding: 1em 0;
}
.install-container textarea.license {
width: 50%;
height: 16em;
margin: 1em auto;
display: block;
}
.install-container .buttons {
display: block;
text-align: center;
margin-top: 0.5em;
}
.install-container table {
margin: 0 auto;
}

View file

@ -23,9 +23,9 @@ const (
gochanVersionKeyConstant = "gochan"
DatabaseVersion = 6
UnsupportedSQLVersionMsg = `syntax error in SQL query, confirm you are using a supported driver and SQL server (error text: %s)`
mysqlConnStr = "%s:%s@tcp(%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci"
postgresConnStr = "postgres://%s:%s@%s/%s?sslmode=disable"
sqlite3ConnStr = "file:%s?_auth&_auth_user=%s&_auth_pass=%s&_auth_crypt=sha1"
MySQLConnStr = "%s:%s@tcp(%s)/%s?parseTime=true&collation=utf8mb4_unicode_ci"
PostgresConnStr = "postgres://%s:%s@%s/%s?sslmode=disable"
SQLite3ConnStr = "file:%s?_auth&_auth_user=%s&_auth_pass=%s&_auth_crypt=sha1"
)
var (
@ -353,14 +353,14 @@ func setupDBConn(cfg *config.SQLConfig) (db *GCDB, err error) {
}
switch cfg.DBtype {
case "mysql":
db.connStr = fmt.Sprintf(mysqlConnStr, cfg.DBusername, cfg.DBpassword, cfg.DBhost, cfg.DBname)
db.connStr = fmt.Sprintf(MySQLConnStr, cfg.DBusername, cfg.DBpassword, cfg.DBhost, cfg.DBname)
replacerArr = append(replacerArr, mysqlReplacerArr...)
mysql.SetLogger(gcutil.Logger())
case "postgres":
db.connStr = fmt.Sprintf(postgresConnStr, cfg.DBusername, cfg.DBpassword, cfg.DBhost, cfg.DBname)
db.connStr = fmt.Sprintf(PostgresConnStr, cfg.DBusername, cfg.DBpassword, cfg.DBhost, cfg.DBname)
replacerArr = append(replacerArr, postgresReplacerArr...)
case "sqlite3":
db.connStr = fmt.Sprintf(sqlite3ConnStr, cfg.DBhost, cfg.DBusername, cfg.DBpassword)
db.connStr = fmt.Sprintf(SQLite3ConnStr, cfg.DBhost, cfg.DBusername, cfg.DBpassword)
replacerArr = append(replacerArr, sqlite3ReplacerArr...)
default:
return nil, ErrUnsupportedDB

View file

@ -20,12 +20,7 @@ type templateRef interface {
// InitMinifier sets up the HTML/JS/JSON minifier if enabled in gochan.json
func InitMinifier() {
var siteConfig *config.SiteConfig
if config.GetInitialSetupStatus() == config.InitialSetupComplete {
siteConfig = config.GetSiteConfig()
} else {
siteConfig = &config.GetDefaultConfig().SiteConfig
}
siteConfig := config.GetSiteConfig()
minifier = minify.New()
if siteConfig.MinifyHTML {
@ -43,9 +38,6 @@ func canMinify(mediaType string) (minify bool) {
InitMinifier()
}
}()
if config.GetInitialSetupStatus() != config.InitialSetupComplete {
return true
}
siteConfig := config.GetSiteConfig()
if mediaType == "text/html" && siteConfig.MinifyHTML {
return true

View file

@ -1,5 +1,68 @@
<form class="install-container" method="POST" action="/install/{{.nextPage}}">
{{if eq "" .page}}
Welcome to the Gochan installer! This installer will help you configure Gochan to run on your system, including setting the necessary directories and connecting to the SQL database.
<p>Welcome to the Gochan installer! This installer will help you configure Gochan, including setting the necessary directories and connecting to the SQL database, in preparation for running a fresh Gochan installation or migrating another imageboard database (if supported) to Gochan.</p>
<p class="text-bold">This does not install files like templates, or provision the database. It only creates a configuration file for gochan to use.</p>
{{else if eq .page "license"}}
<p class="text-center">Gochan is licensed under the BSD 3-Clause License, shown below. By using Gochan, you agree to the terms of this license,</p>
<textarea class="license">{{.license}}</textarea>
{{else if eq .page "paths"}}
<table>
<tr>
<th>Output gochan.json Location</th>
<td><input type="text" name="configpath" required/></td>
</tr>
<tr>
<th>Templates Directory</th>
<td><input type="text" name="templatedir" value="{{.systemCriticalConfig.TemplateDir}}"/></td>
</tr>
<tr>
<th>Log Directory</th>
<td><input type="text" name="logdir"/></td>
</tr>
<tr>
<th>Web Root</th>
<td><input type="text" name="webroot" value="{{.systemCriticalConfig.WebRoot}}"/></td>
</tr>
</table>
{{else if eq .page "database"}}
<table>
<tr>
<th>SQL Provider</th>
<td><select name="dbtype" required>
<option value="" disabled selected>Select a database</option>
<option value="mysql">MySQL/MariaDB</option>
<option value="postgres">PostgreSQL</option>
<option value="sqlite3">SQLite</option>
</select></td>
</tr>
<tr>
<th>Database Host</th>
<td><input type="text" name="dbhost" required/></td>
</tr>
<tr>
<th>Database Name</th>
<td><input type="text" name="dbname" required/></td>
</tr>
<tr>
<th>Database User</th>
<td><input type="text" name="dbuser" required/></td>
</tr>
<tr>
<th>Database Password</th>
<td><input type="password" name="dbpass"/></td>
</tr>
<tr>
<th>Database Prefix</th>
<td><input type="text" name="dbprefix"/></td>
</tr>
</table>
{{else if eq .page "dbtest"}}
<p class="text-center">Database connection was successful! Click next to continue.</p>
{{else}}
{{end}}
<p class="text-center">Invalid page</p>
{{end}}
<section class="buttons">
<input type="submit" value="{{if eq `finish` .nextPage}}Finish{{else}}Next{{end}}" />
</section>
</form>