mirror of
https://github.com/thomiceli/opengist.git
synced 2025-07-09 09:28:03 +02:00
Add Postgres and MySQL databases support (#335)
This commit is contained in:
@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type AdminSetting struct {
|
||||
Key string `gorm:"uniqueIndex"`
|
||||
Key string `gorm:"index:,unique"`
|
||||
Value string
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ func UpdateSetting(key string, value string) error {
|
||||
}
|
||||
|
||||
func setSetting(key string, value string) error {
|
||||
return db.Create(&AdminSetting{Key: key, Value: value}).Error
|
||||
return db.FirstOrCreate(&AdminSetting{Key: key, Value: value}, &AdminSetting{Key: key}).Error
|
||||
}
|
||||
|
||||
func initAdminSettings(settings map[string]string) error {
|
||||
@ -64,9 +64,9 @@ func initAdminSettings(settings map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type DBAuthInfo struct{}
|
||||
type AuthInfo struct{}
|
||||
|
||||
func (auth DBAuthInfo) RequireLogin() (bool, error) {
|
||||
func (auth AuthInfo) RequireLogin() (bool, error) {
|
||||
s, err := GetSetting(SettingRequireLogin)
|
||||
if err != nil {
|
||||
return true, err
|
||||
@ -74,7 +74,7 @@ func (auth DBAuthInfo) RequireLogin() (bool, error) {
|
||||
return s == "1", nil
|
||||
}
|
||||
|
||||
func (auth DBAuthInfo) AllowGistsWithoutLogin() (bool, error) {
|
||||
func (auth AuthInfo) AllowGistsWithoutLogin() (bool, error) {
|
||||
s, err := GetSetting(SettingAllowGistsWithoutLogin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -2,38 +2,133 @@ package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm/logger"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
msqlite "github.com/glebarez/go-sqlite"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/config"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func Setup(dbPath string, sharedCache bool) error {
|
||||
var err error
|
||||
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
||||
const (
|
||||
SQLite databaseType = iota
|
||||
PostgreSQL
|
||||
MySQL
|
||||
)
|
||||
|
||||
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
||||
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
|
||||
type databaseType int
|
||||
|
||||
func (d databaseType) String() string {
|
||||
return [...]string{"SQLite", "PostgreSQL", "MySQL"}[d]
|
||||
}
|
||||
|
||||
type databaseInfo struct {
|
||||
Type databaseType
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
}
|
||||
|
||||
var DatabaseInfo *databaseInfo
|
||||
|
||||
func parseDBURI(uri string) (*databaseInfo, error) {
|
||||
info := &databaseInfo{}
|
||||
|
||||
if !strings.Contains(uri, "://") {
|
||||
info.Type = SQLite
|
||||
if uri == "file::memory:" {
|
||||
info.Database = "file::memory:"
|
||||
return info, nil
|
||||
}
|
||||
info.Database = filepath.Join(config.GetHomeDir(), uri)
|
||||
return info, nil
|
||||
}
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid URI: %v", err)
|
||||
}
|
||||
|
||||
sharedCacheStr := ""
|
||||
if sharedCache {
|
||||
sharedCacheStr = "&cache=shared"
|
||||
switch u.Scheme {
|
||||
case "postgres", "postgresql":
|
||||
info.Type = PostgreSQL
|
||||
case "mysql", "mariadb":
|
||||
info.Type = MySQL
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown database: %v", err)
|
||||
}
|
||||
|
||||
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}); err != nil {
|
||||
if u.Host != "" {
|
||||
host, port, _ := strings.Cut(u.Host, ":")
|
||||
info.Host = host
|
||||
info.Port = port
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
info.User = u.User.Username()
|
||||
info.Password, _ = u.User.Password()
|
||||
}
|
||||
|
||||
switch info.Type {
|
||||
case PostgreSQL, MySQL:
|
||||
info.Database = strings.TrimPrefix(u.Path, "/")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown database: %v", err)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func Setup(dbUri string, sharedCache bool) error {
|
||||
dbInfo, err := parseDBURI(dbUri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Msgf("Setting up a %s database connection", dbInfo.Type)
|
||||
var setupFunc func(databaseInfo, bool) error
|
||||
switch dbInfo.Type {
|
||||
case SQLite:
|
||||
setupFunc = setupSQLite
|
||||
case PostgreSQL:
|
||||
setupFunc = setupPostgres
|
||||
case MySQL:
|
||||
setupFunc = setupMySQL
|
||||
default:
|
||||
return fmt.Errorf("unknown database type: %v", dbInfo.Type)
|
||||
}
|
||||
|
||||
maxAttempts := 60
|
||||
retryInterval := 1 * time.Second
|
||||
|
||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||
err = setupFunc(*dbInfo, sharedCache)
|
||||
if err == nil {
|
||||
log.Info().Msg("Database connection established")
|
||||
break
|
||||
}
|
||||
|
||||
if attempt < maxAttempts {
|
||||
log.Warn().Err(err).Msgf("Failed to connect to database (attempt %d), retrying in %v...", attempt, retryInterval)
|
||||
time.Sleep(retryInterval)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseInfo = dbInfo
|
||||
|
||||
if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -46,7 +141,7 @@ func Setup(dbPath string, sharedCache bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ApplyMigrations(db); err != nil {
|
||||
if err = applyMigrations(db, dbInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -75,11 +170,7 @@ func CountAll(table interface{}) (int64, error) {
|
||||
}
|
||||
|
||||
func IsUniqueConstraintViolation(err error) bool {
|
||||
var sqliteErr *msqlite.Error
|
||||
if errors.As(err, &sqliteErr) && sqliteErr.Code() == 2067 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return errors.Is(err, gorm.ErrDuplicatedKey)
|
||||
}
|
||||
|
||||
func Ping() error {
|
||||
@ -90,3 +181,65 @@ func Ping() error {
|
||||
|
||||
return sql.Ping()
|
||||
}
|
||||
|
||||
func setupSQLite(dbInfo databaseInfo, sharedCache bool) error {
|
||||
var err error
|
||||
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
|
||||
|
||||
if !slices.Contains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
|
||||
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
|
||||
}
|
||||
|
||||
sharedCacheStr := ""
|
||||
if sharedCache {
|
||||
sharedCacheStr = "&cache=shared"
|
||||
}
|
||||
|
||||
db, err = gorm.Open(sqlite.Open(dbInfo.Database+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
TranslateError: true,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func setupPostgres(dbInfo databaseInfo, sharedCache bool) error {
|
||||
var err error
|
||||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", dbInfo.Host, dbInfo.Port, dbInfo.User, dbInfo.Password, dbInfo.Database)
|
||||
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
TranslateError: true,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func setupMySQL(dbInfo databaseInfo, sharedCache bool) error {
|
||||
var err error
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbInfo.User, dbInfo.Password, dbInfo.Host, dbInfo.Port, dbInfo.Database)
|
||||
|
||||
db, err = gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: dsn,
|
||||
DontSupportRenameIndex: true,
|
||||
}), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
TranslateError: true,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func DeprecationDBFilename() {
|
||||
if config.C.DBFilename != "" {
|
||||
log.Warn().Msg("The 'db-filename'/'OG_DB_FILENAME' configuration option is deprecated and will be removed in a future version. Please use 'db-uri'/'OG_DB_URI' instead.")
|
||||
}
|
||||
|
||||
if config.C.DBUri == "" {
|
||||
config.C.DBUri = config.C.DBFilename
|
||||
}
|
||||
}
|
||||
|
||||
func TruncateDatabase() error {
|
||||
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
@ -15,10 +16,21 @@ type Invitation struct {
|
||||
|
||||
func GetAllInvitations() ([]*Invitation, error) {
|
||||
var invitations []*Invitation
|
||||
err := db.
|
||||
Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) desc").
|
||||
Order("id asc").
|
||||
Find(&invitations).Error
|
||||
dialect := db.Dialector.Name()
|
||||
query := db.Model(&Invitation{})
|
||||
|
||||
switch dialect {
|
||||
case "sqlite":
|
||||
query = query.Order("(((expires_at >= strftime('%s', 'now')) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
|
||||
case "postgres":
|
||||
query = query.Order("(((expires_at >= EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
|
||||
case "mysql":
|
||||
query = query.Order("(((expires_at >= UNIX_TIMESTAMP()) AND ((nb_max <= 0) OR (nb_used < nb_max)))) DESC")
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database dialect: %s", dialect)
|
||||
}
|
||||
|
||||
err := query.Order("id ASC").Find(&invitations).Error
|
||||
|
||||
return invitations, err
|
||||
}
|
||||
|
@ -11,7 +11,19 @@ type MigrationVersion struct {
|
||||
Version uint
|
||||
}
|
||||
|
||||
func ApplyMigrations(db *gorm.DB) error {
|
||||
func applyMigrations(db *gorm.DB, dbInfo *databaseInfo) error {
|
||||
switch dbInfo.Type {
|
||||
case SQLite:
|
||||
return applySqliteMigrations(db)
|
||||
case PostgreSQL, MySQL:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown database type: %s", dbInfo.Type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func applySqliteMigrations(db *gorm.DB) error {
|
||||
// Create migration table if it doesn't exist
|
||||
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating migration version table")
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex"`
|
||||
Username string `gorm:"uniqueIndex,size:191"`
|
||||
Password string
|
||||
IsAdmin bool
|
||||
CreatedAt int64
|
||||
|
Reference in New Issue
Block a user