From 6a87d74258d5816d4501c645601f86e4ba4d945c Mon Sep 17 00:00:00 2001 From: Eggbertx Date: Fri, 16 May 2025 14:19:52 -0700 Subject: [PATCH] Implement database configuration and validation --- cmd/gochan-installer/main.go | 125 +++++++++++++++++++++++++--- frontend/sass/global.scss | 1 + frontend/sass/global/installer.scss | 22 +++++ html/css/global.css | 20 +++++ html/css/global/installer.css | 19 +++++ pkg/gcsql/database.go | 12 +-- pkg/server/serverutil/minifier.go | 10 +-- templates/install.html | 67 ++++++++++++++- 8 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 frontend/sass/global/installer.scss create mode 100644 html/css/global/installer.css diff --git a/cmd/gochan-installer/main.go b/cmd/gochan-installer/main.go index 96efbbce..223c0eb5 100644 --- a/cmd/gochan-installer/main.go +++ b/cmd/gochan-installer/main.go @@ -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 } diff --git a/frontend/sass/global.scss b/frontend/sass/global.scss index 3c3a4d31..6c606452 100644 --- a/frontend/sass/global.scss +++ b/frontend/sass/global.scss @@ -6,6 +6,7 @@ @use "global/watcher"; @use 'global/bans'; @use 'global/animations'; +@use 'global/installer'; .increase-line-height { header, .post, .reply { diff --git a/frontend/sass/global/installer.scss b/frontend/sass/global/installer.scss new file mode 100644 index 00000000..b0a09265 --- /dev/null +++ b/frontend/sass/global/installer.scss @@ -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; + } +} \ No newline at end of file diff --git a/html/css/global.css b/html/css/global.css index bf74b922..67dd17bb 100644 --- a/html/css/global.css +++ b/html/css/global.css @@ -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; } diff --git a/html/css/global/installer.css b/html/css/global/installer.css new file mode 100644 index 00000000..8c4a4d98 --- /dev/null +++ b/html/css/global/installer.css @@ -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; +} diff --git a/pkg/gcsql/database.go b/pkg/gcsql/database.go index bcbe4812..4b6750d8 100644 --- a/pkg/gcsql/database.go +++ b/pkg/gcsql/database.go @@ -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 diff --git a/pkg/server/serverutil/minifier.go b/pkg/server/serverutil/minifier.go index 9a3f35cd..c334922a 100644 --- a/pkg/server/serverutil/minifier.go +++ b/pkg/server/serverutil/minifier.go @@ -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 diff --git a/templates/install.html b/templates/install.html index 32e296a1..31766a87 100644 --- a/templates/install.html +++ b/templates/install.html @@ -1,5 +1,68 @@ +
{{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. +

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.

+

This does not install files like templates, or provision the database. It only creates a configuration file for gochan to use.

+ {{else if eq .page "license"}} +

Gochan is licensed under the BSD 3-Clause License, shown below. By using Gochan, you agree to the terms of this license,

+ +{{else if eq .page "paths"}} + + + + + + + + + + + + + + + + + +
Output gochan.json Location
Templates Directory
Log Directory
Web Root
+{{else if eq .page "database"}} + + + + + + + + + + + + + + + + + + + + + + + + + +
SQL Provider
Database Host
Database Name
Database User
Database Password
Database Prefix
+{{else if eq .page "dbtest"}} +

Database connection was successful! Click next to continue.

{{else}} -{{end}} \ No newline at end of file +

Invalid page

+{{end}} +
+ +
+
\ No newline at end of file