From 940194ab1c8b0c120b2330519aaf386b200f3243 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Mon, 17 Mar 2025 13:23:43 +0100 Subject: [PATCH] Backups: Detect server version to determine SSL support #4837 Signed-off-by: Michael Mayer --- .my.cnf | 1 + go.mod | 1 + go.sum | 4 +- internal/config/config.go | 21 ++++--- internal/config/config_db.go | 86 +++++++++++++++++++++++++- internal/config/config_db_test.go | 20 ++++++ internal/photoprism/backup/database.go | 41 +++++++++++- 7 files changed, 158 insertions(+), 16 deletions(-) diff --git a/.my.cnf b/.my.cnf index 3ce8bac69..fc2637052 100644 --- a/.my.cnf +++ b/.my.cnf @@ -3,3 +3,4 @@ user=root password=photoprism host=mariadb port=4001 +skip-ssl=true \ No newline at end of file diff --git a/go.mod b/go.mod index bc521f49f..e37be6d4c 100644 --- a/go.mod +++ b/go.mod @@ -87,6 +87,7 @@ require ( github.com/swaggo/gin-swagger v1.6.0 github.com/urfave/cli/v2 v2.27.6 github.com/zitadel/oidc/v3 v3.36.1 + golang.org/x/mod v0.24.0 golang.org/x/sys v0.31.0 ) diff --git a/go.sum b/go.sum index a9851fa35..ed19416ae 100644 --- a/go.sum +++ b/go.sum @@ -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.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.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/internal/config/config.go b/internal/config/config.go index 599fad6c7..ea2bbbe76 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -66,16 +66,17 @@ var initThumbsMutex sync.Mutex // Config holds database, cache and all parameters of photoprism type Config struct { - once sync.Once - cliCtx *cli.Context - options *Options - settings *customize.Settings - db *gorm.DB - hub *hub.Config - token string - serial string - env string - start bool + once sync.Once + cliCtx *cli.Context + options *Options + settings *customize.Settings + db *gorm.DB + dbVersion string + hub *hub.Config + token string + serial string + env string + start bool } func init() { diff --git a/internal/config/config_db.go b/internal/config/config_db.go index 2eb6e1f28..d7cec1f1a 100644 --- a/internal/config/config_db.go +++ b/internal/config/config_db.go @@ -13,6 +13,7 @@ import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" + "golang.org/x/mod/semver" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity/migrate" @@ -56,6 +57,49 @@ func (c *Config) DatabaseDriver() string { 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). func (c *Config) DatabaseDsn() string { if c.options.DatabaseDsn == "" { @@ -343,15 +387,49 @@ func (c *Config) checkDb(db *gorm.DB) error { 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 { + + 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 - } 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") } 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) + } 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. c.db = db diff --git a/internal/config/config_db_test.go b/internal/config/config_db_test.go index bfd1cb928..20e24236b 100644 --- a/internal/config/config_db_test.go +++ b/internal/config/config_db_test.go @@ -13,6 +13,26 @@ func TestConfig_DatabaseDriver(t *testing.T) { 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) { c := NewConfig(CliTestContext()) diff --git a/internal/photoprism/backup/database.go b/internal/photoprism/backup/database.go index f99f55d1c..dc5c2d9e2 100644 --- a/internal/photoprism/backup/database.go +++ b/internal/photoprism/backup/database.go @@ -84,7 +84,10 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er "-p"+c.DatabasePassword(), 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( c.MariadbDumpBin(), "--protocol", "tcp", @@ -94,7 +97,22 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er "-p"+c.DatabasePassword(), 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: if !fs.FileExistsNotEmpty(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", 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( c.MariadbBin(), "--protocol", "tcp", @@ -258,7 +279,23 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er "-f", 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: log.Infoln("restore: dropping existing sqlite database tables") tables.Drop(c.Db())