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:
parent
772bd265f9
commit
6a87d74258
8 changed files with 246 additions and 30 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@use "global/watcher";
|
||||
@use 'global/bans';
|
||||
@use 'global/animations';
|
||||
@use 'global/installer';
|
||||
|
||||
.increase-line-height {
|
||||
header, .post, .reply {
|
||||
|
|
22
frontend/sass/global/installer.scss
Normal file
22
frontend/sass/global/installer.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
19
html/css/global/installer.css
Normal file
19
html/css/global/installer.css
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue