2020-01-02 00:03:07 +01:00
package config
import (
"errors"
2020-12-11 12:46:28 +01:00
"fmt"
2021-10-06 07:10:50 +02:00
"os"
2020-05-30 14:52:47 +02:00
"path/filepath"
2020-05-28 16:26:22 +02:00
"runtime"
2020-12-11 12:46:28 +01:00
"strconv"
2020-01-06 02:14:17 +01:00
"strings"
2020-01-02 00:03:07 +01:00
"time"
2022-10-15 21:54:11 +02:00
"github.com/photoprism/photoprism/internal/migrate"
2020-01-02 00:03:07 +01:00
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
2022-04-08 14:22:07 +02:00
2020-01-02 00:03:07 +01:00
"github.com/photoprism/photoprism/internal/entity"
2020-01-08 19:51:21 +01:00
"github.com/photoprism/photoprism/internal/mutex"
2022-10-08 18:50:49 +02:00
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
2020-01-02 00:03:07 +01:00
)
2022-04-08 14:22:07 +02:00
// SQL Databases.
// TODO: PostgresSQL support requires upgrading GORM, so generic column data types can be used.
const (
MySQL = "mysql"
MariaDB = "mariadb"
Postgres = "postgres"
SQLite3 = "sqlite3"
)
// SQLite default DSNs.
const (
SQLiteTestDB = ".test.db"
SQLiteMemoryDSN = ":memory:"
)
2020-01-02 00:03:07 +01:00
// DatabaseDriver returns the database driver name.
func ( c * Config ) DatabaseDriver ( ) string {
2020-12-18 20:42:12 +01:00
switch strings . ToLower ( c . options . DatabaseDriver ) {
2020-12-15 20:14:06 +01:00
case MySQL , MariaDB :
2020-12-18 20:42:12 +01:00
c . options . DatabaseDriver = MySQL
2021-12-09 07:47:23 +01:00
case SQLite3 , "sqlite" , "sqllite" , "test" , "file" , "" :
c . options . DatabaseDriver = SQLite3
2020-05-30 14:52:47 +02:00
case "tidb" :
log . Warnf ( "config: database driver 'tidb' is deprecated, using sqlite" )
2021-12-09 07:47:23 +01:00
c . options . DatabaseDriver = SQLite3
2020-12-18 20:42:12 +01:00
c . options . DatabaseDsn = ""
2020-05-30 14:52:47 +02:00
default :
2020-12-18 20:42:12 +01:00
log . Warnf ( "config: unsupported database driver %s, using sqlite" , c . options . DatabaseDriver )
2021-12-09 07:47:23 +01:00
c . options . DatabaseDriver = SQLite3
2020-12-18 20:42:12 +01:00
c . options . DatabaseDsn = ""
2020-01-02 00:03:07 +01:00
}
2020-12-18 20:42:12 +01:00
return c . options . DatabaseDriver
2020-01-02 00:03:07 +01:00
}
// DatabaseDsn returns the database data source name (DSN).
func ( c * Config ) DatabaseDsn ( ) string {
2020-12-18 20:42:12 +01:00
if c . options . DatabaseDsn == "" {
2020-05-30 14:52:47 +02:00
switch c . DatabaseDriver ( ) {
2020-12-15 20:14:06 +01:00
case MySQL , MariaDB :
2022-02-23 15:08:28 +01:00
address := c . DatabaseServer ( )
// Connect via TCP or Unix Domain Socket?
if strings . HasPrefix ( address , "/" ) {
log . Debugf ( "mariadb: connecting via Unix domain socket" )
address = fmt . Sprintf ( "unix(%s)" , address )
} else {
address = fmt . Sprintf ( "tcp(%s)" , address )
}
2020-12-11 12:46:28 +01:00
return fmt . Sprintf (
2022-02-23 15:08:28 +01:00
"%s:%s@%s/%s?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true" ,
2020-12-11 12:46:28 +01:00
c . DatabaseUser ( ) ,
c . DatabasePassword ( ) ,
2022-02-23 15:08:28 +01:00
address ,
2020-12-11 12:46:28 +01:00
c . DatabaseName ( ) ,
)
2020-12-15 20:14:06 +01:00
case Postgres :
return fmt . Sprintf (
"user=%s password=%s dbname=%s host=%s port=%d sslmode=disable TimeZone=UTC" ,
c . DatabaseUser ( ) ,
c . DatabasePassword ( ) ,
c . DatabaseName ( ) ,
c . DatabaseHost ( ) ,
c . DatabasePort ( ) ,
)
2021-12-09 07:47:23 +01:00
case SQLite3 :
2022-09-15 07:29:09 +02:00
return filepath . Join ( c . StoragePath ( ) , "index.db?_busy_timeout=5000" )
2020-05-30 14:52:47 +02:00
default :
log . Errorf ( "config: empty database dsn" )
return ""
}
2020-01-02 00:03:07 +01:00
}
2020-12-18 20:42:12 +01:00
return c . options . DatabaseDsn
2020-01-02 00:03:07 +01:00
}
2020-12-11 12:46:28 +01:00
// ParseDatabaseDsn parses the database dsn and extracts user, password, database server, and name.
func ( c * Config ) ParseDatabaseDsn ( ) {
2020-12-18 20:42:12 +01:00
if c . options . DatabaseDsn == "" || c . options . DatabaseServer != "" {
2020-12-11 12:46:28 +01:00
return
}
2022-09-15 07:29:09 +02:00
d := NewDSN ( c . options . DatabaseDsn )
c . options . DatabaseName = d . Name
c . options . DatabaseServer = d . Server
c . options . DatabaseUser = d . User
c . options . DatabasePassword = d . Password
2020-12-11 12:46:28 +01:00
}
// DatabaseServer the database server.
func ( c * Config ) DatabaseServer ( ) string {
c . ParseDatabaseDsn ( )
2022-09-15 07:29:09 +02:00
if c . DatabaseDriver ( ) == SQLite3 {
return ""
} else if c . options . DatabaseServer == "" {
2020-12-11 12:46:28 +01:00
return "localhost"
}
2020-12-18 20:42:12 +01:00
return c . options . DatabaseServer
2020-12-11 12:46:28 +01:00
}
// DatabaseHost the database server host.
func ( c * Config ) DatabaseHost ( ) string {
2022-09-15 07:29:09 +02:00
if c . DatabaseDriver ( ) == SQLite3 {
return ""
}
2020-12-11 12:46:28 +01:00
if s := strings . Split ( c . DatabaseServer ( ) , ":" ) ; len ( s ) > 0 {
return s [ 0 ]
}
2020-12-18 20:42:12 +01:00
return c . options . DatabaseServer
2020-12-11 12:46:28 +01:00
}
// DatabasePort the database server port.
func ( c * Config ) DatabasePort ( ) int {
const defaultPort = 3306
2022-09-15 07:29:09 +02:00
if server := c . DatabaseServer ( ) ; server == "" {
return 0
} else if s := strings . Split ( server , ":" ) ; len ( s ) != 2 {
2020-12-11 12:46:28 +01:00
return defaultPort
} else if port , err := strconv . Atoi ( s [ 1 ] ) ; err != nil {
return defaultPort
} else if port < 1 || port > 65535 {
return defaultPort
} else {
return port
}
}
// DatabasePortString the database server port as string.
func ( c * Config ) DatabasePortString ( ) string {
2022-09-15 07:29:09 +02:00
if c . DatabaseDriver ( ) == SQLite3 {
return ""
}
2020-12-11 12:46:28 +01:00
return strconv . Itoa ( c . DatabasePort ( ) )
}
// DatabaseName the database schema name.
func ( c * Config ) DatabaseName ( ) string {
c . ParseDatabaseDsn ( )
2022-09-15 07:29:09 +02:00
if c . DatabaseDriver ( ) == SQLite3 {
return c . DatabaseDsn ( )
} else if c . options . DatabaseName == "" {
2020-12-11 12:46:28 +01:00
return "photoprism"
}
2020-12-18 20:42:12 +01:00
return c . options . DatabaseName
2020-12-11 12:46:28 +01:00
}
// DatabaseUser returns the database user name.
func ( c * Config ) DatabaseUser ( ) string {
2022-09-15 07:29:09 +02:00
if c . DatabaseDriver ( ) == SQLite3 {
return ""
}
2020-12-11 12:46:28 +01:00
c . ParseDatabaseDsn ( )
2020-12-18 20:42:12 +01:00
if c . options . DatabaseUser == "" {
2020-12-11 12:46:28 +01:00
return "photoprism"
}
2020-12-18 20:42:12 +01:00
return c . options . DatabaseUser
2020-12-11 12:46:28 +01:00
}
// DatabasePassword returns the database user password.
func ( c * Config ) DatabasePassword ( ) string {
2022-09-15 07:29:09 +02:00
if c . DatabaseDriver ( ) == SQLite3 {
return ""
}
2020-12-11 12:46:28 +01:00
c . ParseDatabaseDsn ( )
2020-12-18 20:42:12 +01:00
return c . options . DatabasePassword
2020-12-11 12:46:28 +01:00
}
2020-07-13 10:41:45 +02:00
// DatabaseConns returns the maximum number of open connections to the database.
2020-05-28 15:37:08 +02:00
func ( c * Config ) DatabaseConns ( ) int {
2020-12-18 20:42:12 +01:00
limit := c . options . DatabaseConns
2020-07-13 10:41:45 +02:00
if limit <= 0 {
limit = ( runtime . NumCPU ( ) * 2 ) + 16
}
if limit > 1024 {
limit = 1024
2020-05-28 15:37:08 +02:00
}
2020-07-13 10:41:45 +02:00
return limit
}
// DatabaseConnsIdle returns the maximum number of idle connections to the database (equal or less than open).
func ( c * Config ) DatabaseConnsIdle ( ) int {
2020-12-18 20:42:12 +01:00
limit := c . options . DatabaseConnsIdle
2020-07-13 10:41:45 +02:00
if limit <= 0 {
limit = runtime . NumCPU ( ) + 8
}
if limit > c . DatabaseConns ( ) {
limit = c . DatabaseConns ( )
}
return limit
2020-05-28 15:37:08 +02:00
}
2020-01-02 00:03:07 +01:00
// Db returns the db connection.
func ( c * Config ) Db ( ) * gorm . DB {
if c . db == nil {
2020-04-30 20:07:03 +02:00
log . Fatal ( "config: database not connected" )
2020-01-02 00:03:07 +01:00
}
return c . db
}
// CloseDb closes the db connection (if any).
func ( c * Config ) CloseDb ( ) error {
if c . db != nil {
if err := c . db . Close ( ) ; err == nil {
c . db = nil
} else {
return err
}
}
return nil
}
2021-07-16 18:02:51 +02:00
// SetDbOptions sets the database collation to unicode if supported.
func ( c * Config ) SetDbOptions ( ) {
switch c . DatabaseDriver ( ) {
case MySQL , MariaDB :
c . Db ( ) . Set ( "gorm:table_options" , "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" )
case Postgres :
// Ignore for now.
2021-12-09 07:47:23 +01:00
case SQLite3 :
2021-07-16 18:02:51 +02:00
// Not required as unicode is default.
}
}
2022-09-28 09:01:17 +02:00
// RegisterDb sets the database options and connection provider.
func ( c * Config ) RegisterDb ( ) {
c . SetDbOptions ( )
entity . SetDbProvider ( c )
}
2021-11-28 13:52:27 +01:00
// InitDb initializes the database without running previously failed migrations.
2020-04-30 20:07:03 +02:00
func ( c * Config ) InitDb ( ) {
2022-09-28 09:01:17 +02:00
c . RegisterDb ( )
2022-03-30 20:36:25 +02:00
c . MigrateDb ( false , nil )
2021-11-28 13:52:27 +01:00
}
// MigrateDb initializes the database and migrates the schema if needed.
2022-03-30 20:36:25 +02:00
func ( c * Config ) MigrateDb ( runFailed bool , ids [ ] string ) {
2022-10-13 22:11:02 +02:00
entity . Admin . UserName = c . AdminUser ( )
2022-10-15 21:54:11 +02:00
entity . InitDb ( migrate . Opt ( runFailed , ids ) )
2020-06-25 01:20:58 +02:00
2022-09-02 21:30:50 +02:00
// Init admin account?
if c . AdminPassword ( ) == "" {
log . Warnf ( "config: password required to initialize %s account" , clean . LogQuote ( c . AdminUser ( ) ) )
2022-09-02 22:55:57 +02:00
} else {
entity . Admin . InitAccount ( c . AdminUser ( ) , c . AdminPassword ( ) )
2022-09-02 21:30:50 +02:00
}
2020-06-25 01:20:58 +02:00
2022-09-28 09:01:17 +02:00
go entity . Error { } . LogEvents ( )
2020-01-02 00:03:07 +01:00
}
2020-05-08 19:34:29 +02:00
// InitTestDb drops all tables in the currently configured database and re-creates them.
func ( c * Config ) InitTestDb ( ) {
entity . ResetTestFixtures ( )
2020-06-25 01:20:58 +02:00
2022-09-02 21:30:50 +02:00
if c . AdminPassword ( ) == "" {
// Do nothing.
2022-09-02 22:55:57 +02:00
} else {
entity . Admin . InitAccount ( c . AdminUser ( ) , c . AdminPassword ( ) )
2022-09-02 21:30:50 +02:00
}
2020-06-25 01:20:58 +02:00
2022-09-28 09:01:17 +02:00
go entity . Error { } . LogEvents ( )
2020-04-08 13:24:06 +02:00
}
2020-07-13 10:41:45 +02:00
// connectDb establishes a database connection.
func ( c * Config ) connectDb ( ) error {
2022-10-08 18:50:49 +02:00
// Make sure this is not running twice.
2020-01-08 19:51:21 +01:00
mutex . Db . Lock ( )
defer mutex . Db . Unlock ( )
2022-10-08 18:50:49 +02:00
// Get database driver and data source name.
2020-01-02 00:03:07 +01:00
dbDriver := c . DatabaseDriver ( )
dbDsn := c . DatabaseDsn ( )
if dbDriver == "" {
2020-01-06 02:14:17 +01:00
return errors . New ( "config: database driver not specified" )
2020-01-02 00:03:07 +01:00
}
if dbDsn == "" {
2020-01-06 02:14:17 +01:00
return errors . New ( "config: database DSN not specified" )
2020-01-02 00:03:07 +01:00
}
2022-10-08 18:50:49 +02:00
// Open database connection.
2020-01-02 00:03:07 +01:00
db , err := gorm . Open ( dbDriver , dbDsn )
if err != nil || db == nil {
for i := 1 ; i <= 12 ; i ++ {
db , err = gorm . Open ( dbDriver , dbDsn )
if db != nil && err == nil {
break
}
2020-04-30 20:07:03 +02:00
time . Sleep ( 5 * time . Second )
2020-01-02 00:03:07 +01:00
}
if err != nil || db == nil {
log . Fatal ( err )
}
}
2022-10-08 18:50:49 +02:00
// Configure database logging.
2020-01-06 14:32:15 +01:00
db . LogMode ( false )
db . SetLogger ( log )
2020-05-28 16:26:22 +02:00
2022-10-08 18:50:49 +02:00
// Set database connection parameters.
2020-05-28 15:37:08 +02:00
db . DB ( ) . SetMaxOpenConns ( c . DatabaseConns ( ) )
2020-07-13 10:41:45 +02:00
db . DB ( ) . SetMaxIdleConns ( c . DatabaseConnsIdle ( ) )
2022-10-08 18:50:49 +02:00
db . DB ( ) . SetConnMaxLifetime ( time . Hour )
// Check database server version.
switch dbDriver {
case MySQL :
type Res struct {
Value string ` gorm:"column:Value;" `
}
var res Res
if err = db . Raw ( "SHOW VARIABLES LIKE 'innodb_version'" ) . Scan ( & res ) . Error ; err != nil {
return err
} else if v := strings . Split ( res . Value , "." ) ; len ( v ) < 3 {
log . Warnf ( "config: unknown database server version" )
} else if major := txt . UInt ( v [ 0 ] ) ; major < 10 {
err = fmt . Errorf ( "config: MySQL %s is not supported, please upgrade to MariaDB 10.5.12 or later (https://docs.photoprism.app/getting-started/#databases)" , res . Value )
return err
} else if txt . UInt ( v [ 1 ] ) <= 5 && txt . UInt ( v [ 2 ] ) <= 12 {
log . Errorf ( "config: MariaDB %s is not supported, please upgrade to MariaDB 10.5.12 or later (https://docs.photoprism.app/getting-started/#databases)" , res . Value )
}
}
2020-01-06 14:32:15 +01:00
2020-01-02 00:03:07 +01:00
c . db = db
2020-07-13 10:41:45 +02:00
2020-01-02 00:03:07 +01:00
return err
}
2020-01-06 02:14:17 +01:00
// ImportSQL imports a file to the currently configured database.
func ( c * Config ) ImportSQL ( filename string ) {
2021-10-06 07:10:50 +02:00
contents , err := os . ReadFile ( filename )
2020-01-06 02:14:17 +01:00
if err != nil {
log . Error ( err )
return
}
statements := strings . Split ( string ( contents ) , ";\n" )
2020-01-06 04:24:49 +01:00
q := c . Db ( ) . Unscoped ( )
2020-01-06 02:14:17 +01:00
for _ , stmt := range statements {
// Skip empty lines and comments
if len ( stmt ) < 3 || stmt [ 0 ] == '#' || stmt [ 0 ] == ';' {
continue
}
2020-01-06 14:32:15 +01:00
var result struct { }
2020-01-06 04:24:49 +01:00
2020-04-30 15:41:47 +02:00
q . Raw ( stmt ) . Scan ( & result )
2020-01-06 02:14:17 +01:00
}
}