diff --git a/internal/commands/cluster_register.go b/internal/commands/cluster_register.go index 4ff345036..a001aae3b 100644 --- a/internal/commands/cluster_register.go +++ b/internal/commands/cluster_register.go @@ -23,6 +23,7 @@ import ( clusternode "github.com/photoprism/photoprism/internal/service/cluster/node" "github.com/photoprism/photoprism/internal/service/cluster/theme" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/http/header" "github.com/photoprism/photoprism/pkg/log/status" @@ -449,7 +450,7 @@ func persistRegisterResponse(conf *config.Config, resp *cluster.RegisterResponse // DB settings (MySQL/MariaDB only) if resp.Database.Name != "" && resp.Database.User != "" { - updates.SetDatabaseDriver(config.MySQL) + updates.SetDatabaseDriver(enum.MySQL) updates.SetDatabaseName(resp.Database.Name) updates.SetDatabaseServer(fmt.Sprintf("%s:%d", resp.Database.Host, resp.Database.Port)) updates.SetDatabaseUser(resp.Database.User) diff --git a/internal/config/config.go b/internal/config/config.go index 7dfdf6a4f..d45275505 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,6 +61,7 @@ import ( "github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/pkg/checksum" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/rnd" @@ -688,7 +689,7 @@ func (c *Config) IndexWorkers() int { } // Limit number of workers when using SQLite3 to avoid database locking issues. - if c.DatabaseDriver() == SQLite3 && (cores >= 8 && c.options.IndexWorkers <= 0 || c.options.IndexWorkers > 4) { + if c.DatabaseDriver() == enum.SQLite3 && (cores >= 8 && c.options.IndexWorkers <= 0 || c.options.IndexWorkers > 4) { return 4 } diff --git a/internal/config/config_db.go b/internal/config/config_db.go index 3f8698d15..e90255cd4 100644 --- a/internal/config/config_db.go +++ b/internal/config/config_db.go @@ -22,35 +22,26 @@ import ( "github.com/photoprism/photoprism/internal/service/cluster" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/dsn" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/txt" ) -// SQL Databases. -// TODO: PostgreSQL support requires upgrading GORM, so generic column data types can be used. -const ( - Auto = "auto" - MySQL = dsn.DriverMySQL - MariaDB = dsn.DriverMariaDB - Postgres = dsn.DriverPostgres - SQLite3 = dsn.DriverSQLite3 -) - // DatabaseDriver returns the database driver name. func (c *Config) DatabaseDriver() string { c.normalizeDatabaseDSN() switch strings.ToLower(c.options.DatabaseDriver) { - case MySQL, MariaDB: - c.options.DatabaseDriver = MySQL - case SQLite3, "sqlite", "test", "file", "": - c.options.DatabaseDriver = SQLite3 + case enum.MySQL, enum.MariaDB: + c.options.DatabaseDriver = enum.MySQL + case enum.SQLite3, "sqlite", "test", "file", "": + c.options.DatabaseDriver = enum.SQLite3 case "tidb": log.Warnf("config: database driver 'tidb' is deprecated, using sqlite") - c.options.DatabaseDriver = SQLite3 + c.options.DatabaseDriver = enum.SQLite3 c.options.DatabaseDSN = "" default: log.Warnf("config: unsupported database driver %s, using sqlite", c.options.DatabaseDriver) - c.options.DatabaseDriver = SQLite3 + c.options.DatabaseDriver = enum.SQLite3 c.options.DatabaseDSN = "" } @@ -60,9 +51,9 @@ func (c *Config) DatabaseDriver() string { // DatabaseDriverName returns the formatted database driver name. func (c *Config) DatabaseDriverName() string { switch c.DatabaseDriver() { - case MySQL, MariaDB: + case enum.MySQL, enum.MariaDB: return "MariaDB" - case SQLite3, "sqlite", "test", "file", "": + case enum.SQLite3, "sqlite", "test", "file", "": return "SQLite" case "tidb": return "TiDB" @@ -92,7 +83,7 @@ func (c *Config) DatabaseSsl() bool { } switch c.DatabaseDriver() { - case MySQL: + case enum.MySQL: // see https://mariadb.org/mission-impossible-zero-configuration-ssl/ return c.IsDatabaseVersion("v11.4") default: @@ -115,7 +106,7 @@ func (c *Config) DatabaseDSN() string { // Generate matching database DSN based on the configured database driver. if c.NoDatabaseDSN() { switch c.DatabaseDriver() { - case MySQL: + case enum.MySQL: databaseServer := c.DatabaseServer() // Connect via Unix Domain Socket? @@ -132,10 +123,10 @@ func (c *Config) DatabaseDSN() string { c.DatabasePassword(), databaseServer, c.DatabaseName(), - dsn.Params[dsn.DriverMySQL], + dsn.Params[enum.MySQL], c.DatabaseTimeout(), ) - case Postgres: + case enum.Postgres: return fmt.Sprintf( "user=%s password=%s dbname=%s host=%s port=%d connect_timeout=%d %s", c.DatabaseUser(), @@ -144,10 +135,10 @@ func (c *Config) DatabaseDSN() string { c.DatabaseHost(), c.DatabasePort(), c.DatabaseTimeout(), - dsn.Params[dsn.DriverPostgres], + dsn.Params[enum.Postgres], ) - case SQLite3: - return filepath.Join(c.StoragePath(), fmt.Sprintf("index.db?%s", dsn.Params[dsn.DriverSQLite3])) + case enum.SQLite3: + return filepath.Join(c.StoragePath(), fmt.Sprintf("index.db?%s", dsn.Params[enum.SQLite3])) default: log.Errorf("config: empty database dsn") return "" @@ -155,11 +146,11 @@ func (c *Config) DatabaseDSN() string { } // If missing, add the required parameters to the configured MySQL/MariaDB DSN. - if c.DatabaseDriver() == MySQL && !strings.Contains(c.options.DatabaseDSN, "?") { + if c.DatabaseDriver() == enum.MySQL && !strings.Contains(c.options.DatabaseDSN, "?") { c.options.DatabaseDSN = fmt.Sprintf( "%s?%s&timeout=%ds", c.options.DatabaseDSN, - dsn.Params[dsn.DriverMySQL], + dsn.Params[enum.MySQL], c.DatabaseTimeout()) } @@ -181,7 +172,7 @@ func (c *Config) HasDatabaseDSN() bool { // ReportDatabaseDSN checks if the database data source name (DSN) should be reported // instead of database name, server, user, and password. func (c *Config) ReportDatabaseDSN() bool { - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return true } @@ -192,7 +183,7 @@ func (c *Config) ReportDatabaseDSN() bool { func (c *Config) ParseDatabaseDSN() { if c.NoDatabaseDSN() { return - } else if c.options.DatabaseServer != "" && c.DatabaseDriver() == SQLite3 { + } else if c.options.DatabaseServer != "" && c.DatabaseDriver() == enum.SQLite3 { return } @@ -214,7 +205,7 @@ func (c *Config) DatabaseFile() string { func (c *Config) DatabaseServer() string { c.ParseDatabaseDSN() - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return "" } else if c.options.DatabaseServer == "" { return localhost @@ -227,7 +218,7 @@ func (c *Config) DatabaseServer() string { func (c *Config) DatabaseHost() string { c.ParseDatabaseDSN() - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return "" } @@ -239,7 +230,7 @@ func (c *Config) DatabaseHost() string { func (c *Config) DatabasePort() int { c.ParseDatabaseDSN() - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return 0 } @@ -249,7 +240,7 @@ func (c *Config) DatabasePort() int { // DatabasePortString the database server port as string. func (c *Config) DatabasePortString() string { - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return "" } @@ -260,7 +251,7 @@ func (c *Config) DatabasePortString() string { func (c *Config) DatabaseName() string { c.ParseDatabaseDSN() - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return c.DatabaseDSN() } else if c.options.DatabaseName == "" { return "photoprism" @@ -271,7 +262,7 @@ func (c *Config) DatabaseName() string { // DatabaseUser returns the database user name. func (c *Config) DatabaseUser() string { - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return "" } @@ -286,7 +277,7 @@ func (c *Config) DatabaseUser() string { // DatabasePassword returns the database user password. func (c *Config) DatabasePassword() string { - if c.DatabaseDriver() == SQLite3 { + if c.DatabaseDriver() == enum.SQLite3 { return "" } @@ -358,7 +349,7 @@ func (c *Config) DatabaseProvisionPrefix() string { // ShouldAutoRotateDatabase decides whether callers should request DB rotation automatically. // It is used by both the CLI and node bootstrap to avoid unnecessary provisioning calls. func (c *Config) ShouldAutoRotateDatabase() bool { - if c.Portal() || c.DatabaseDriver() != MySQL { + if c.Portal() || c.DatabaseDriver() != enum.MySQL { return false } @@ -440,11 +431,11 @@ func (c *Config) CloseDb() error { // SetDbOptions sets the database collation to unicode if supported. func (c *Config) SetDbOptions() { switch c.DatabaseDriver() { - case MySQL, MariaDB: + case enum.MySQL, enum.MariaDB: c.Db().Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci") - case Postgres: + case enum.Postgres: // Ignore for now. - case SQLite3: + case enum.SQLite3: // Not required as Unicode is default. } } @@ -515,7 +506,7 @@ func (c *Config) checkDb(db *gorm.DB) error { } switch c.DatabaseDriver() { - case MySQL: + case enum.MySQL: type Res struct { Value string `gorm:"column:Value;"` } @@ -544,7 +535,7 @@ func (c *Config) checkDb(db *gorm.DB) error { case !c.IsDatabaseVersion("v10.5.12"): return fmt.Errorf("config: MariaDB %s is not supported, see https://docs.photoprism.app/getting-started/#databases", c.dbVersion) } - case SQLite3: + case enum.SQLite3: type Res struct { Value string `gorm:"column:Value;"` } diff --git a/internal/config/config_db_test.go b/internal/config/config_db_test.go index 6b2013544..284b6163f 100644 --- a/internal/config/config_db_test.go +++ b/internal/config/config_db_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" + "github.com/photoprism/photoprism/pkg/enum" + "github.com/photoprism/photoprism/internal/service/cluster" ) @@ -26,16 +28,16 @@ func TestConfig_DatabaseDriver(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) - assert.Equal(t, SQLite3, c.DatabaseDriver()) + assert.Equal(t, enum.SQLite3, c.DatabaseDriver()) }) t.Run("NormalizesDeprecatedDSN", func(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) - c.options.DatabaseDriver = MySQL + c.options.DatabaseDriver = enum.MySQL c.options.Deprecated.DatabaseDsn = "user:pass@tcp(localhost:3306)/photoprism" - assert.Equal(t, MySQL, c.DatabaseDriver()) + assert.Equal(t, enum.MySQL, c.DatabaseDriver()) assert.Equal(t, "user:pass@tcp(localhost:3306)/photoprism", c.options.DatabaseDSN) assert.Empty(t, c.options.Deprecated.DatabaseDsn) }) @@ -65,7 +67,7 @@ func TestConfig_normalizeDatabaseDSN(t *testing.T) { c := NewConfig(CliTestContext()) c.options.Deprecated.DatabaseDsn = "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true" - c.options.DatabaseDriver = MySQL + c.options.DatabaseDriver = enum.MySQL assert.Equal(t, "honeypot:1234", c.DatabaseServer()) assert.Equal(t, "honeypot", c.DatabaseHost()) @@ -79,7 +81,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) { c := NewConfig(CliTestContext()) c.options.DatabaseDSN = "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true" - c.options.DatabaseDriver = SQLite3 + c.options.DatabaseDriver = enum.SQLite3 assert.Equal(t, "", c.DatabaseServer()) assert.Equal(t, "", c.DatabaseHost()) @@ -88,7 +90,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) { assert.Equal(t, "", c.DatabaseUser()) assert.Equal(t, "", c.DatabasePassword()) - c.options.DatabaseDriver = MySQL + c.options.DatabaseDriver = enum.MySQL assert.Equal(t, "honeypot:1234", c.DatabaseServer()) assert.Equal(t, "honeypot", c.DatabaseHost()) @@ -97,7 +99,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) { assert.Equal(t, "foo", c.DatabaseUser()) assert.Equal(t, "b@r", c.DatabasePassword()) - c.options.DatabaseDriver = SQLite3 + c.options.DatabaseDriver = enum.SQLite3 assert.Equal(t, "", c.DatabaseServer()) assert.Equal(t, "", c.DatabaseHost()) @@ -110,7 +112,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) { target := NewConfig(CliTestContext()) resetDatabaseOptions(target) - target.options.DatabaseDriver = MySQL + target.options.DatabaseDriver = enum.MySQL target.options.DatabaseServer = "db.internal:3306" target.options.DatabaseName = "photoprism" target.options.DatabaseUser = "app" @@ -129,7 +131,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) { cfg := NewConfig(CliTestContext()) resetDatabaseOptions(cfg) - cfg.options.DatabaseDriver = SQLite3 + cfg.options.DatabaseDriver = enum.SQLite3 cfg.options.DatabaseDSN = "file:/data/app.db?_busy_timeout=5000" cfg.options.DatabaseServer = "/tmp/mysql.sock" cfg.options.DatabaseName = "existing-name" @@ -191,9 +193,9 @@ func TestConfig_DatabasePassword(t *testing.T) { // Test setting the password via secret file. _ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "testdata/secret_database") assert.Equal(t, "", c.DatabasePassword()) - c.Options().DatabaseDriver = MySQL + c.Options().DatabaseDriver = enum.MySQL assert.Equal(t, "StoryOfAmélie", c.DatabasePassword()) - c.Options().DatabaseDriver = SQLite3 + c.Options().DatabaseDriver = enum.SQLite3 _ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "") assert.Equal(t, "", c.DatabasePassword()) @@ -222,19 +224,19 @@ func TestShouldAutoRotateDatabase(t *testing.T) { t.Run("PortalAlwaysFalse", func(t *testing.T) { conf := NewMinimalTestConfig(t.TempDir()) conf.Options().NodeRole = cluster.RolePortal - conf.Options().DatabaseDriver = MySQL + conf.Options().DatabaseDriver = enum.MySQL assert.False(t, conf.ShouldAutoRotateDatabase()) }) t.Run("NonMySQLDriverFalse", func(t *testing.T) { conf := NewMinimalTestConfig(t.TempDir()) - conf.Options().DatabaseDriver = SQLite3 + conf.Options().DatabaseDriver = enum.SQLite3 assert.False(t, conf.ShouldAutoRotateDatabase()) }) t.Run("MySQLMissingFieldsTrue", func(t *testing.T) { conf := NewMinimalTestConfig(t.TempDir()) - conf.Options().DatabaseDriver = MySQL + conf.Options().DatabaseDriver = enum.MySQL conf.Options().DatabaseName = "photoprism" conf.Options().DatabaseUser = "" conf.Options().DatabasePassword = "" @@ -246,7 +248,7 @@ func TestConfig_DatabaseDSN(t *testing.T) { c := NewConfig(CliTestContext()) resetDatabaseOptions(c) driver := c.DatabaseDriver() - assert.Equal(t, SQLite3, driver) + assert.Equal(t, enum.SQLite3, driver) c.options.DatabaseDSN = "" c.options.DatabaseDriver = "MariaDB" assert.Equal(t, "photoprism:@tcp(localhost)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=15s", c.DatabaseDSN()) @@ -263,7 +265,7 @@ func TestConfig_DatabaseDSN(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) - conf.options.DatabaseDriver = MySQL + conf.options.DatabaseDriver = enum.MySQL conf.options.DatabaseServer = "proxy.internal:6032" conf.options.DatabaseName = "tenantdb" conf.options.DatabaseUser = "tenant" @@ -279,7 +281,7 @@ func TestConfig_DatabaseDSN(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) - conf.options.DatabaseDriver = MySQL + conf.options.DatabaseDriver = enum.MySQL conf.options.DatabaseServer = "/var/run/mysql.sock" conf.options.DatabaseName = "tenantdb" conf.options.DatabaseUser = "tenant" @@ -305,7 +307,7 @@ func TestConfig_DatabaseDSNFlags(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) - conf.options.DatabaseDriver = MySQL + conf.options.DatabaseDriver = enum.MySQL conf.options.Deprecated.DatabaseDsn = "user:pass@tcp(db.internal:3306)/photoprism" assert.False(t, conf.NoDatabaseDSN()) @@ -319,10 +321,10 @@ func TestConfig_ReportDatabaseDSN(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) - assert.Equal(t, SQLite3, conf.DatabaseDriver()) + assert.Equal(t, enum.SQLite3, conf.DatabaseDriver()) assert.True(t, conf.ReportDatabaseDSN()) - conf.options.DatabaseDriver = MySQL + conf.options.DatabaseDriver = enum.MySQL conf.options.DatabaseDSN = "" assert.False(t, conf.ReportDatabaseDSN()) @@ -335,7 +337,7 @@ func TestConfig_DatabaseFile(t *testing.T) { // Ensure SQLite defaults resetDatabaseOptions(c) driver := c.DatabaseDriver() - assert.Equal(t, SQLite3, driver) + assert.Equal(t, enum.SQLite3, driver) c.options.DatabaseDSN = "" assert.Equal(t, ProjectRoot+"/storage/testdata/index.db", c.DatabaseFile()) assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN()) diff --git a/internal/config/config_thumb_test.go b/internal/config/config_thumb_test.go index 0db6bf2f7..d894e575d 100644 --- a/internal/config/config_thumb_test.go +++ b/internal/config/config_thumb_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/internal/thumb" + "github.com/photoprism/photoprism/pkg/enum" ) func TestConfig_ConvertSize(t *testing.T) { @@ -48,7 +49,7 @@ func TestConfig_ThumbFilter(t *testing.T) { assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter()) c.options.ThumbFilter = "linear" assert.Equal(t, thumb.ResampleLinear, c.ThumbFilter()) - c.options.ThumbFilter = Auto + c.options.ThumbFilter = enum.Auto assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter()) c.options.ThumbFilter = "" assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter()) @@ -92,7 +93,7 @@ func TestConfig_PngSize(t *testing.T) { func TestConfig_ThumbLibrary(t *testing.T) { c := NewConfig(CliTestContext()) assert.False(t, c.DisableVips()) - c.options.ThumbLibrary = Auto + c.options.ThumbLibrary = enum.Auto assert.Equal(t, "vips", c.ThumbLibrary()) c.options.DisableVips = true assert.Equal(t, "imaging", c.ThumbLibrary()) diff --git a/internal/config/flags.go b/internal/config/flags.go index b2c32c31f..2acaea8c3 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -15,6 +15,7 @@ import ( "github.com/photoprism/photoprism/internal/service/hub/places" "github.com/photoprism/photoprism/internal/thumb" "github.com/photoprism/photoprism/pkg/authn" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/http/header" "github.com/photoprism/photoprism/pkg/http/scheme" @@ -941,7 +942,7 @@ var Flags = CliFlags{ Flag: &cli.StringFlag{ Name: "database-provision-driver", Usage: "auto-provisioning `DRIVER` (auto, mysql)", - Value: Auto, + Value: enum.Auto, EnvVars: EnvVars("DATABASE_PROVISION_DRIVER"), Hidden: true, }}, { @@ -1113,7 +1114,7 @@ var Flags = CliFlags{ Name: "thumb-library", Aliases: []string{"thumbs"}, Usage: "image processing `LIBRARY` to be used for generating thumbnails (auto, imaging, vips)", - Value: Auto, + Value: enum.Auto, EnvVars: EnvVars("THUMB_LIBRARY"), }}, { Flag: &cli.StringFlag{ diff --git a/internal/config/report_test.go b/internal/config/report_test.go index d02a55697..f1286cdab 100644 --- a/internal/config/report_test.go +++ b/internal/config/report_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/pkg/enum" ) func TestConfig_Report(t *testing.T) { @@ -34,7 +36,7 @@ func TestConfig_ReportDatabaseSection(t *testing.T) { rows, _ := conf.Report() values := collect(rows) - assert.Equal(t, SQLite3, values["database-driver"]) + assert.Equal(t, enum.SQLite3, values["database-driver"]) assert.Equal(t, conf.DatabaseDSN(), values["database-dsn"]) _, hasName := values["database-name"] assert.False(t, hasName) @@ -43,7 +45,7 @@ func TestConfig_ReportDatabaseSection(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) - conf.options.DatabaseDriver = MySQL + conf.options.DatabaseDriver = enum.MySQL conf.options.DatabaseServer = "db.internal:3306" conf.options.DatabaseName = "photoprism" conf.options.DatabaseUser = "app" @@ -52,7 +54,7 @@ func TestConfig_ReportDatabaseSection(t *testing.T) { rows, _ := conf.Report() values := collect(rows) - assert.Equal(t, MySQL, values["database-driver"]) + assert.Equal(t, enum.MySQL, values["database-driver"]) assert.Equal(t, "photoprism", values["database-name"]) assert.Equal(t, "db.internal:3306", values["database-server"]) assert.Equal(t, "db.internal", values["database-host"]) @@ -66,13 +68,13 @@ func TestConfig_ReportDatabaseSection(t *testing.T) { conf := NewConfig(CliTestContext()) resetDatabaseOptions(conf) - conf.options.DatabaseDriver = MySQL + conf.options.DatabaseDriver = enum.MySQL conf.options.DatabaseDSN = "user:pass@tcp(db.internal:3306)/photoprism" rows, _ := conf.Report() values := collect(rows) - assert.Equal(t, MySQL, values["database-driver"]) + assert.Equal(t, enum.MySQL, values["database-driver"]) assert.Equal(t, "user:***@tcp(db.internal:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=15s", values["database-dsn"]) _, hasName := values["database-name"] assert.False(t, hasName) diff --git a/internal/config/test.go b/internal/config/test.go index dd8d864f3..c5fa27801 100644 --- a/internal/config/test.go +++ b/internal/config/test.go @@ -23,7 +23,7 @@ import ( "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/capture" "github.com/photoprism/photoprism/pkg/clean" - "github.com/photoprism/photoprism/pkg/dsn" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt/report" @@ -127,19 +127,19 @@ func NewTestOptionsForPath(dbName, dataPath string) *Options { // Set default test database driver. if testDriver == "test" || testDriver == "sqlite" || testDriver == "" || testDsn == "" { - testDriver = dsn.DriverSQLite3 + testDriver = enum.SQLite3 } // Set default database DSN. - if testDriver == dsn.DriverSQLite3 { + if testDriver == enum.SQLite3 { if testDsn == "" && dbName != "" { if testDsn = fmt.Sprintf(".%s.db", clean.TypeLower(dbName)); !fs.FileExists(testDsn) { log.Tracef("sqlite: test database %s does not already exist", clean.Log(testDsn)) } else if err := os.Remove(testDsn); err != nil { log.Errorf("sqlite: failed to remove existing test database %s (%s)", clean.Log(testDsn), err) } - } else if testDsn == "" || testDsn == dsn.SQLiteTestDB { - testDsn = dsn.SQLiteTestDB + } else if testDsn == "" || testDsn == enum.SQLiteTestDB { + testDsn = enum.SQLiteTestDB if !fs.FileExists(testDsn) { log.Tracef("sqlite: test database %s does not already exist", clean.Log(testDsn)) } else if err := os.Remove(testDsn); err != nil { @@ -205,7 +205,7 @@ func NewTestOptionsError() *Options { OriginalsPath: dataPath + "/originals", ImportPath: dataPath + "/import", TempPath: dataPath + "/temp", - DatabaseDriver: SQLite3, + DatabaseDriver: enum.SQLite3, DatabaseDSN: ".test-error.db", } @@ -244,7 +244,7 @@ func NewMinimalTestConfigWithDb(dbName, dataPath string) *Config { cachedDb := false // Try to restore test db from cache. - if len(testDbCache) > 0 && c.DatabaseDriver() == SQLite3 && !fs.FileExists(c.DatabaseDSN()) { + if len(testDbCache) > 0 && c.DatabaseDriver() == enum.SQLite3 && !fs.FileExists(c.DatabaseDSN()) { if err := os.WriteFile(c.DatabaseDSN(), testDbCache, fs.ModeFile); err != nil { log.Warnf("config: %s (restore test database)", err) } else { @@ -264,7 +264,7 @@ func NewMinimalTestConfigWithDb(dbName, dataPath string) *Config { c.InitTestDb() - if testDbCache == nil && c.DatabaseDriver() == SQLite3 && fs.FileExistsNotEmpty(c.DatabaseDSN()) { + if testDbCache == nil && c.DatabaseDriver() == enum.SQLite3 && fs.FileExistsNotEmpty(c.DatabaseDSN()) { testDbMutex.Lock() defer testDbMutex.Unlock() diff --git a/internal/entity/db_conn.go b/internal/entity/db_conn.go index 14d83a92b..4f3bc77ab 100644 --- a/internal/entity/db_conn.go +++ b/internal/entity/db_conn.go @@ -8,16 +8,6 @@ import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" - - "github.com/photoprism/photoprism/pkg/dsn" -) - -// Supported test databases. -const ( - MySQL = dsn.DriverMySQL - SQLite3 = dsn.DriverSQLite3 - SQLiteTestDB = ".test.db" - SQLiteMemoryDSN = ":memory:?cache=shared" ) // dbConn is the global gorm.DB connection provider. diff --git a/internal/entity/entity_counts.go b/internal/entity/entity_counts.go index ad885f83d..fee587d85 100644 --- a/internal/entity/entity_counts.go +++ b/internal/entity/entity_counts.go @@ -11,6 +11,7 @@ import ( "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/time/unix" ) @@ -103,7 +104,7 @@ func UpdateSubjectCounts(public bool) (err error) { condition := gorm.Expr("subj_type = ?", SubjPerson) switch DbDialect() { - case MySQL: + case enum.MySQL: res = Db().Exec(`UPDATE ? LEFT JOIN ( SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos FROM files f @@ -114,7 +115,7 @@ func UpdateSubjectCounts(public bool) (err error) { SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END, subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END WHERE ?`, gorm.Expr(subjTable), photosJoin, condition) - case SQLite3: + case enum.SQLite3: // Update files count. res = Db().Table(subjTable). UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id)"+ @@ -187,7 +188,7 @@ func UpdateLabelCounts() (err error) { start := time.Now() var res *gorm.DB - if IsDialect(MySQL) { + if IsDialect(enum.MySQL) { res = Db().Exec(`UPDATE labels LEFT JOIN ( SELECT p2.label_id, COUNT(DISTINCT photo_id) AS label_photos FROM ( SELECT pl.label_id as label_id, p.id AS photo_id FROM photos p @@ -201,7 +202,7 @@ func UpdateLabelCounts() (err error) { ) p2 GROUP BY p2.label_id ) b ON b.label_id = labels.id SET photo_count = CASE WHEN b.label_photos IS NULL THEN 0 ELSE b.label_photos END`) - } else if IsDialect(SQLite3) { + } else if IsDialect(enum.SQLite3) { res = Db(). Table("labels"). UpdateColumn("photo_count", diff --git a/internal/entity/entity_init.go b/internal/entity/entity_init.go index 488c80465..13c10e30c 100644 --- a/internal/entity/entity_init.go +++ b/internal/entity/entity_init.go @@ -6,6 +6,7 @@ import ( "github.com/photoprism/photoprism/internal/entity/migrate" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" ) @@ -55,13 +56,13 @@ func InitTestDb(driver, dbDsn string) *DbConn { // Set default test database driver. if driver == "test" || driver == "sqlite" || driver == "" || dbDsn == "" { - driver = SQLite3 + driver = enum.SQLite3 } // Set default database DSN. - if driver == SQLite3 { - if dbDsn == "" || dbDsn == SQLiteTestDB { - dbDsn = SQLiteTestDB + if driver == enum.SQLite3 { + if dbDsn == "" || dbDsn == enum.SQLiteTestDB { + dbDsn = enum.SQLiteTestDB if !fs.FileExists(dbDsn) { log.Debugf("sqlite: test database %s does not already exist", clean.Log(dbDsn)) } else if err := os.Remove(dbDsn); err != nil { diff --git a/internal/entity/face.go b/internal/entity/face.go index c57c849c0..42d6f308c 100644 --- a/internal/entity/face.go +++ b/internal/entity/face.go @@ -11,6 +11,7 @@ import ( "time" "github.com/photoprism/photoprism/internal/ai/face" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -340,7 +341,7 @@ func (m *Face) RefreshPhotos() error { var err error switch DbDialect() { - case MySQL: + case enum.MySQL: update := fmt.Sprintf(`UPDATE photos p JOIN files f ON f.photo_id = p.id JOIN %s m ON m.file_uid = f.file_uid SET p.checked_at = NULL WHERE m.face_id = ?`, Marker{}.TableName()) err = UnscopedDb().Exec(update, m.ID).Error diff --git a/internal/entity/file.go b/internal/entity/file.go index 066571081..04d5aa1de 100644 --- a/internal/entity/file.go +++ b/internal/entity/file.go @@ -17,6 +17,7 @@ import ( "github.com/photoprism/photoprism/internal/ai/face" "github.com/photoprism/photoprism/internal/config/customize" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/media" "github.com/photoprism/photoprism/pkg/media/colors" @@ -129,7 +130,7 @@ func (m File) RegenerateIndex() { } switch DbDialect() { - case MySQL: + case enum.MySQL: Log("files", "regenerate photo_taken_at", Db().Exec("UPDATE files JOIN ? p ON p.id = files.photo_id SET files.photo_taken_at = p.taken_at_local WHERE ?", gorm.Expr(photosTable), updateWhere).Error) @@ -141,7 +142,7 @@ func (m File) RegenerateIndex() { Log("files", "regenerate time_index", Db().Exec("UPDATE files SET time_index = CASE WHEN media_id IS NOT NULL AND photo_taken_at IS NOT NULL THEN CONCAT(100000000000000 - CAST(photo_taken_at AS UNSIGNED), '-', media_id) ELSE NULL END WHERE ?", updateWhere).Error) - case SQLite3: + case enum.SQLite3: Log("files", "regenerate photo_taken_at", Db().Exec("UPDATE files SET photo_taken_at = (SELECT p.taken_at_local FROM ? p WHERE p.id = photo_id) WHERE ?", gorm.Expr(photosTable), updateWhere).Error) diff --git a/internal/entity/marker.go b/internal/entity/marker.go index 90f8a6537..c6e3e1290 100644 --- a/internal/entity/marker.go +++ b/internal/entity/marker.go @@ -14,6 +14,7 @@ import ( "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/thumb/crop" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -575,7 +576,7 @@ func (m *Marker) RefreshPhotos() error { var err error switch DbDialect() { - case MySQL: + case enum.MySQL: err = UnscopedDb().Exec(`UPDATE photos p JOIN files f ON f.photo_id = p.id JOIN ? m ON m.file_uid = f.file_uid SET p.checked_at = NULL WHERE m.marker_uid = ?`, diff --git a/internal/entity/migrate/dialects.go b/internal/entity/migrate/dialects.go index 9ee0d400e..41cd23b62 100644 --- a/internal/entity/migrate/dialects.go +++ b/internal/entity/migrate/dialects.go @@ -1,19 +1,17 @@ package migrate -import "sync" +import ( + "sync" -// Supported database dialects. -const ( - MySQL = "mysql" - SQLite3 = "sqlite3" + "github.com/photoprism/photoprism/pkg/enum" ) var Dialects = map[string]Migrations{ - MySQL: DialectMySQL, - SQLite3: DialectSQLite3, + enum.MySQL: DialectMySQL, + enum.SQLite3: DialectSQLite3, } var once = map[string]*sync.Once{ - MySQL: {}, - SQLite3: {}, + enum.MySQL: {}, + enum.SQLite3: {}, } diff --git a/internal/entity/mysql8_test.go b/internal/entity/mysql8_test.go index 3ce57e920..7c6842fbc 100644 --- a/internal/entity/mysql8_test.go +++ b/internal/entity/mysql8_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/photoprism/photoprism/internal/entity/migrate" + "github.com/photoprism/photoprism/pkg/enum" "github.com/jinzhu/gorm" ) @@ -17,7 +18,7 @@ func TestMySQL8(t *testing.T) { t.Skip("skipping MySQL 8 test: PHOTOPRISM_TEST_DSN_MYSQL8 is not set") } - dbDriver := MySQL + dbDriver := enum.MySQL db, err := gorm.Open(dbDriver, dbDsn) if err != nil || db == nil { diff --git a/internal/entity/photo_estimate.go b/internal/entity/photo_estimate.go index 76c361976..d04f43053 100644 --- a/internal/entity/photo_estimate.go +++ b/internal/entity/photo_estimate.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/geo" "github.com/photoprism/photoprism/pkg/txt" @@ -99,14 +100,14 @@ func (m *Photo) EstimateLocation(force bool) { var mostRecent Photos switch DbDialect() { - case MySQL: + case enum.MySQL: err = UnscopedDb(). Where("photo_lat <> 0 AND photo_lng <> 0"). Where("place_src <> '' AND place_src <> ? AND place_id IS NOT NULL AND place_id <> '' AND place_id <> 'zz'", SrcEstimate). Where("taken_src <> '' AND taken_at BETWEEN CAST(? AS DATETIME) AND CAST(? AS DATETIME)", rangeMin, rangeMax). Order(gorm.Expr("ABS(TIMESTAMPDIFF(SECOND, taken_at, ?))", m.TakenAt)).Limit(2). Preload("Place").Find(&mostRecent).Error - case SQLite3: + case enum.SQLite3: err = UnscopedDb(). Where("photo_lat <> 0 AND photo_lng <> 0"). Where("place_src <> '' AND place_src <> ? AND place_id IS NOT NULL AND place_id <> '' AND place_id <> 'zz'", SrcEstimate). diff --git a/internal/entity/photo_merge.go b/internal/entity/photo_merge.go index 0fc24e146..45694b837 100644 --- a/internal/entity/photo_merge.go +++ b/internal/entity/photo_merge.go @@ -5,6 +5,7 @@ import ( "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -108,11 +109,11 @@ func (m *Photo) Merge(mergeMeta, mergeUuid bool) (original Photo, merged Photos, logResult(UnscopedDb().Exec("UPDATE photos SET photo_quality = -1, deleted_at = ? WHERE id = ?", Now(), merge.ID)) switch DbDialect() { - case MySQL: + case enum.MySQL: logResult(UnscopedDb().Exec("UPDATE IGNORE photos_keywords SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID)) logResult(UnscopedDb().Exec("UPDATE IGNORE photos_labels SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID)) logResult(UnscopedDb().Exec("UPDATE IGNORE photos_albums SET photo_uid = ? WHERE photo_uid = ?", original.PhotoUID, merge.PhotoUID)) - case SQLite3: + case enum.SQLite3: logResult(UnscopedDb().Exec("UPDATE OR IGNORE photos_keywords SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID)) logResult(UnscopedDb().Exec("UPDATE OR IGNORE photos_labels SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID)) logResult(UnscopedDb().Exec("UPDATE OR IGNORE photos_albums SET photo_uid = ? WHERE photo_uid = ?", original.PhotoUID, merge.PhotoUID)) diff --git a/internal/entity/query/albums.go b/internal/entity/query/albums.go index 95691da52..79c39f137 100644 --- a/internal/entity/query/albums.go +++ b/internal/entity/query/albums.go @@ -10,6 +10,7 @@ import ( "github.com/photoprism/photoprism/internal/entity/sortby" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/media" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -120,7 +121,7 @@ func UpdateAlbumDates() error { defer mutex.Index.Unlock() switch DbDialect() { - case MySQL: + case enum.MySQL: return UnscopedDb().Exec(`UPDATE albums INNER JOIN ( SELECT photo_path, MAX(taken_at_local) AS taken_max FROM photos WHERE taken_src = 'meta' AND photos.photo_quality >= 3 AND photos.deleted_at IS NULL diff --git a/internal/entity/query/covers.go b/internal/entity/query/covers.go index 54aaf7798..210d7a11c 100644 --- a/internal/entity/query/covers.go +++ b/internal/entity/query/covers.go @@ -12,6 +12,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/media" ) @@ -45,7 +46,7 @@ func UpdateAlbumManualCovers(albums ...entity.Album) (err error) { condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumManual, entity.SrcAuto) switch DbDialect() { - case MySQL: + case enum.MySQL: res = Db().Exec(`UPDATE albums LEFT JOIN ( SELECT p2.album_uid, f.file_hash FROM files f, ( SELECT pa.album_uid, max(p.id) AS photo_id FROM photos p @@ -54,7 +55,7 @@ func UpdateAlbumManualCovers(albums ...entity.Album) (err error) { GROUP BY pa.album_uid) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?) ) b ON b.album_uid = albums.album_uid SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition) - case SQLite3: + case enum.SQLite3: res = Db().Table(entity.Album{}.TableName()). UpdateColumn("thumb", gorm.Expr(`( SELECT f.file_hash FROM files f @@ -108,7 +109,7 @@ func UpdateAlbumFolderCovers(albums ...entity.Album) (err error) { condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumFolder, entity.SrcAuto) switch DbDialect() { - case MySQL: + case enum.MySQL: res = Db().Exec(`UPDATE albums LEFT JOIN ( SELECT p2.photo_path, f.file_hash FROM files f, ( SELECT p.photo_path, max(p.id) AS photo_id FROM photos p @@ -116,7 +117,7 @@ func UpdateAlbumFolderCovers(albums ...entity.Album) (err error) { GROUP BY p.photo_path) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?) ) b ON b.photo_path = albums.album_path SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition) - case SQLite3: + case enum.SQLite3: res = Db().Table(entity.Album{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( SELECT f.file_hash FROM files f,( SELECT p.photo_path, max(p.id) AS photo_id FROM photos p @@ -171,7 +172,7 @@ func UpdateAlbumMonthCovers(albums ...entity.Album) (err error) { condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumMonth, entity.SrcAuto) switch DbDialect() { - case MySQL: + case enum.MySQL: res = Db().Exec(`UPDATE albums LEFT JOIN ( SELECT p2.photo_year, p2.photo_month, f.file_hash FROM files f, ( SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p @@ -179,7 +180,7 @@ func UpdateAlbumMonthCovers(albums ...entity.Album) (err error) { GROUP BY p.photo_year, p.photo_month) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?) ) b ON b.photo_year = albums.album_year AND b.photo_month = albums.album_month SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition) - case SQLite3: + case enum.SQLite3: res = Db().Table(entity.Album{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( SELECT f.file_hash FROM files f,( SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p @@ -322,7 +323,7 @@ func refreshFolderAlbumCover(album entity.Album) error { } switch DbDialect() { - case MySQL: + case enum.MySQL: res := Db().Exec(`UPDATE albums LEFT JOIN ( SELECT p2.photo_path, f.file_hash FROM files f, ( SELECT p.photo_path, max(p.id) AS photo_id FROM photos p @@ -338,7 +339,7 @@ func refreshFolderAlbumCover(album entity.Album) error { ) return res.Error - case SQLite3: + case enum.SQLite3: res := Db().Table(entity.Album{}.TableName()). Where("album_uid = ? AND album_type = ? AND thumb_src = ?", album.AlbumUID, entity.AlbumFolder, entity.SrcAuto). UpdateColumn("thumb", gorm.Expr(`( @@ -366,7 +367,7 @@ func refreshMonthAlbumCover(album entity.Album) error { } switch DbDialect() { - case MySQL: + case enum.MySQL: res := Db().Exec(`UPDATE albums LEFT JOIN ( SELECT p2.photo_year, p2.photo_month, f.file_hash FROM files f, ( SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p @@ -383,7 +384,7 @@ func refreshMonthAlbumCover(album entity.Album) error { ) return res.Error - case SQLite3: + case enum.SQLite3: res := Db().Table(entity.Album{}.TableName()). Where("album_uid = ? AND album_type = ? AND thumb_src = ?", album.AlbumUID, entity.AlbumMonth, entity.SrcAuto). UpdateColumn("thumb", gorm.Expr(`( @@ -416,7 +417,7 @@ func UpdateLabelCovers() (err error) { condition := gorm.Expr("thumb_src = ?", entity.SrcAuto) switch DbDialect() { - case MySQL: + case enum.MySQL: res = Db().Exec(`UPDATE labels LEFT JOIN ( SELECT p2.label_id, f.file_hash FROM files f, ( SELECT pl.label_id as label_id, max(p.id) AS photo_id FROM photos p @@ -432,7 +433,7 @@ func UpdateLabelCovers() (err error) { ) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?) AND f.file_missing = 0 ) b ON b.label_id = labels.id SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition) - case SQLite3: + case enum.SQLite3: res = Db().Table(entity.Label{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( SELECT f.file_hash FROM files f JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100 @@ -494,7 +495,7 @@ func UpdateSubjectCovers(public bool) (err error) { // Compose SQL update query. switch DbDialect() { - case MySQL: + case enum.MySQL: res = Db().Exec(`UPDATE subjects LEFT JOIN ( SELECT m.subj_uid, m.q, MAX(m.thumb) AS marker_thumb FROM markers m @@ -508,7 +509,7 @@ func UpdateSubjectCovers(public bool) (err error) { photosJoin, condition, ) - case SQLite3: + case enum.SQLite3: // from := gorm.Expr(fmt.Sprintf("%s m WHERE m.subj_uid = %s.subj_uid ", markerTable, subjTable)) res = Db().Table(entity.Subject{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( diff --git a/internal/entity/query/file_selection.go b/internal/entity/query/file_selection.go index 54bd8d7ab..43adefc9f 100644 --- a/internal/entity/query/file_selection.go +++ b/internal/entity/query/file_selection.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/internal/entity" @@ -98,9 +99,9 @@ func SelectedFiles(frm form.Selection, o FileSelection) (results entity.Files, e var concat string switch DbDialect() { - case MySQL: + case enum.MySQL: concat = "CONCAT(a.path, '/%')" - case SQLite3: + case enum.SQLite3: concat = "a.path || '/%'" default: return results, fmt.Errorf("unknown sql dialect: %s", DbDialect()) diff --git a/internal/entity/query/folders.go b/internal/entity/query/folders.go index 5a2ec3dcb..7f9f31a43 100644 --- a/internal/entity/query/folders.go +++ b/internal/entity/query/folders.go @@ -5,6 +5,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/mutex" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/media" ) @@ -75,7 +76,7 @@ func UpdateFolderDates() error { defer mutex.Index.Unlock() switch DbDialect() { - case MySQL: + case enum.MySQL: return UnscopedDb().Exec(`UPDATE folders INNER JOIN (SELECT photo_path, MAX(taken_at_local) AS taken_max diff --git a/internal/entity/query/photo_selection.go b/internal/entity/query/photo_selection.go index b282dec3d..746b179a7 100644 --- a/internal/entity/query/photo_selection.go +++ b/internal/entity/query/photo_selection.go @@ -6,6 +6,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/pkg/enum" ) // SelectedPhotos finds photos based on the given selection form, e.g. for adding them to an album. @@ -24,9 +25,9 @@ func SelectedPhotos(frm form.Selection) (results entity.Photos, err error) { var concat string switch DbDialect() { - case MySQL: + case enum.MySQL: concat = "CONCAT(a.path, '/%')" - case SQLite3: + case enum.SQLite3: concat = "a.path || '/%'" default: return results, fmt.Errorf("unknown sql dialect: %s", DbDialect()) diff --git a/internal/entity/query/query.go b/internal/entity/query/query.go index d59f7dda5..ccc72af67 100644 --- a/internal/entity/query/query.go +++ b/internal/entity/query/query.go @@ -29,16 +29,11 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/pkg/enum" ) var log = event.Log -// Supported database dialect identifiers. -const ( - MySQL = "mysql" - SQLite3 = "sqlite3" -) - // Cols represents a list of database columns. type Cols []string @@ -79,7 +74,7 @@ func DbDialect() string { // BatchSize returns the maximum query parameter number based on the current sql database dialect. func BatchSize() int { switch DbDialect() { - case SQLite3: + case enum.SQLite3: return 333 default: return 1000 diff --git a/internal/entity/search/conditions.go b/internal/entity/search/conditions.go index e4db13ab1..7530bfa9e 100644 --- a/internal/entity/search/conditions.go +++ b/internal/entity/search/conditions.go @@ -14,7 +14,7 @@ import ( // expressions. It strips operators that we don't expect to persist in the // statement and lets callers provide their own surrounding wildcards. func Like(s string) string { - return strings.Trim(clean.SqlString(s), " |&*%") + return strings.Trim(clean.SQLString(s, Db().Dialect().GetName()), " |&*%") } // LikeAny builds OR-chained LIKE predicates for a text column. The input string diff --git a/internal/entity/search/conditions_test.go b/internal/entity/search/conditions_test.go index c387ec138..c61dec7e3 100644 --- a/internal/entity/search/conditions_test.go +++ b/internal/entity/search/conditions_test.go @@ -8,6 +8,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/txt" ) @@ -16,8 +17,11 @@ func TestLike(t *testing.T) { assert.Equal(t, "", Like("")) }) t.Run("Special", func(t *testing.T) { - s := " ' \" \t \n %_''" - exp := "'' \"\" %_''''" + s := " ' \" \t \n %_''\\" + exp := "'' \" %_''''\\" + if entity.DbDialect() == enum.MySQL { + exp = "'' \" %_''''\\\\" + } result := Like(s) t.Logf("String..: %s", s) t.Logf("Expected: %s", exp) @@ -135,7 +139,7 @@ func TestLikeAnyWord(t *testing.T) { t.Fatalf("two where conditions expected: %#v", w) } else { assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0]) - assert.Equal(t, "k.keyword LIKE '\"\"us''a%'", w[1]) + assert.Equal(t, "k.keyword LIKE '\"us''a%'", w[1]) } }) } diff --git a/internal/entity/sortby/random.go b/internal/entity/sortby/random.go index 3a79a7b98..7f47dbebe 100644 --- a/internal/entity/sortby/random.go +++ b/internal/entity/sortby/random.go @@ -4,21 +4,18 @@ import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" -) -const ( - MySQL = "mysql" - SQLite3 = "sqlite3" + "github.com/photoprism/photoprism/pkg/enum" ) // RandomExpr returns the name of the random function depending on the SQL dialect. func RandomExpr(dialect gorm.Dialect) *gorm.SqlExpr { switch dialect.GetName() { - case MySQL: + case enum.MySQL: // A seed integer can be passed as an argument, e.g. "RAND(2342)", to generate // reproducible pseudo-random values, see https://mariadb.com/kb/en/rand/. return gorm.Expr("RAND()") - case SQLite3: + case enum.SQLite3: // SQLite does not support specifying a seed to generate a deterministic sequence // of pseudo-random values, see https://www.sqlite.org/lang_corefunc.html#random. return gorm.Expr("RANDOM()") diff --git a/internal/entity/sortby/random_test.go b/internal/entity/sortby/random_test.go index 4f7f91f02..d127b3fd1 100644 --- a/internal/entity/sortby/random_test.go +++ b/internal/entity/sortby/random_test.go @@ -5,12 +5,14 @@ import ( "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/pkg/enum" + "github.com/stretchr/testify/assert" ) func TestRandomExpr(t *testing.T) { - mysql, _ := gorm.GetDialect(MySQL) - sqlite3, _ := gorm.GetDialect(SQLite3) + mysql, _ := gorm.GetDialect(enum.MySQL) + sqlite3, _ := gorm.GetDialect(enum.SQLite3) assert.Equal(t, gorm.Expr("RAND()"), RandomExpr(mysql)) assert.Equal(t, gorm.Expr("RANDOM()"), RandomExpr(sqlite3)) diff --git a/internal/entity/subject.go b/internal/entity/subject.go index 531213ce2..46a0e4c42 100644 --- a/internal/entity/subject.go +++ b/internal/entity/subject.go @@ -11,6 +11,7 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt" ) @@ -490,7 +491,7 @@ func (m *Subject) RefreshPhotos() error { var err error switch DbDialect() { - case MySQL: + case enum.MySQL: update := fmt.Sprintf(`UPDATE photos p JOIN files f ON f.photo_id = p.id JOIN %s m ON m.file_uid = f.file_uid SET p.checked_at = NULL WHERE m.subj_uid = ?`, Marker{}.TableName()) err = UnscopedDb().Exec(update, m.SubjUID).Error diff --git a/internal/photoprism/backup/database.go b/internal/photoprism/backup/database.go index a463d9db4..95c0ab70d 100644 --- a/internal/photoprism/backup/database.go +++ b/internal/photoprism/backup/database.go @@ -15,10 +15,10 @@ import ( "github.com/dustin/go-humanize/english" - "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" ) @@ -73,7 +73,7 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er var cmd *exec.Cmd switch c.DatabaseDriver() { - case config.MySQL, config.MariaDB: + case enum.MySQL, enum.MariaDB: // Connect via Unix Domain Socket? if socketName := c.DatabaseServer(); strings.HasPrefix(socketName, "/") { cmd = exec.Command( // #nosec G204 database connection parameters from trusted config @@ -112,7 +112,7 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er c.DatabaseName(), ) } - case config.SQLite3: + case enum.SQLite3: if !fs.FileExistsNotEmpty(c.DatabaseFile()) { return fmt.Errorf("sqlite database file %s not found", clean.LogQuote(c.DatabaseFile())) } @@ -254,7 +254,7 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er var cmd *exec.Cmd switch c.DatabaseDriver() { - case config.MySQL, config.MariaDB: + case enum.MySQL, enum.MariaDB: // Connect via Unix Domain Socket? if socketName := c.DatabaseServer(); strings.HasPrefix(socketName, "/") { cmd = exec.Command( // #nosec G204 database connection parameters from config @@ -296,7 +296,7 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er c.DatabaseName(), ) } - case config.SQLite3: + case enum.SQLite3: log.Infoln("restore: dropping existing sqlite database tables") tables.Drop(c.Db()) cmd = exec.Command( // #nosec G204 sqlite restore uses configured binary and db path diff --git a/internal/service/cluster/node/bootstrap_test.go b/internal/service/cluster/node/bootstrap_test.go index 7816b516e..9ec7b71de 100644 --- a/internal/service/cluster/node/bootstrap_test.go +++ b/internal/service/cluster/node/bootstrap_test.go @@ -14,7 +14,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/service/cluster" - "github.com/photoprism/photoprism/pkg/dsn" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -61,7 +61,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) { Secrets: &cluster.RegisterSecrets{ClientSecret: cluster.ExampleClientSecret}, JWKSUrl: jwksURL, Database: cluster.RegisterDatabase{ - Driver: dsn.DriverMySQL, + Driver: enum.MySQL, Host: "db.local", Port: 3306, Name: "pp_db", @@ -92,7 +92,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) { expectedAppName = c.About() expectedAppVersion = c.Version() // Gate rotate=true: driver mysql and no DSN/fields. - c.Options().DatabaseDriver = dsn.DriverMySQL + c.Options().DatabaseDriver = enum.MySQL c.Options().DatabaseDSN = "" c.Options().DatabaseName = "" c.Options().DatabaseUser = "" @@ -105,7 +105,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) { assert.Equal(t, cluster.ExampleClientSecret, c.NodeClientSecret()) // DSN branch should be preferred and persisted. assert.Contains(t, c.Options().DatabaseDSN, "@tcp(db.local:3306)/pp_db") - assert.Equal(t, dsn.DriverMySQL, c.Options().DatabaseDriver) + assert.Equal(t, enum.MySQL, c.Options().DatabaseDriver) assert.Equal(t, srv.URL+"/.well-known/jwks.json", c.JWKSUrl()) assert.Equal(t, "192.0.2.0/24", c.ClusterCIDR()) } @@ -277,7 +277,7 @@ func TestRegister_SQLite_NoDBPersist(t *testing.T) { // NodeClientSecret should persist, but DB should remain SQLite (no DSN update). assert.Equal(t, cluster.ExampleClientSecret, c.NodeClientSecret()) - assert.Equal(t, config.SQLite3, c.DatabaseDriver()) + assert.Equal(t, enum.SQLite3, c.DatabaseDriver()) assert.Equal(t, origDSN, c.Options().DatabaseDSN) assert.Equal(t, srv.URL+"/.well-known/jwks.json", c.JWKSUrl()) assert.Equal(t, "203.0.113.0/24", c.ClusterCIDR()) diff --git a/internal/service/cluster/provisioner/credentials.go b/internal/service/cluster/provisioner/credentials.go index b27bea3c3..741b94e6b 100644 --- a/internal/service/cluster/provisioner/credentials.go +++ b/internal/service/cluster/provisioner/credentials.go @@ -8,7 +8,7 @@ import ( "time" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/pkg/dsn" + "github.com/photoprism/photoprism/pkg/enum" ) // Credentials contains the connection details returned when ensuring a node database. @@ -35,9 +35,9 @@ func EnsureCredentials(ctx context.Context, conf *config.Config, nodeUUID, nodeN driver := strings.ToLower(DatabaseDriver) switch driver { - case dsn.DriverMySQL, dsn.DriverMariaDB: + case enum.MySQL, enum.MariaDB: // ok - case dsn.DriverSQLite3, dsn.DriverPostgres: + case enum.SQLite3, enum.Postgres: return out, false, errors.New("database must be MySQL/MariaDB for auto-provisioning") default: // Driver is configured externally for the provisioner (decoupled from app config). diff --git a/internal/service/cluster/provisioner/naming.go b/internal/service/cluster/provisioner/naming.go index 0a486522f..c802d30ee 100644 --- a/internal/service/cluster/provisioner/naming.go +++ b/internal/service/cluster/provisioner/naming.go @@ -10,6 +10,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/service/cluster" "github.com/photoprism/photoprism/pkg/dsn" + "github.com/photoprism/photoprism/pkg/enum" "github.com/photoprism/photoprism/pkg/rnd" ) @@ -64,9 +65,9 @@ func GenerateCredentials(conf *config.Config, nodeUUID, nodeName string) (dbName func BuildDSN(driver, host string, port int, user, pass, name string) string { d := strings.ToLower(driver) switch d { - case dsn.DriverMySQL, dsn.DriverMariaDB: + case enum.MySQL, enum.MariaDB: return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", - user, pass, host, port, name, dsn.Params[dsn.DriverMySQL], + user, pass, host, port, name, dsn.Params[enum.MySQL], ) default: log.Warnf("provisioner: unsupported driver %q, falling back to mysql DSN format", driver) diff --git a/internal/service/cluster/provisioner/naming_test.go b/internal/service/cluster/provisioner/naming_test.go index 7d18c5291..760fbde66 100644 --- a/internal/service/cluster/provisioner/naming_test.go +++ b/internal/service/cluster/provisioner/naming_test.go @@ -9,6 +9,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/service/cluster" + "github.com/photoprism/photoprism/pkg/enum" ) func TestGenerateCredentials_StabilityAndBudgets(t *testing.T) { @@ -103,7 +104,7 @@ func TestEnsureCredentials_SqliteRejected(t *testing.T) { ctx := context.Background() c := config.NewConfig(config.CliTestContext()) origDriver := DatabaseDriver - DatabaseDriver = config.SQLite3 + DatabaseDriver = enum.SQLite3 t.Cleanup(func() { DatabaseDriver = origDriver }) _, _, err := EnsureCredentials(ctx, c, "11111111-1111-4111-8111-111111111111", "pp-node-01", false) diff --git a/pkg/clean/sql.go b/pkg/clean/sql.go index ba41876a6..1a88f3fe2 100644 --- a/pkg/clean/sql.go +++ b/pkg/clean/sql.go @@ -1,24 +1,35 @@ package clean -// SqlSpecial checks if the byte must be escaped/omitted in SQL. -func SqlSpecial(b byte) (special bool, omit bool) { +import "github.com/photoprism/photoprism/pkg/enum" + +// SQLSpecial checks if the byte must be escaped/omitted in SQL. +func SQLSpecial(b byte, dialect string) (special bool, omit bool) { if b < 32 { return true, true } - switch b { - case '"', '\'', '\\': - return true, false - default: - return false, false + if dialect == enum.MySQL { + switch b { + case '\'', '\\': + return true, false + default: + return false, false + } + } else { + switch b { + case '\'': + return true, false + default: + return false, false + } } } -// SqlString escapes a string for use in an SQL query. -func SqlString(s string) string { +// SQLString escapes a string for use in an SQL query. +func SQLString(s string, dialect string) string { var i int for i = 0; i < len(s); i++ { - if found, _ := SqlSpecial(s[i]); found { + if found, _ := SQLSpecial(s[i], dialect); found { break } } @@ -35,7 +46,7 @@ func SqlString(s string) string { j := i for ; i < len(s); i++ { - if special, omit := SqlSpecial(s[i]); omit { + if special, omit := SQLSpecial(s[i], dialect); omit { // Omit control characters. continue } else if special { diff --git a/pkg/clean/sql_test.go b/pkg/clean/sql_test.go index 5da499d28..dbbb8ff5e 100644 --- a/pkg/clean/sql_test.go +++ b/pkg/clean/sql_test.go @@ -4,81 +4,210 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/pkg/enum" ) -func TestSqlSpecial(t *testing.T) { - t.Run("Special", func(t *testing.T) { - if s, o := SqlSpecial(1); !s { +func TestSQLSpecial(t *testing.T) { + t.Run("Special MySQL", func(t *testing.T) { + if s, o := SQLSpecial(1, enum.MySQL); !s { t.Error("char is special") } else if !o { t.Error("\" must be omitted") } - if s, o := SqlSpecial(31); !s { + if s, o := SQLSpecial(31, enum.MySQL); !s { t.Error("char is special") } else if !o { t.Error("\" must be omitted") } - if s, o := SqlSpecial('\\'); !s { + if s, o := SQLSpecial('\\', enum.MySQL); !s { t.Error("\\ is special") } else if o { t.Error("\\ must not be omitted") } - if s, o := SqlSpecial('\''); !s { + if s, o := SQLSpecial('\'', enum.MySQL); !s { t.Error("' is special") } else if o { t.Error("' must not be omitted") } + }) + t.Run("Special SQLite", func(t *testing.T) { + if s, o := SQLSpecial(1, enum.SQLite3); !s { + t.Error("char is special") + } else if !o { + t.Error("\" must be omitted") + } - if s, o := SqlSpecial('"'); !s { - t.Error("\" is special") + if s, o := SQLSpecial(31, enum.SQLite3); !s { + t.Error("char is special") + } else if !o { + t.Error("\" must be omitted") + } + + if s, o := SQLSpecial('\'', enum.SQLite3); !s { + t.Error("' is special") } else if o { - t.Error("\" must not be omitted") + t.Error("' must not be omitted") } }) - t.Run("NotSpecial", func(t *testing.T) { - if s, o := SqlSpecial(32); s { + t.Run("Special Postgres", func(t *testing.T) { + if s, o := SQLSpecial(1, enum.Postgres); !s { + t.Error("char is special") + } else if !o { + t.Error("\" must be omitted") + } + + if s, o := SQLSpecial(31, enum.Postgres); !s { + t.Error("char is special") + } else if !o { + t.Error("\" must be omitted") + } + + if s, o := SQLSpecial('\'', enum.Postgres); !s { + t.Error("' is special") + } else if o { + t.Error("' must not be omitted") + } + }) + + t.Run("NotSpecial MySQL", func(t *testing.T) { + if s, o := SQLSpecial(32, enum.MySQL); s { t.Error("space is not special") } else if o { t.Error("space must not be omitted") } - if s, o := SqlSpecial('A'); s { + if s, o := SQLSpecial('A', enum.MySQL); s { t.Error("A is not special") } else if o { t.Error("A must not be omitted") } - if s, o := SqlSpecial('a'); s { + if s, o := SQLSpecial('a', enum.MySQL); s { t.Error("a is not special") } else if o { t.Error("a must not be omitted") } - if s, o := SqlSpecial('_'); s { + if s, o := SQLSpecial('_', enum.MySQL); s { t.Error("_ is not special") } else if o { t.Error("_ must not be omitted") } + + if s, o := SQLSpecial('"', enum.MySQL); s { + t.Error("\" is not special") + } else if o { + t.Error("\" must not be omitted") + } + }) + t.Run("NotSpecial SQLite", func(t *testing.T) { + if s, o := SQLSpecial(32, enum.SQLite3); s { + t.Error("space is not special") + } else if o { + t.Error("space must not be omitted") + } + + if s, o := SQLSpecial('A', enum.SQLite3); s { + t.Error("A is not special") + } else if o { + t.Error("A must not be omitted") + } + + if s, o := SQLSpecial('a', enum.SQLite3); s { + t.Error("a is not special") + } else if o { + t.Error("a must not be omitted") + } + + if s, o := SQLSpecial('_', enum.SQLite3); s { + t.Error("_ is not special") + } else if o { + t.Error("_ must not be omitted") + } + + if s, o := SQLSpecial('"', enum.SQLite3); s { + t.Error("\" is not special") + } else if o { + t.Error("\" must not be omitted") + } + + if s, o := SQLSpecial('\\', enum.SQLite3); s { + t.Error("\\ is not special") + } else if o { + t.Error("\\ must not be omitted") + } + }) + t.Run("NotSpecial Postgres", func(t *testing.T) { + if s, o := SQLSpecial(32, enum.Postgres); s { + t.Error("space is not special") + } else if o { + t.Error("space must not be omitted") + } + + if s, o := SQLSpecial('A', enum.Postgres); s { + t.Error("A is not special") + } else if o { + t.Error("A must not be omitted") + } + + if s, o := SQLSpecial('a', enum.Postgres); s { + t.Error("a is not special") + } else if o { + t.Error("a must not be omitted") + } + + if s, o := SQLSpecial('_', enum.Postgres); s { + t.Error("_ is not special") + } else if o { + t.Error("_ must not be omitted") + } + + if s, o := SQLSpecial('"', enum.Postgres); s { + t.Error("\" is not special") + } else if o { + t.Error("\" must not be omitted") + } + + if s, o := SQLSpecial('\\', enum.Postgres); s { + t.Error("\\ is not special") + } else if o { + t.Error("\\ must not be omitted") + } }) } func TestSqlString(t *testing.T) { t.Run("Empty", func(t *testing.T) { - assert.Equal(t, "", SqlString("")) + assert.Equal(t, "", SQLString("", enum.MySQL)) + assert.Equal(t, "", SQLString("", enum.SQLite3)) + assert.Equal(t, "", SQLString("", enum.Postgres)) }) t.Run("Special", func(t *testing.T) { - s := "' \" \t \n %_''" - exp := "'' \"\" %_''''" - result := SqlString(s) + s := "' \" \t \n %_''\\" + exp := "'' \" %_''''\\\\" + result := SQLString(s, enum.MySQL) + t.Logf("String..: %s", s) + t.Logf("Expected: %s", exp) + t.Logf("Result..: %s", result) + assert.Equal(t, exp, result) + exp = "'' \" %_''''\\" + result = SQLString(s, enum.SQLite3) + t.Logf("String..: %s", s) + t.Logf("Expected: %s", exp) + t.Logf("Result..: %s", result) + assert.Equal(t, exp, result) + exp = "'' \" %_''''\\" + result = SQLString(s, enum.Postgres) t.Logf("String..: %s", s) t.Logf("Expected: %s", exp) t.Logf("Result..: %s", result) assert.Equal(t, exp, result) }) t.Run("Alnum", func(t *testing.T) { - assert.Equal(t, "123ABCabc", SqlString("123ABCabc")) + assert.Equal(t, "123ABCabc", SQLString("123ABCabc", enum.MySQL)) }) } diff --git a/pkg/dsn/driver.go b/pkg/dsn/driver.go index 70c0bedac..8c4f20540 100644 --- a/pkg/dsn/driver.go +++ b/pkg/dsn/driver.go @@ -1,23 +1,11 @@ package dsn -// SQL database drivers. -const ( - DriverMySQL = "mysql" - DriverMariaDB = "mariadb" - DriverPostgres = "postgres" - DriverSQLite3 = "sqlite3" -) - -// SQLite default DSNs. -const ( - SQLiteTestDB = ".test.db" - SQLiteMemory = ":memory:" -) +import "github.com/photoprism/photoprism/pkg/enum" // Params maps required DSN parameters by driver type. var Params = Values{ - DriverMySQL: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true", - DriverMariaDB: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true", - DriverPostgres: "sslmode=disable TimeZone=UTC", - DriverSQLite3: "_busy_timeout=5000", + enum.MySQL: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true", + enum.MariaDB: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true", + enum.Postgres: "sslmode=disable TimeZone=UTC", + enum.SQLite3: "_busy_timeout=5000", } diff --git a/pkg/dsn/dsn.go b/pkg/dsn/dsn.go index fe4aa9271..740105d99 100644 --- a/pkg/dsn/dsn.go +++ b/pkg/dsn/dsn.go @@ -31,6 +31,8 @@ import ( "strconv" "strings" "unicode" + + "github.com/photoprism/photoprism/pkg/enum" ) // dsnPattern is a regular expression matching a database DSN string. @@ -75,7 +77,7 @@ func (d *DSN) MaskPassword() (s string) { } // Mask password in PostgreSQL-style DSN. - if d.Driver == DriverPostgres || strings.Contains(s, "password=") { + if d.Driver == enum.Postgres || strings.Contains(s, "password=") { return dsnPostgresPasswordPattern.ReplaceAllStringFunc(s, func(segment string) string { matches := dsnPostgresPasswordPattern.FindStringSubmatch(segment) if len(matches) != 3 { @@ -107,7 +109,7 @@ func (d *DSN) MaskPassword() (s string) { // Host the database server host. func (d *DSN) Host() string { - if d.Driver == DriverSQLite3 { + if d.Driver == enum.SQLite3 { return "" } @@ -117,16 +119,17 @@ func (d *DSN) Host() string { // Port the database server port. func (d *DSN) Port() int { - if d.Driver == DriverSQLite3 { + switch d.Driver { + case enum.SQLite3: return 0 } defaultPort := 0 switch d.Driver { - case DriverMySQL, DriverMariaDB: + case enum.MySQL, enum.MariaDB: defaultPort = 3306 - case DriverPostgres: + case enum.Postgres: defaultPort = 5432 } @@ -249,7 +252,7 @@ func (d *DSN) parsePostgres() bool { } } - d.Driver = DriverPostgres + d.Driver = enum.Postgres d.User = values["user"] d.Password = values["password"] d.Name = name @@ -371,13 +374,13 @@ func (d *DSN) detectDriver() { switch driver { case "postgres", "postgresql": - d.Driver = DriverPostgres + d.Driver = enum.Postgres return case "mysql", "mariadb": - d.Driver = DriverMySQL + d.Driver = enum.MySQL return case "sqlite", "sqlite3", "file": - d.Driver = DriverSQLite3 + d.Driver = enum.SQLite3 return } @@ -389,26 +392,26 @@ func (d *DSN) detectDriver() { lower := strings.ToLower(d.DSN) if strings.Contains(lower, "postgres://") || strings.Contains(lower, "postgresql://") { - d.Driver = DriverPostgres + d.Driver = enum.Postgres return } if d.Net == "tcp" || d.Net == "unix" || strings.Contains(lower, "@tcp(") || strings.Contains(lower, "@unix(") { - d.Driver = DriverMySQL + d.Driver = enum.MySQL return } if strings.HasPrefix(lower, "file:") || strings.HasSuffix(lower, ".db") || strings.HasSuffix(strings.ToLower(d.Name), ".db") { - d.Driver = DriverSQLite3 + d.Driver = enum.SQLite3 return } if strings.Contains(lower, " host=") && strings.Contains(lower, " dbname=") { - d.Driver = DriverPostgres + d.Driver = enum.Postgres return } if d.Server != "" && (strings.Contains(d.Server, ":") || d.Net != "") && d.Driver == "" { - d.Driver = DriverMySQL + d.Driver = enum.MySQL } } diff --git a/pkg/dsn/dsn_test.go b/pkg/dsn/dsn_test.go index 52af01054..ae8fa0af6 100644 --- a/pkg/dsn/dsn_test.go +++ b/pkg/dsn/dsn_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/pkg/enum" ) func TestDSN_HostAndPort(t *testing.T) { @@ -95,7 +97,7 @@ func TestDSN_ParsePostgres(t *testing.T) { in: "user=alice password=s3cr3t dbname=app", want: DSN{ DSN: "user=alice password=s3cr3t dbname=app", - Driver: DriverPostgres, + Driver: enum.Postgres, User: "alice", Password: "s3cr3t", Name: "app", @@ -107,7 +109,7 @@ func TestDSN_ParsePostgres(t *testing.T) { in: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require", want: DSN{ DSN: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require", - Driver: DriverPostgres, + Driver: enum.Postgres, User: "alice", Password: "s3cr3t", Server: "db.internal:5432", @@ -121,7 +123,7 @@ func TestDSN_ParsePostgres(t *testing.T) { in: `user="alice" password="s ec ret" dbname="app" host=db.internal`, want: DSN{ DSN: `user="alice" password="s ec ret" dbname="app" host=db.internal`, - Driver: DriverPostgres, + Driver: enum.Postgres, User: "alice", Password: "s ec ret", Server: "db.internal", diff --git a/pkg/dsn/parse_test.go b/pkg/dsn/parse_test.go index 26b28feef..fd634d57f 100644 --- a/pkg/dsn/parse_test.go +++ b/pkg/dsn/parse_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/pkg/enum" ) func TestParse(t *testing.T) { @@ -17,7 +19,7 @@ func TestParse(t *testing.T) { in: "user:secret@tcp(localhost:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true", want: DSN{ DSN: "user:secret@tcp(localhost:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true", - Driver: DriverMySQL, + Driver: enum.MySQL, User: "user", Password: "secret", Net: "tcp", @@ -31,7 +33,7 @@ func TestParse(t *testing.T) { in: "mysql://user:secret@localhost:3306/photoprism?parseTime=true", want: DSN{ DSN: "mysql://user:secret@localhost:3306/photoprism?parseTime=true", - Driver: DriverMySQL, + Driver: enum.MySQL, User: "user", Password: "secret", Server: "localhost:3306", @@ -44,7 +46,7 @@ func TestParse(t *testing.T) { in: "user:secret@unix(/var/run/mysql.sock)/photoprism", want: DSN{ DSN: "user:secret@unix(/var/run/mysql.sock)/photoprism", - Driver: DriverMySQL, + Driver: enum.MySQL, User: "user", Password: "secret", Net: "unix", @@ -57,7 +59,7 @@ func TestParse(t *testing.T) { in: "file:/data/index.db?_busy_timeout=5000", want: DSN{ DSN: "file:/data/index.db?_busy_timeout=5000", - Driver: DriverSQLite3, + Driver: enum.SQLite3, Server: "file:/data", Name: "index.db", Params: "_busy_timeout=5000", @@ -68,7 +70,7 @@ func TestParse(t *testing.T) { in: "/index.db?_busy_timeout=5000", want: DSN{ DSN: "/index.db?_busy_timeout=5000", - Driver: DriverSQLite3, + Driver: enum.SQLite3, Server: "", Name: "index.db", Params: "_busy_timeout=5000", @@ -79,7 +81,7 @@ func TestParse(t *testing.T) { in: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require", want: DSN{ DSN: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require", - Driver: DriverPostgres, + Driver: enum.Postgres, User: "alice", Password: "s3cr3t", Server: "db.internal:5432", diff --git a/pkg/enum/enum.go b/pkg/enum/enum.go index d0b20a92c..1899cec61 100644 --- a/pkg/enum/enum.go +++ b/pkg/enum/enum.go @@ -24,3 +24,8 @@ Additional information can be found in our Developer Guide: */ package enum + +// Automatically choose the appropriate "thing" +const ( + Auto = "auto" +) diff --git a/pkg/enum/sql.go b/pkg/enum/sql.go new file mode 100644 index 000000000..8b261bbad --- /dev/null +++ b/pkg/enum/sql.go @@ -0,0 +1,19 @@ +package enum + +// Supported database(s) +const ( + MySQL = "mysql" + MariaDB = "mariadb" + SQLite3 = "sqlite3" +) + +// Future database(s) +const ( + Postgres = "postgres" +) + +// Test database(s) +const ( + SQLiteTestDB = ".test.db" + SQLiteMemoryDSN = ":memory:?cache=shared" +)