c426a184c5
See also #3301, #3311, and #3298. Signed-off-by: Michael Mayer <michael@photoprism.app>
426 lines
9.9 KiB
Go
426 lines
9.9 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jinzhu/gorm"
|
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
"github.com/photoprism/photoprism/internal/migrate"
|
|
"github.com/photoprism/photoprism/internal/mutex"
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
)
|
|
|
|
// 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:"
|
|
)
|
|
|
|
// DatabaseDriver returns the database driver name.
|
|
func (c *Config) DatabaseDriver() string {
|
|
switch strings.ToLower(c.options.DatabaseDriver) {
|
|
case MySQL, MariaDB:
|
|
c.options.DatabaseDriver = MySQL
|
|
case SQLite3, "sqlite", "sqllite", "test", "file", "":
|
|
c.options.DatabaseDriver = SQLite3
|
|
case "tidb":
|
|
log.Warnf("config: database driver 'tidb' is deprecated, using sqlite")
|
|
c.options.DatabaseDriver = SQLite3
|
|
c.options.DatabaseDsn = ""
|
|
default:
|
|
log.Warnf("config: unsupported database driver %s, using sqlite", c.options.DatabaseDriver)
|
|
c.options.DatabaseDriver = SQLite3
|
|
c.options.DatabaseDsn = ""
|
|
}
|
|
|
|
return c.options.DatabaseDriver
|
|
}
|
|
|
|
// DatabaseDsn returns the database data source name (DSN).
|
|
func (c *Config) DatabaseDsn() string {
|
|
if c.options.DatabaseDsn == "" {
|
|
switch c.DatabaseDriver() {
|
|
case MySQL, MariaDB:
|
|
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)
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"%s:%s@%s/%s?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
|
|
c.DatabaseUser(),
|
|
c.DatabasePassword(),
|
|
address,
|
|
c.DatabaseName(),
|
|
)
|
|
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(),
|
|
)
|
|
case SQLite3:
|
|
return filepath.Join(c.StoragePath(), "index.db?_busy_timeout=5000")
|
|
default:
|
|
log.Errorf("config: empty database dsn")
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return c.options.DatabaseDsn
|
|
}
|
|
|
|
// DatabaseFile returns the filename part of a sqlite database DSN.
|
|
func (c *Config) DatabaseFile() string {
|
|
fileName, _, _ := strings.Cut(c.DatabaseDsn(), "?")
|
|
return fileName
|
|
}
|
|
|
|
// ParseDatabaseDsn parses the database dsn and extracts user, password, database server, and name.
|
|
func (c *Config) ParseDatabaseDsn() {
|
|
if c.options.DatabaseDsn == "" || c.options.DatabaseServer != "" {
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// DatabaseServer the database server.
|
|
func (c *Config) DatabaseServer() string {
|
|
c.ParseDatabaseDsn()
|
|
|
|
if c.DatabaseDriver() == SQLite3 {
|
|
return ""
|
|
} else if c.options.DatabaseServer == "" {
|
|
return "localhost"
|
|
}
|
|
|
|
return c.options.DatabaseServer
|
|
}
|
|
|
|
// DatabaseHost the database server host.
|
|
func (c *Config) DatabaseHost() string {
|
|
if c.DatabaseDriver() == SQLite3 {
|
|
return ""
|
|
}
|
|
|
|
if s := strings.Split(c.DatabaseServer(), ":"); len(s) > 0 {
|
|
return s[0]
|
|
}
|
|
|
|
return c.options.DatabaseServer
|
|
}
|
|
|
|
// DatabasePort the database server port.
|
|
func (c *Config) DatabasePort() int {
|
|
const defaultPort = 3306
|
|
|
|
if server := c.DatabaseServer(); server == "" {
|
|
return 0
|
|
} else if s := strings.Split(server, ":"); len(s) != 2 {
|
|
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 {
|
|
if c.DatabaseDriver() == SQLite3 {
|
|
return ""
|
|
}
|
|
|
|
return strconv.Itoa(c.DatabasePort())
|
|
}
|
|
|
|
// DatabaseName the database schema name.
|
|
func (c *Config) DatabaseName() string {
|
|
c.ParseDatabaseDsn()
|
|
|
|
if c.DatabaseDriver() == SQLite3 {
|
|
return c.DatabaseDsn()
|
|
} else if c.options.DatabaseName == "" {
|
|
return "photoprism"
|
|
}
|
|
|
|
return c.options.DatabaseName
|
|
}
|
|
|
|
// DatabaseUser returns the database user name.
|
|
func (c *Config) DatabaseUser() string {
|
|
if c.DatabaseDriver() == SQLite3 {
|
|
return ""
|
|
}
|
|
|
|
c.ParseDatabaseDsn()
|
|
|
|
if c.options.DatabaseUser == "" {
|
|
return "photoprism"
|
|
}
|
|
|
|
return c.options.DatabaseUser
|
|
}
|
|
|
|
// DatabasePassword returns the database user password.
|
|
func (c *Config) DatabasePassword() string {
|
|
if c.DatabaseDriver() == SQLite3 {
|
|
return ""
|
|
}
|
|
|
|
c.ParseDatabaseDsn()
|
|
|
|
return c.options.DatabasePassword
|
|
}
|
|
|
|
// DatabaseConns returns the maximum number of open connections to the database.
|
|
func (c *Config) DatabaseConns() int {
|
|
limit := c.options.DatabaseConns
|
|
|
|
if limit <= 0 {
|
|
limit = (runtime.NumCPU() * 2) + 16
|
|
}
|
|
|
|
if limit > 1024 {
|
|
limit = 1024
|
|
}
|
|
|
|
return limit
|
|
}
|
|
|
|
// DatabaseConnsIdle returns the maximum number of idle connections to the database (equal or less than open).
|
|
func (c *Config) DatabaseConnsIdle() int {
|
|
limit := c.options.DatabaseConnsIdle
|
|
|
|
if limit <= 0 {
|
|
limit = runtime.NumCPU() + 8
|
|
}
|
|
|
|
if limit > c.DatabaseConns() {
|
|
limit = c.DatabaseConns()
|
|
}
|
|
|
|
return limit
|
|
}
|
|
|
|
// Db returns the db connection.
|
|
func (c *Config) Db() *gorm.DB {
|
|
if c.db == nil {
|
|
log.Fatal("config: database not connected")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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.
|
|
case SQLite3:
|
|
// Not required as unicode is default.
|
|
}
|
|
}
|
|
|
|
// RegisterDb sets the database options and connection provider.
|
|
func (c *Config) RegisterDb() {
|
|
c.SetDbOptions()
|
|
entity.SetDbProvider(c)
|
|
}
|
|
|
|
// InitDb initializes the database without running previously failed migrations.
|
|
func (c *Config) InitDb() {
|
|
c.RegisterDb()
|
|
c.MigrateDb(false, nil)
|
|
}
|
|
|
|
// MigrateDb initializes the database and migrates the schema if needed.
|
|
func (c *Config) MigrateDb(runFailed bool, ids []string) {
|
|
entity.Admin.UserName = c.AdminUser()
|
|
|
|
// Only migrate once automatically per version.
|
|
version := migrate.FirstOrCreateVersion(c.Db(), migrate.NewVersion(c.Version(), c.Edition()))
|
|
entity.InitDb(migrate.Opt(version.NeedsMigration(), runFailed, ids))
|
|
if err := version.Migrated(c.Db()); err != nil {
|
|
log.Warnf("config: %s (migrate)", err)
|
|
}
|
|
|
|
// Init admin account?
|
|
if c.AdminPassword() == "" {
|
|
log.Warnf("config: password required to initialize %s account", clean.LogQuote(c.AdminUser()))
|
|
} else {
|
|
entity.Admin.InitAccount(c.AdminUser(), c.AdminPassword())
|
|
}
|
|
|
|
go entity.Error{}.LogEvents()
|
|
}
|
|
|
|
// InitTestDb drops all tables in the currently configured database and re-creates them.
|
|
func (c *Config) InitTestDb() {
|
|
entity.ResetTestFixtures()
|
|
|
|
if c.AdminPassword() == "" {
|
|
// Do nothing.
|
|
} else {
|
|
entity.Admin.InitAccount(c.AdminUser(), c.AdminPassword())
|
|
}
|
|
|
|
go entity.Error{}.LogEvents()
|
|
}
|
|
|
|
// connectDb checks the database server version.
|
|
func (c *Config) checkDb(db *gorm.DB) error {
|
|
switch c.DatabaseDriver() {
|
|
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 nil
|
|
} 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 {
|
|
return fmt.Errorf("config: MySQL %s is not supported, see https://docs.photoprism.app/getting-started/#databases", res.Value)
|
|
} else if sub := txt.UInt(v[1]); sub < 5 || sub == 5 && txt.UInt(v[2]) < 12 {
|
|
return fmt.Errorf("config: MariaDB %s is not supported, see https://docs.photoprism.app/getting-started/#databases", res.Value)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// connectDb establishes a database connection.
|
|
func (c *Config) connectDb() error {
|
|
// Make sure this is not running twice.
|
|
mutex.Db.Lock()
|
|
defer mutex.Db.Unlock()
|
|
|
|
// Get database driver and data source name.
|
|
dbDriver := c.DatabaseDriver()
|
|
dbDsn := c.DatabaseDsn()
|
|
|
|
if dbDriver == "" {
|
|
return errors.New("config: database driver not specified")
|
|
}
|
|
|
|
if dbDsn == "" {
|
|
return errors.New("config: database DSN not specified")
|
|
}
|
|
|
|
// Open database connection.
|
|
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
|
|
}
|
|
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
|
|
if err != nil || db == nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Configure database logging.
|
|
db.LogMode(false)
|
|
db.SetLogger(log)
|
|
|
|
// Set database connection parameters.
|
|
db.DB().SetMaxOpenConns(c.DatabaseConns())
|
|
db.DB().SetMaxIdleConns(c.DatabaseConnsIdle())
|
|
db.DB().SetConnMaxLifetime(time.Hour)
|
|
|
|
// Check database server version.
|
|
if err = c.checkDb(db); err != nil {
|
|
if c.Unsafe() {
|
|
log.Error(err)
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Ok.
|
|
c.db = db
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImportSQL imports a file to the currently configured database.
|
|
func (c *Config) ImportSQL(filename string) {
|
|
contents, err := os.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
|
|
statements := strings.Split(string(contents), ";\n")
|
|
q := c.Db().Unscoped()
|
|
|
|
for _, stmt := range statements {
|
|
// Skip empty lines and comments
|
|
if len(stmt) < 3 || stmt[0] == '#' || stmt[0] == ';' {
|
|
continue
|
|
}
|
|
|
|
var result struct{}
|
|
|
|
q.Raw(stmt).Scan(&result)
|
|
}
|
|
}
|