2023-09-02 22:30:57 +00:00
package db
2023-03-14 15:22:52 +00:00
import (
2023-04-17 12:25:39 +00:00
"errors"
2024-09-20 14:01:09 +00:00
"fmt"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm/logger"
"net/url"
"path/filepath"
2024-01-04 04:11:46 +00:00
"slices"
2023-06-16 16:08:33 +00:00
"strings"
2024-09-20 14:01:09 +00:00
"time"
2023-06-16 16:08:33 +00:00
2023-06-09 13:25:41 +00:00
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
2023-03-14 15:22:52 +00:00
"gorm.io/gorm"
)
var db * gorm . DB
2024-09-20 14:01:09 +00:00
const (
SQLite databaseType = iota
PostgreSQL
MySQL
)
2023-03-14 15:22:52 +00:00
2024-09-20 14:01:09 +00:00
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 )
2023-06-09 13:25:41 +00:00
}
2024-09-20 14:01:09 +00:00
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 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 )
2023-09-16 22:59:47 +00:00
}
2024-09-20 14:01:09 +00:00
return info , nil
}
func Setup ( dbUri string , sharedCache bool ) error {
dbInfo , err := parseDBURI ( dbUri )
if err != nil {
2023-03-14 15:22:52 +00:00
return err
}
2024-09-20 14:01:09 +00:00
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
2023-06-21 16:19:17 +00:00
if err = db . SetupJoinTable ( & Gist { } , "Likes" , & Like { } ) ; err != nil {
return err
}
if err = db . SetupJoinTable ( & User { } , "Liked" , & Like { } ) ; err != nil {
return err
}
2024-10-24 21:23:00 +00:00
if err = db . AutoMigrate ( & User { } , & Gist { } , & SSHKey { } , & AdminSetting { } , & Invitation { } , & WebAuthnCredential { } , & TOTP { } ) ; err != nil {
2023-03-14 15:22:52 +00:00
return err
}
2024-09-20 14:01:09 +00:00
if err = applyMigrations ( db , dbInfo ) ; err != nil {
2023-09-17 00:55:17 +00:00
return err
}
2023-04-18 00:33:19 +00:00
2023-04-16 22:17:06 +00:00
// Default admin setting values
return initAdminSettings ( map [ string ] string {
2024-05-12 21:40:11 +00:00
SettingDisableSignup : "0" ,
SettingRequireLogin : "0" ,
SettingAllowGistsWithoutLogin : "0" ,
SettingDisableLoginForm : "0" ,
SettingDisableGravatar : "0" ,
2023-04-16 22:17:06 +00:00
} )
2023-03-14 15:22:52 +00:00
}
2023-09-16 22:59:47 +00:00
func Close ( ) error {
sqlDB , err := db . DB ( )
if err != nil {
return err
}
return sqlDB . Close ( )
}
2023-03-14 15:22:52 +00:00
func CountAll ( table interface { } ) ( int64 , error ) {
var count int64
err := db . Model ( table ) . Count ( & count ) . Error
return count , err
}
2023-04-17 12:25:39 +00:00
func IsUniqueConstraintViolation ( err error ) bool {
2024-09-20 14:01:09 +00:00
return errors . Is ( err , gorm . ErrDuplicatedKey )
2023-04-17 12:25:39 +00:00
}
2023-12-16 00:27:00 +00:00
func Ping ( ) error {
sql , err := db . DB ( )
if err != nil {
return err
}
return sql . Ping ( )
}
2024-09-20 14:01:09 +00:00
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 {
2024-10-24 21:23:00 +00:00
return db . Migrator ( ) . DropTable ( "likes" , & User { } , "gists" , & SSHKey { } , & AdminSetting { } , & Invitation { } , & WebAuthnCredential { } , & TOTP { } )
2024-09-20 14:01:09 +00:00
}