Backups: Detect server version to determine SSL support #4837

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-03-17 13:23:43 +01:00
parent 9af3478f71
commit 940194ab1c
7 changed files with 158 additions and 16 deletions

View File

@@ -3,3 +3,4 @@ user=root
password=photoprism password=photoprism
host=mariadb host=mariadb
port=4001 port=4001
skip-ssl=true

1
go.mod
View File

@@ -87,6 +87,7 @@ require (
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/urfave/cli/v2 v2.27.6 github.com/urfave/cli/v2 v2.27.6
github.com/zitadel/oidc/v3 v3.36.1 github.com/zitadel/oidc/v3 v3.36.1
golang.org/x/mod v0.24.0
golang.org/x/sys v0.31.0 golang.org/x/sys v0.31.0
) )

4
go.sum
View File

@@ -489,8 +489,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@@ -71,6 +71,7 @@ type Config struct {
options *Options options *Options
settings *customize.Settings settings *customize.Settings
db *gorm.DB db *gorm.DB
dbVersion string
hub *hub.Config hub *hub.Config
token string token string
serial string serial string

View File

@@ -13,6 +13,7 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/jinzhu/gorm/dialects/sqlite"
"golang.org/x/mod/semver"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/entity/migrate" "github.com/photoprism/photoprism/internal/entity/migrate"
@@ -56,6 +57,49 @@ func (c *Config) DatabaseDriver() string {
return c.options.DatabaseDriver return c.options.DatabaseDriver
} }
// DatabaseDriverName returns the formatted database driver name.
func (c *Config) DatabaseDriverName() string {
switch c.DatabaseDriver() {
case MySQL, MariaDB:
return "MariaDB"
case SQLite3, "sqlite", "sqllite", "test", "file", "":
return "SQLite"
case "tidb":
return "TiDB"
default:
return "unsupported database"
}
}
// DatabaseVersion returns the database version string, if known.
func (c *Config) DatabaseVersion() string {
return c.dbVersion
}
// IsDatabaseVersion checks if the database version is at least the specified version in semver format.
func (c *Config) IsDatabaseVersion(semverVersion string) bool {
if semverVersion == "" {
return true
}
return semver.Compare(c.DatabaseVersion(), semverVersion) >= 0
}
// DatabaseSsl checks if the database supports SSL connections for backup and restore.
func (c *Config) DatabaseSsl() bool {
if c.dbVersion == "" {
return false
}
switch c.DatabaseDriver() {
case MySQL:
// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
return c.IsDatabaseVersion("v11.3")
default:
return false
}
}
// DatabaseDsn returns the database data source name (DSN). // DatabaseDsn returns the database data source name (DSN).
func (c *Config) DatabaseDsn() string { func (c *Config) DatabaseDsn() string {
if c.options.DatabaseDsn == "" { if c.options.DatabaseDsn == "" {
@@ -343,15 +387,49 @@ func (c *Config) checkDb(db *gorm.DB) error {
type Res struct { type Res struct {
Value string `gorm:"column:Value;"` Value string `gorm:"column:Value;"`
} }
var res Res var res Res
if err := db.Raw("SHOW VARIABLES LIKE 'innodb_version'").Scan(&res).Error; err != nil {
err := db.Raw("SHOW VARIABLES LIKE 'innodb_version'").Scan(&res).Error
if err != nil {
err = db.Raw("SELECT VERSION() AS Value").Scan(&res).Error
}
// Version query not supported.
if err != nil {
log.Tracef("config: failed to detect database version (%s)", err)
return nil return nil
} else if v := strings.Split(res.Value, "."); len(v) < 3 { }
if v := strings.Split(res.Value, "."); len(v) < 3 {
log.Warnf("config: unknown database server version") log.Warnf("config: unknown database server version")
} else if major := txt.UInt(v[0]); major < 10 { } 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) 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 { } 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 fmt.Errorf("config: MariaDB %s is not supported, see https://docs.photoprism.app/getting-started/#databases", res.Value)
} else {
c.dbVersion = fmt.Sprintf("v%d.%d", major, sub)
}
case SQLite3:
type Res struct {
Value string `gorm:"column:Value;"`
}
var res Res
err := db.Raw("SELECT sqlite_version() AS Value").Scan(&res).Error
// Version query not supported.
if err != nil {
log.Warnf("config: failed to detect database version (%s)", err)
return nil
}
if v := strings.Split(res.Value, "."); len(v) < 3 {
log.Warnf("config: unknown database server version")
} else {
c.dbVersion = fmt.Sprintf("v%d.%d", txt.UInt(v[0]), txt.UInt(v[1]))
} }
} }
@@ -414,6 +492,10 @@ func (c *Config) connectDb() error {
} }
} }
if dbVersion := c.DatabaseVersion(); dbVersion != "" {
log.Infof("database: opened connection to %s %s", c.DatabaseDriverName(), dbVersion)
}
// Ok. // Ok.
c.db = db c.db = db

View File

@@ -13,6 +13,26 @@ func TestConfig_DatabaseDriver(t *testing.T) {
assert.Equal(t, SQLite3, driver) assert.Equal(t, SQLite3, driver)
} }
func TestConfig_DatabaseDriverName(t *testing.T) {
c := NewConfig(CliTestContext())
driver := c.DatabaseDriverName()
assert.Equal(t, "SQLite", driver)
}
func TestConfig_DatabaseVersion(t *testing.T) {
c := TestConfig()
assert.NotEmpty(t, c.DatabaseVersion())
assert.True(t, c.IsDatabaseVersion("v3.45"))
}
func TestConfig_DatabaseSsl(t *testing.T) {
c := TestConfig()
assert.False(t, c.DatabaseSsl())
}
func TestConfig_ParseDatabaseDsn(t *testing.T) { func TestConfig_ParseDatabaseDsn(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())

View File

@@ -84,7 +84,10 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er
"-p"+c.DatabasePassword(), "-p"+c.DatabasePassword(),
c.DatabaseName(), c.DatabaseName(),
) )
} else { } else if c.DatabaseSsl() {
// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
log.Infof("backup: server supports zero-configuration ssl")
cmd = exec.Command( cmd = exec.Command(
c.MariadbDumpBin(), c.MariadbDumpBin(),
"--protocol", "tcp", "--protocol", "tcp",
@@ -94,7 +97,22 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er
"-p"+c.DatabasePassword(), "-p"+c.DatabasePassword(),
c.DatabaseName(), c.DatabaseName(),
) )
} else {
// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
log.Infof("backup: zero-configuration ssl not supported by the server")
cmd = exec.Command(
c.MariadbDumpBin(),
"--protocol", "tcp",
"--skip-ssl",
"-h", c.DatabaseHost(),
"-P", c.DatabasePortString(),
"-u", c.DatabaseUser(),
"-p"+c.DatabasePassword(),
c.DatabaseName(),
)
} }
case config.SQLite3: case config.SQLite3:
if !fs.FileExistsNotEmpty(c.DatabaseFile()) { if !fs.FileExistsNotEmpty(c.DatabaseFile()) {
return fmt.Errorf("sqlite database file %s not found", clean.LogQuote(c.DatabaseFile())) return fmt.Errorf("sqlite database file %s not found", clean.LogQuote(c.DatabaseFile()))
@@ -247,7 +265,10 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er
"-f", "-f",
c.DatabaseName(), c.DatabaseName(),
) )
} else { } else if c.DatabaseSsl() {
// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
log.Infof("restore: server supports zero-configuration ssl")
cmd = exec.Command( cmd = exec.Command(
c.MariadbBin(), c.MariadbBin(),
"--protocol", "tcp", "--protocol", "tcp",
@@ -258,7 +279,23 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er
"-f", "-f",
c.DatabaseName(), c.DatabaseName(),
) )
} else {
// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
log.Infof("restore: zero-configuration ssl not supported by the server")
cmd = exec.Command(
c.MariadbBin(),
"--protocol", "tcp",
"--skip-ssl",
"-h", c.DatabaseHost(),
"-P", c.DatabasePortString(),
"-u", c.DatabaseUser(),
"-p"+c.DatabasePassword(),
"-f",
c.DatabaseName(),
)
} }
case config.SQLite3: case config.SQLite3:
log.Infoln("restore: dropping existing sqlite database tables") log.Infoln("restore: dropping existing sqlite database tables")
tables.Drop(c.Db()) tables.Drop(c.Db())