diff --git a/internal/config/config.go b/internal/config/config.go index eabfa7de0..6d04948ac 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -170,6 +170,8 @@ func (c *Config) Propagate() { // Init creates directories, parses additional config files, opens a database connection and initializes dependencies. func (c *Config) Init() error { + start := time.Now() + if err := c.CreateDirectories(); err != nil { return err } @@ -215,7 +217,13 @@ func (c *Config) Init() error { c.Propagate() - return c.connectDb() + err := c.connectDb() + + if err == nil { + log.Debugf("config: successfully initialized [%s]", time.Since(start)) + } + + return err } // initStorage initializes storage directories with a random serial. @@ -571,7 +579,7 @@ func (c *Config) UpdateHub() { // initHub initializes PhotoPrism hub config. func (c *Config) initHub() { - c.hub = hub.NewConfig(c.Version(), c.HubConfigFile(), c.serial) + c.hub = hub.NewConfig(c.Version(), c.HubConfigFile(), c.serial, c.options.PartnerID) if err := c.hub.Load(); err == nil { // Do nothing. diff --git a/internal/config/flags.go b/internal/config/flags.go index 7c3dd0dc4..92ea9d0af 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -64,6 +64,12 @@ var GlobalFlags = []cli.Flag{ Usage: "enable experimental features", EnvVar: "PHOTOPRISM_EXPERIMENTAL", }, + cli.StringFlag{ + Name: "partner-id", + Hidden: true, + Usage: "hosting partner id", + EnvVar: "PHOTOPRISM_PARTNER_ID", + }, cli.StringFlag{ Name: "config-file, c", Usage: "load config options from `FILENAME`", diff --git a/internal/config/options.go b/internal/config/options.go index 11a7c9943..01c99a36d 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -26,15 +26,16 @@ const ( // Options provides a struct in which application configuration is stored. // Application code must use functions to get config options, for two reasons: // -// 1. Some options are computed and we don't want to leak implementation details (aims at reducing refactoring overhead). -// -// 2. Paths might actually be dynamic later (if we build a multi-user version). +// 1. We do not want to leak implementation details so refactoring overhead is kept low +// 2. Some config values are dynamically generated +// 3. Paths may become dynamic too at a later time // // See https://github.com/photoprism/photoprism/issues/50#issuecomment-433856358 type Options struct { Name string `json:"-"` Version string `json:"-"` Copyright string `json:"-"` + PartnerID string `yaml:"-" json:"-" flag:"partner-id"` AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"` LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"` Debug bool `yaml:"Debug" json:"Debug" flag:"debug"` diff --git a/internal/entity/db_fixtures.go b/internal/entity/db_fixtures.go index 860966d62..4a36d771b 100644 --- a/internal/entity/db_fixtures.go +++ b/internal/entity/db_fixtures.go @@ -1,5 +1,9 @@ package entity +import ( + "time" +) + // CreateDefaultFixtures inserts default fixtures for test and production. func CreateDefaultFixtures() { CreateUnknownAddress() @@ -11,8 +15,10 @@ func CreateDefaultFixtures() { CreateUnknownLens() } -// ResetTestFixtures re-creates registered database tables and inserts test fixtures. +// ResetTestFixtures recreates database tables and test fixtures. func ResetTestFixtures() { + start := time.Now() + Entities.Migrate(Db(), false) Entities.WaitForMigration(Db()) Entities.Truncate(Db()) @@ -20,4 +26,6 @@ func ResetTestFixtures() { CreateDefaultFixtures() CreateTestFixtures() + + log.Debugf("entity: recreated test fixtures [%s]", time.Since(start)) } diff --git a/internal/entity/db_migrate.go b/internal/entity/db_migrate.go index b40b5f262..80a228b4a 100644 --- a/internal/entity/db_migrate.go +++ b/internal/entity/db_migrate.go @@ -1,7 +1,13 @@ package entity +import ( + "time" +) + // MigrateDb creates database tables and inserts default fixtures as needed. func MigrateDb(dropDeprecated, runFailed bool) { + start := time.Now() + if dropDeprecated { DeprecatedTables.Drop(Db()) } @@ -10,6 +16,8 @@ func MigrateDb(dropDeprecated, runFailed bool) { Entities.WaitForMigration(Db()) CreateDefaultFixtures() + + log.Debugf("entity: successfully initialized [%s]", time.Since(start)) } // InitTestDb connects to and completely initializes the test database incl fixtures. diff --git a/internal/hub/config.go b/internal/hub/config.go index 152f26a1a..5b9ab491c 100644 --- a/internal/hub/config.go +++ b/internal/hub/config.go @@ -24,25 +24,27 @@ import ( // Config represents backend api credentials for maps & geodata. type Config struct { - Key string `json:"key" yaml:"Key"` - Secret string `json:"secret" yaml:"Secret"` - Session string `json:"session" yaml:"Session"` - Status string `json:"status" yaml:"Status"` - Version string `json:"version" yaml:"Version"` - Serial string `json:"serial" yaml:"Serial"` - FileName string `json:"-" yaml:"-"` + Key string `json:"key" yaml:"Key"` + Secret string `json:"secret" yaml:"Secret"` + Session string `json:"session" yaml:"Session"` + Status string `json:"status" yaml:"Status"` + Version string `json:"version" yaml:"Version"` + Serial string `json:"serial" yaml:"Serial"` + FileName string `json:"-" yaml:"-"` + PartnerID string `json:"-" yaml:"-"` } // NewConfig creates a new backend api credentials instance. -func NewConfig(version, fileName, serial string) *Config { +func NewConfig(version, fileName, serial, partner string) *Config { return &Config{ - Key: "", - Secret: "", - Session: "", - Status: "", - Version: version, - Serial: serial, - FileName: fileName, + Key: "", + Secret: "", + Session: "", + Status: "", + Version: version, + Serial: serial, + FileName: fileName, + PartnerID: partner, } } @@ -140,12 +142,12 @@ func (c *Config) Refresh() (err error) { if c.Key != "" { url = fmt.Sprintf(ServiceURL+"/%s", c.Key) method = http.MethodPut - log.Debugf("getting updated api key for maps & places from %s", ApiHost()) + log.Debugf("config: requesting updated api key for maps and places") } else { - log.Debugf("requesting api key for maps & places from %s", ApiHost()) + log.Debugf("config: requesting new api key for maps and places") } - if j, err := json.Marshal(NewRequest(c.Version, c.Serial)); err != nil { + if j, err := json.Marshal(NewRequest(c.Version, c.Serial, c.PartnerID)); err != nil { return err } else if req, err = http.NewRequest(method, url, bytes.NewReader(j)); err != nil { return err @@ -166,7 +168,7 @@ func (c *Config) Refresh() (err error) { if err != nil { return err } else if r.StatusCode >= 400 { - err = fmt.Errorf("getting api key from %s failed (error %d)", ApiHost(), r.StatusCode) + err = fmt.Errorf("fetching api key from %s failed (error %d)", ApiHost(), r.StatusCode) return err } diff --git a/internal/hub/config_test.go b/internal/hub/config_test.go index 4269be6f3..59f43d451 100644 --- a/internal/hub/config_test.go +++ b/internal/hub/config_test.go @@ -8,7 +8,7 @@ import ( func TestConfig_MapKey(t *testing.T) { t.Run("success", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/new.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/new.yml", "zqkunt22r0bewti9", "test") assert.Equal(t, "", c.MapKey()) }) } diff --git a/internal/hub/feedback.go b/internal/hub/feedback.go index dcfdef73d..21b3677a7 100644 --- a/internal/hub/feedback.go +++ b/internal/hub/feedback.go @@ -22,6 +22,7 @@ type Feedback struct { UserEmail string `json:"UserEmail"` UserAgent string `json:"UserAgent"` ApiKey string `json:"ApiKey"` + PartnerID string `json:"PartnerID"` ClientVersion string `json:"ClientVersion"` ClientSerial string `json:"ClientSerial"` ClientOS string `json:"ClientOS"` @@ -30,8 +31,9 @@ type Feedback struct { } // NewFeedback creates a new hub feedback instance. -func NewFeedback(version, serial string) *Feedback { +func NewFeedback(version, serial, partner string) *Feedback { return &Feedback{ + PartnerID: partner, ClientVersion: version, ClientSerial: serial, ClientOS: runtime.GOOS, @@ -41,7 +43,7 @@ func NewFeedback(version, serial string) *Feedback { } func (c *Config) SendFeedback(f form.Feedback) (err error) { - feedback := NewFeedback(c.Version, c.Serial) + feedback := NewFeedback(c.Version, c.Serial, c.PartnerID) feedback.Category = f.Category feedback.Subject = txt.Shorten(f.Message, 50, "...") feedback.Message = f.Message diff --git a/internal/hub/feedback_test.go b/internal/hub/feedback_test.go index 6451dd63e..4901e9780 100644 --- a/internal/hub/feedback_test.go +++ b/internal/hub/feedback_test.go @@ -9,7 +9,7 @@ import ( func TestNewFeedback(t *testing.T) { t.Run("success", func(t *testing.T) { - feedback := NewFeedback("xxx", "zqkunt22r0bewti9") + feedback := NewFeedback("xxx", "zqkunt22r0bewti9", "test") assert.Equal(t, "xxx", feedback.ClientVersion) assert.Equal(t, "zqkunt22r0bewti9", feedback.ClientSerial) }) @@ -17,7 +17,7 @@ func TestNewFeedback(t *testing.T) { func TestSendFeedback(t *testing.T) { t.Run("success", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/new.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/new.yml", "zqkunt22r0bewti9", "test") feedback := Feedback{ Category: "Bug Report", diff --git a/internal/hub/hub_test.go b/internal/hub/hub_test.go index 24e9a09f4..480b88e55 100644 --- a/internal/hub/hub_test.go +++ b/internal/hub/hub_test.go @@ -49,13 +49,13 @@ func Token(size uint) string { } func TestNewConfig(t *testing.T) { - c := NewConfig("0.0.0", "testdata/new.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/new.yml", "zqkunt22r0bewti9", "test") assert.IsType(t, &Config{}, c) } func TestNewRequest(t *testing.T) { - r := NewRequest("0.0.0", "zqkunt22r0bewti9") + r := NewRequest("0.0.0", "zqkunt22r0bewti9", "test") assert.IsType(t, &Request{}, r) @@ -72,7 +72,7 @@ func TestConfig_Refresh(t *testing.T) { t.Run("success", func(t *testing.T) { fileName := fmt.Sprintf("testdata/hub.%s.yml", Token(8)) - c := NewConfig("0.0.0", fileName, "zqkunt22r0bewti9") + c := NewConfig("0.0.0", fileName, "zqkunt22r0bewti9", "test") if err := c.Refresh(); err != nil { t.Fatal(err) @@ -122,7 +122,7 @@ func TestConfig_Refresh(t *testing.T) { func TestConfig_DecodeSession(t *testing.T) { t.Run("hub3.yml", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/hub3.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/hub3.yml", "zqkunt22r0bewti9", "test") err := c.Load() @@ -138,7 +138,7 @@ func TestConfig_DecodeSession(t *testing.T) { func TestConfig_Load(t *testing.T) { t.Run("hub1.yml", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/hub1.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/hub1.yml", "zqkunt22r0bewti9", "test") if err := c.Load(); err != nil { t.Logf(err.Error()) @@ -151,7 +151,7 @@ func TestConfig_Load(t *testing.T) { assert.Equal(t, "0.0.0", c.Version) }) t.Run("hub2.yml", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/hub2.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/hub2.yml", "zqkunt22r0bewti9", "test") if err := c.Load(); err != nil { t.Logf(err.Error()) @@ -164,7 +164,7 @@ func TestConfig_Load(t *testing.T) { assert.Equal(t, "200925-f8e2b580-Darwin-i386-DEBUG", c.Version) }) t.Run("not existing filename", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/hub_xxx.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/hub_xxx.yml", "zqkunt22r0bewti9", "test") if err := c.Load(); err == nil { t.Fatal("file should not exist") @@ -180,7 +180,7 @@ func TestConfig_Save(t *testing.T) { t.Run("existing filename", func(t *testing.T) { assert.FileExists(t, "testdata/hub1.yml") - c := NewConfig("0.0.0", "testdata/hub1.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/hub1.yml", "zqkunt22r0bewti9", "test") if err := c.Load(); err != nil { t.Logf(err.Error()) @@ -219,7 +219,7 @@ func TestConfig_Save(t *testing.T) { assert.Equal(t, "0.0.0", c.Version) }) t.Run("not existing filename", func(t *testing.T) { - c := NewConfig("0.0.0", "testdata/hub_new.yml", "zqkunt22r0bewti9") + c := NewConfig("0.0.0", "testdata/hub_new.yml", "zqkunt22r0bewti9", "test") c.Key = "F60F5B25D59C397989E3CD374F81CDD7710A4FCA" c.Secret = "foo" c.Session = "bar" diff --git a/internal/hub/request.go b/internal/hub/request.go index 104c9cb44..5f72aaaed 100644 --- a/internal/hub/request.go +++ b/internal/hub/request.go @@ -9,6 +9,7 @@ var ServiceURL = "https://hub.photoprism.app/v1/hello" // Request represents basic environment specs for debugging. type Request struct { + PartnerID string `json:"PartnerID"` ClientVersion string `json:"ClientVersion"` ClientSerial string `json:"ClientSerial"` ClientOS string `json:"ClientOS"` @@ -17,8 +18,9 @@ type Request struct { } // NewRequest creates a new backend key request instance. -func NewRequest(version, serial string) *Request { +func NewRequest(version, serial, partner string) *Request { return &Request{ + PartnerID: partner, ClientVersion: version, ClientSerial: serial, ClientOS: runtime.GOOS, diff --git a/internal/migrate/migrations.go b/internal/migrate/migrations.go index c402a1298..b02506389 100644 --- a/internal/migrate/migrations.go +++ b/internal/migrate/migrations.go @@ -17,9 +17,8 @@ type MigrationMap map[string]Migration // Existing finds and returns previously executed database schema migrations. func Existing(db *gorm.DB) MigrationMap { result := make(MigrationMap) - dialect := db.Dialect().GetName() - stmt := db.Model(Migration{}).Where("dialect = ?", dialect) + stmt := db.Model(Migration{}) stmt = stmt.Select("id, dialect, error, source, started_at, finished_at") rows, err := stmt.Rows() @@ -52,7 +51,11 @@ func (m *Migrations) Start(db *gorm.DB, runFailed bool) { // Find previously executed migrations. executed := Existing(db) - log.Debugf("migrate: found %s", english.Plural(len(executed), "previous migration", "previous migrations")) + if prev := len(executed); prev == 0 { + log.Infof("migrate: found no previous migrations") + } else { + log.Debugf("migrate: found %s", english.Plural(len(executed), "previous migration", "previous migrations")) + } for _, migration := range *m { start := time.Now() diff --git a/internal/server/server.go b/internal/server/server.go index 1628f0b91..d1f451454 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "time" "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" @@ -21,6 +22,8 @@ func Start(ctx context.Context, conf *config.Config) { } }() + start := time.Now() + // Set HTTP server mode. if conf.HttpMode() != "" { gin.SetMode(conf.HttpMode()) @@ -76,6 +79,8 @@ func Start(ctx context.Context, conf *config.Config) { Handler: router, } + log.Debugf("http: successfully initialized [%s]", time.Since(start)) + // Start HTTP server. go func() { log.Infof("http: starting web server at %s", server.Addr)