324 lines
7 KiB
Go
324 lines
7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"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/mutex"
|
|
)
|
|
|
|
var dsnPattern = regexp.MustCompile(
|
|
`^(?:(?P<user>.*?)(?::(?P<password>.*))?@)?` +
|
|
`(?:(?P<net>[^\(]*)(?:\((?P<server>[^\)]*)\))?)?` +
|
|
`\/(?P<name>.*?)` +
|
|
`(?:\?(?P<params>[^\?]*))?$`)
|
|
|
|
// 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 SQLite, "sqlite", "sqllite", "test", "file", "":
|
|
c.options.DatabaseDriver = SQLite
|
|
case "tidb":
|
|
log.Warnf("config: database driver 'tidb' is deprecated, using sqlite")
|
|
c.options.DatabaseDriver = SQLite
|
|
c.options.DatabaseDsn = ""
|
|
default:
|
|
log.Warnf("config: unsupported database driver %s, using sqlite", c.options.DatabaseDriver)
|
|
c.options.DatabaseDriver = SQLite
|
|
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:
|
|
return fmt.Sprintf(
|
|
"%s:%s@tcp(%s)/%s?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
|
|
c.DatabaseUser(),
|
|
c.DatabasePassword(),
|
|
c.DatabaseServer(),
|
|
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 SQLite:
|
|
return filepath.Join(c.StoragePath(), "index.db")
|
|
default:
|
|
log.Errorf("config: empty database dsn")
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return c.options.DatabaseDsn
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
matches := dsnPattern.FindStringSubmatch(c.options.DatabaseDsn)
|
|
names := dsnPattern.SubexpNames()
|
|
|
|
for i, match := range matches {
|
|
switch names[i] {
|
|
case "user":
|
|
c.options.DatabaseUser = match
|
|
case "password":
|
|
c.options.DatabasePassword = match
|
|
case "server":
|
|
c.options.DatabaseServer = match
|
|
case "name":
|
|
c.options.DatabaseName = match
|
|
}
|
|
}
|
|
}
|
|
|
|
// DatabaseServer the database server.
|
|
func (c *Config) DatabaseServer() string {
|
|
c.ParseDatabaseDsn()
|
|
|
|
if c.options.DatabaseServer == "" {
|
|
return "localhost"
|
|
}
|
|
|
|
return c.options.DatabaseServer
|
|
}
|
|
|
|
// DatabaseHost the database server host.
|
|
func (c *Config) DatabaseHost() string {
|
|
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 s := strings.Split(c.DatabaseServer(), ":"); 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 {
|
|
return strconv.Itoa(c.DatabasePort())
|
|
}
|
|
|
|
// DatabaseName the database schema name.
|
|
func (c *Config) DatabaseName() string {
|
|
c.ParseDatabaseDsn()
|
|
|
|
if c.options.DatabaseName == "" {
|
|
return "photoprism"
|
|
}
|
|
|
|
return c.options.DatabaseName
|
|
}
|
|
|
|
// DatabaseUser returns the database user name.
|
|
func (c *Config) DatabaseUser() string {
|
|
c.ParseDatabaseDsn()
|
|
|
|
if c.options.DatabaseUser == "" {
|
|
return "photoprism"
|
|
}
|
|
|
|
return c.options.DatabaseUser
|
|
}
|
|
|
|
// DatabasePassword returns the database user password.
|
|
func (c *Config) DatabasePassword() string {
|
|
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 SQLite:
|
|
// Not required as unicode is default.
|
|
}
|
|
}
|
|
|
|
// InitDb will initialize the database connection and schema.
|
|
func (c *Config) InitDb() {
|
|
c.SetDbOptions()
|
|
entity.SetDbProvider(c)
|
|
entity.MigrateDb(false)
|
|
|
|
entity.Admin.InitPassword(c.AdminPassword())
|
|
|
|
go entity.SaveErrorMessages()
|
|
}
|
|
|
|
// InitTestDb drops all tables in the currently configured database and re-creates them.
|
|
func (c *Config) InitTestDb() {
|
|
c.SetDbOptions()
|
|
entity.SetDbProvider(c)
|
|
entity.ResetTestFixtures()
|
|
|
|
entity.Admin.InitPassword(c.AdminPassword())
|
|
|
|
go entity.SaveErrorMessages()
|
|
}
|
|
|
|
// connectDb establishes a database connection.
|
|
func (c *Config) connectDb() error {
|
|
mutex.Db.Lock()
|
|
defer mutex.Db.Unlock()
|
|
|
|
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")
|
|
}
|
|
|
|
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 {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
db.LogMode(false)
|
|
db.SetLogger(log)
|
|
|
|
db.DB().SetMaxOpenConns(c.DatabaseConns())
|
|
db.DB().SetMaxIdleConns(c.DatabaseConnsIdle())
|
|
db.DB().SetConnMaxLifetime(10 * time.Minute)
|
|
|
|
c.db = db
|
|
|
|
return err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|