diff --git a/assets/server/templates/index.tmpl b/assets/server/templates/index.tmpl index 45a1d3035..75896dcb6 100644 --- a/assets/server/templates/index.tmpl +++ b/assets/server/templates/index.tmpl @@ -1,54 +1,65 @@ - - - + + + - PhotoPrism + {{ .name }} - - - - + + + + - - - - - + + + + + - - + + - - - + + + - + - +
-
-
+
+
+
+
+
+
+
+
+
- \ No newline at end of file + diff --git a/internal/api/api_test.go b/internal/api/api_test.go index ab1dd4b77..a4cb07c89 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -5,19 +5,18 @@ import ( "net/http/httptest" "github.com/gin-gonic/gin" - "github.com/photoprism/photoprism/internal/photoprism" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" ) // API test helper -func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf photoprism.Config) { - conf = test.NewConfig() +func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, ctx *context.Context) { + ctx = context.TestContext() gin.SetMode(gin.TestMode) app = gin.New() router = app.Group("/api/v1") - return app, router, conf + return app, router, ctx } // See https://medium.com/@craigchilds94/testing-gin-json-responses-1f258ce3b0b1 diff --git a/internal/api/photos.go b/internal/api/photos.go index 2bb65338a..b1c84e163 100644 --- a/internal/api/photos.go +++ b/internal/api/photos.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" + "github.com/photoprism/photoprism/internal/context" log "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" @@ -26,10 +27,10 @@ import ( // before: date Find photos taken before (format: "2006-01-02") // after: date Find photos taken after (format: "2006-01-02") // favorites: bool Find favorites only -func GetPhotos(router *gin.RouterGroup, conf photoprism.Config) { +func GetPhotos(router *gin.RouterGroup, ctx *context.Context) { router.GET("/photos", func(c *gin.Context) { var form forms.PhotoSearchForm - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + search := photoprism.NewSearch(ctx.OriginalsPath(), ctx.Db()) err := c.MustBindWith(&form, binding.Form) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -52,12 +53,12 @@ func GetPhotos(router *gin.RouterGroup, conf photoprism.Config) { // // Parameters: // id: int Photo ID as returned by the API -func LikePhoto(router *gin.RouterGroup, conf photoprism.Config) { +func LikePhoto(router *gin.RouterGroup, ctx *context.Context) { router.POST("/photos/:id/like", func(c *gin.Context) { - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + search := photoprism.NewSearch(ctx.OriginalsPath(), ctx.Db()) photoID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - log.Printf("could not find image for id: %s", err.Error()) + log.Errorf("could not find image for id: %s", err.Error()) c.Data(http.StatusNotFound, "image", []byte("")) return } @@ -70,7 +71,7 @@ func LikePhoto(router *gin.RouterGroup, conf photoprism.Config) { } photo.PhotoFavorite = true - conf.Db().Save(&photo) + ctx.Db().Save(&photo) c.JSON(http.StatusOK, http.Response{}) }) } @@ -79,12 +80,12 @@ func LikePhoto(router *gin.RouterGroup, conf photoprism.Config) { // // Parameters: // id: int Photo ID as returned by the API -func DislikePhoto(router *gin.RouterGroup, conf photoprism.Config) { +func DislikePhoto(router *gin.RouterGroup, ctx *context.Context) { router.DELETE("/photos/:id/like", func(c *gin.Context) { - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + search := photoprism.NewSearch(ctx.OriginalsPath(), ctx.Db()) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - log.Printf("could not find image for id: %s", err.Error()) + log.Errorf("could not find image for id: %s", err.Error()) c.Data(http.StatusNotFound, "image", []byte("")) return } @@ -97,7 +98,7 @@ func DislikePhoto(router *gin.RouterGroup, conf photoprism.Config) { } photo.PhotoFavorite = false - conf.Db().Save(&photo) + ctx.Db().Save(&photo) c.JSON(http.StatusOK, http.Response{}) }) } diff --git a/internal/api/photos_test.go b/internal/api/photos_test.go index 33e24b358..410d32920 100644 --- a/internal/api/photos_test.go +++ b/internal/api/photos_test.go @@ -8,9 +8,9 @@ import ( ) func TestGetPhotos(t *testing.T) { - app, router, conf := NewApiTest() + app, router, ctx := NewApiTest() - GetPhotos(router, conf) + GetPhotos(router, ctx) result := PerformRequest(app, "GET", "/api/v1/photos?count=10") @@ -18,9 +18,9 @@ func TestGetPhotos(t *testing.T) { } func TestLikePhoto(t *testing.T) { - app, router, conf := NewApiTest() + app, router, ctx := NewApiTest() - LikePhoto(router, conf) + LikePhoto(router, ctx) result := PerformRequest(app, "POST", "/api/v1/photos/1/like") @@ -31,9 +31,9 @@ func TestLikePhoto(t *testing.T) { } func TestDislikePhoto(t *testing.T) { - app, router, conf := NewApiTest() + app, router, ctx := NewApiTest() - DislikePhoto(router, conf) + DislikePhoto(router, ctx) result := PerformRequest(app, "DELETE", "/api/v1/photos/1/like") diff --git a/internal/api/thumbnails.go b/internal/api/thumbnails.go index 60bda0d60..411a64c77 100644 --- a/internal/api/thumbnails.go +++ b/internal/api/thumbnails.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" + "github.com/photoprism/photoprism/internal/context" log "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" @@ -22,18 +23,18 @@ var photoIconSvg = []byte(` // type: string Format, either "fit" or "square" // size: int Size in pixels // hash: string The file hash as returned by the search API -func GetThumbnail(router *gin.RouterGroup, conf photoprism.Config) { +func GetThumbnail(router *gin.RouterGroup, ctx *context.Context) { router.GET("/thumbnails/:type/:size/:hash", func(c *gin.Context) { fileHash := c.Param("hash") thumbnailType := c.Param("type") size, err := strconv.Atoi(c.Param("size")) if err != nil { - log.Printf("invalid size: %s", c.Param("size")) + log.Errorf("invalid size: %s", c.Param("size")) c.Data(400, "image/svg+xml", photoIconSvg) return } - search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + search := photoprism.NewSearch(ctx.OriginalsPath(), ctx.Db()) file, err := search.FindFileByHash(fileHash) if err != nil { @@ -41,36 +42,36 @@ func GetThumbnail(router *gin.RouterGroup, conf photoprism.Config) { return } - fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath(), file.FileName) + fileName := fmt.Sprintf("%s/%s", ctx.OriginalsPath(), file.FileName) mediaFile, err := photoprism.NewMediaFile(fileName) if err != nil { - log.Printf("could not find image for thumbnail: %s", err.Error()) + log.Errorf("could not find image for thumbnail: %s", err.Error()) c.Data(404, "image/svg+xml", photoIconSvg) // Set missing flag so that the file doesn't show up in search results anymore file.FileMissing = true - conf.Db().Save(&file) + ctx.Db().Save(&file) return } switch thumbnailType { case "fit": - if thumbnail, err := mediaFile.Thumbnail(conf.ThumbnailsPath(), size); err == nil { + if thumbnail, err := mediaFile.Thumbnail(ctx.ThumbnailsPath(), size); err == nil { c.File(thumbnail.Filename()) } else { - log.Printf("could not create thumbnail: %s", err.Error()) + log.Errorf("could not create thumbnail: %s", err.Error()) c.Data(400, "image/svg+xml", photoIconSvg) } case "square": - if thumbnail, err := mediaFile.SquareThumbnail(conf.ThumbnailsPath(), size); err == nil { + if thumbnail, err := mediaFile.SquareThumbnail(ctx.ThumbnailsPath(), size); err == nil { c.File(thumbnail.Filename()) } else { - log.Printf("could not create square thumbnail: %s", err.Error()) + log.Errorf("could not create square thumbnail: %s", err.Error()) c.Data(400, "image/svg+xml", photoIconSvg) } default: - log.Printf("unknown thumbnail type: %s", thumbnailType) + log.Errorf("unknown thumbnail type: %s", thumbnailType) c.Data(400, "image/svg+xml", photoIconSvg) } }) diff --git a/internal/commands/config.go b/internal/commands/config.go index 29cf2a9cb..335dd72f5 100644 --- a/internal/commands/config.go +++ b/internal/commands/config.go @@ -15,41 +15,41 @@ var ConfigCommand = cli.Command{ } func configAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) fmt.Printf("NAME VALUE\n") - fmt.Printf("debug %t\n", conf.Debug()) - fmt.Printf("log-level %s\n", conf.LogLevel()) - fmt.Printf("config-file %s\n", conf.ConfigFile()) - fmt.Printf("app-name %s\n", conf.AppName()) - fmt.Printf("app-version %s\n", conf.AppVersion()) - fmt.Printf("app-copyright %s\n", conf.AppCopyright()) + fmt.Printf("name %s\n", app.Name()) + fmt.Printf("version %s\n", app.Version()) + fmt.Printf("copyright %s\n", app.Copyright()) + fmt.Printf("debug %t\n", app.Debug()) + fmt.Printf("log-level %s\n", app.LogLevel()) + fmt.Printf("config-file %s\n", app.ConfigFile()) - fmt.Printf("database-driver %s\n", conf.DatabaseDriver()) - fmt.Printf("database-dsn %s\n", conf.DatabaseDsn()) + fmt.Printf("database-driver %s\n", app.DatabaseDriver()) + fmt.Printf("database-dsn %s\n", app.DatabaseDsn()) - fmt.Printf("http-host %s\n", conf.HttpServerHost()) - fmt.Printf("http-port %d\n", conf.HttpServerPort()) - fmt.Printf("http-mode %s\n", conf.HttpServerMode()) + fmt.Printf("http-host %s\n", app.HttpServerHost()) + fmt.Printf("http-port %d\n", app.HttpServerPort()) + fmt.Printf("http-mode %s\n", app.HttpServerMode()) - fmt.Printf("sql-host %s\n", conf.SqlServerHost()) - fmt.Printf("sql-port %d\n", conf.SqlServerPort()) - fmt.Printf("sql-password %s\n", conf.SqlServerPassword()) - fmt.Printf("sql-path %s\n", conf.SqlServerPath()) + fmt.Printf("sql-host %s\n", app.SqlServerHost()) + fmt.Printf("sql-port %d\n", app.SqlServerPort()) + fmt.Printf("sql-password %s\n", app.SqlServerPassword()) + fmt.Printf("sql-path %s\n", app.SqlServerPath()) - fmt.Printf("assets-path %s\n", conf.AssetsPath()) - fmt.Printf("originals-path %s\n", conf.OriginalsPath()) - fmt.Printf("import-path %s\n", conf.ImportPath()) - fmt.Printf("export-path %s\n", conf.ExportPath()) - fmt.Printf("cache-path %s\n", conf.CachePath()) - fmt.Printf("thumbnails-path %s\n", conf.ThumbnailsPath()) - fmt.Printf("tf-model-path %s\n", conf.TensorFlowModelPath()) - fmt.Printf("templates-path %s\n", conf.HttpTemplatesPath()) - fmt.Printf("favicons-path %s\n", conf.HttpFaviconsPath()) - fmt.Printf("public-path %s\n", conf.HttpPublicPath()) - fmt.Printf("public-build-path %s\n", conf.HttpPublicBuildPath()) + fmt.Printf("assets-path %s\n", app.AssetsPath()) + fmt.Printf("originals-path %s\n", app.OriginalsPath()) + fmt.Printf("import-path %s\n", app.ImportPath()) + fmt.Printf("export-path %s\n", app.ExportPath()) + fmt.Printf("cache-path %s\n", app.CachePath()) + fmt.Printf("thumbnails-path %s\n", app.ThumbnailsPath()) + fmt.Printf("tf-model-path %s\n", app.TensorFlowModelPath()) + fmt.Printf("templates-path %s\n", app.HttpTemplatesPath()) + fmt.Printf("favicons-path %s\n", app.HttpFaviconsPath()) + fmt.Printf("public-path %s\n", app.HttpPublicPath()) + fmt.Printf("public-build-path %s\n", app.HttpPublicBuildPath()) - fmt.Printf("darktable-cli %s\n", conf.DarktableCli()) + fmt.Printf("darktable-cli %s\n", app.DarktableCli()) return nil } diff --git a/internal/commands/config_test.go b/internal/commands/config_test.go index 6e5ac0e43..fdd5faf9b 100644 --- a/internal/commands/config_test.go +++ b/internal/commands/config_test.go @@ -3,16 +3,16 @@ package commands import ( "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestConfigCommand(t *testing.T) { var err error - ctx := test.CliContext() + ctx := context.CliTestContext() - output := test.Capture(func() { + output := context.CaptureOutput(func() { err = ConfigCommand.Run(ctx) }) diff --git a/internal/commands/convert.go b/internal/commands/convert.go index 76d7c41c9..30c44b5cb 100644 --- a/internal/commands/convert.go +++ b/internal/commands/convert.go @@ -15,17 +15,17 @@ var ConvertCommand = cli.Command{ } func convertAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - if err := conf.CreateDirectories(); err != nil { + if err := app.CreateDirectories(); err != nil { return err } - log.Infof("converting RAW images in %s to JPEG", conf.OriginalsPath()) + log.Infof("converting RAW images in %s to JPEG", app.OriginalsPath()) - converter := photoprism.NewConverter(conf.DarktableCli()) + converter := photoprism.NewConverter(app.DarktableCli()) - converter.ConvertAll(conf.OriginalsPath()) + converter.ConvertAll(app.OriginalsPath()) log.Infof("image conversion complete") diff --git a/internal/commands/export.go b/internal/commands/export.go index 4c24fab92..fa72a1ab9 100644 --- a/internal/commands/export.go +++ b/internal/commands/export.go @@ -40,9 +40,9 @@ var exportFlags = []cli.Flag{ } func exportAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - if err := conf.CreateDirectories(); err != nil { + if err := app.CreateDirectories(); err != nil { return err } @@ -70,13 +70,13 @@ func exportAction(ctx *cli.Context) error { } } - exportPath := fmt.Sprintf("%s/%s", conf.ExportPath(), name) + exportPath := fmt.Sprintf("%s/%s", app.ExportPath(), name) size := ctx.Int("size") - originals := photoprism.FindOriginalsByDate(conf.OriginalsPath(), afterDate, beforeDate) + originals := photoprism.FindOriginalsByDate(app.OriginalsPath(), afterDate, beforeDate) log.Infof("exporting photos to %s", exportPath) - photoprism.ExportPhotosFromOriginals(originals, conf.ThumbnailsPath(), exportPath, size) + photoprism.ExportPhotosFromOriginals(originals, app.ThumbnailsPath(), exportPath, size) log.Infof("photo export complete") diff --git a/internal/commands/import.go b/internal/commands/import.go index 3f4f321d3..36a97ebfd 100644 --- a/internal/commands/import.go +++ b/internal/commands/import.go @@ -15,25 +15,25 @@ var ImportCommand = cli.Command{ } func importAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - if err := conf.CreateDirectories(); err != nil { + if err := app.CreateDirectories(); err != nil { return err } - conf.MigrateDb() + app.MigrateDb() - log.Infof("importing photos from %s", conf.ImportPath()) + log.Infof("importing photos from %s", app.ImportPath()) - tensorFlow := photoprism.NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := photoprism.NewTensorFlow(app.TensorFlowModelPath()) - indexer := photoprism.NewIndexer(conf.OriginalsPath(), tensorFlow, conf.Db()) + indexer := photoprism.NewIndexer(app.OriginalsPath(), tensorFlow, app.Db()) - converter := photoprism.NewConverter(conf.DarktableCli()) + converter := photoprism.NewConverter(app.DarktableCli()) - importer := photoprism.NewImporter(conf.OriginalsPath(), indexer, converter) + importer := photoprism.NewImporter(app.OriginalsPath(), indexer, converter) - importer.ImportPhotosFromDirectory(conf.ImportPath()) + importer.ImportPhotosFromDirectory(app.ImportPath()) log.Info("photo import complete") diff --git a/internal/commands/index.go b/internal/commands/index.go index 3cc1a1b03..f17e96200 100644 --- a/internal/commands/index.go +++ b/internal/commands/index.go @@ -15,19 +15,19 @@ var IndexCommand = cli.Command{ } func indexAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - if err := conf.CreateDirectories(); err != nil { + if err := app.CreateDirectories(); err != nil { return err } - conf.MigrateDb() + app.MigrateDb() - log.Infof("indexing photos in %s", conf.OriginalsPath()) + log.Infof("indexing photos in %s", app.OriginalsPath()) - tensorFlow := photoprism.NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := photoprism.NewTensorFlow(app.TensorFlowModelPath()) - indexer := photoprism.NewIndexer(conf.OriginalsPath(), tensorFlow, conf.Db()) + indexer := photoprism.NewIndexer(app.OriginalsPath(), tensorFlow, app.Db()) files := indexer.IndexAll() diff --git a/internal/commands/migrate.go b/internal/commands/migrate.go index 3465cc03f..4cdcf586b 100644 --- a/internal/commands/migrate.go +++ b/internal/commands/migrate.go @@ -14,11 +14,11 @@ var MigrateCommand = cli.Command{ } func migrateAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext( ctx) log.Infoln("migrating database") - conf.MigrateDb() + app.MigrateDb() log.Infoln("database migration complete") diff --git a/internal/commands/start.go b/internal/commands/start.go index a6fdb0dfe..a8510496d 100644 --- a/internal/commands/start.go +++ b/internal/commands/start.go @@ -38,21 +38,21 @@ var startFlags = []cli.Flag{ } func startAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - if conf.HttpServerPort() < 1 { + if app.HttpServerPort() < 1 { log.Fatal("server port must be a positive integer") } - if err := conf.CreateDirectories(); err != nil { + if err := app.CreateDirectories(); err != nil { log.Fatal(err) } - conf.MigrateDb() + app.MigrateDb() - log.Infof("starting web server at %s:%d", conf.HttpServerHost(), conf.HttpServerPort()) + log.Infof("starting web server at %s:%d", app.HttpServerHost(), app.HttpServerPort()) - server.Start(conf) + server.Start(app) return nil } diff --git a/internal/commands/thumbnails.go b/internal/commands/thumbnails.go index 13aef1813..841554c97 100644 --- a/internal/commands/thumbnails.go +++ b/internal/commands/thumbnails.go @@ -29,13 +29,13 @@ var ThumbnailsCommand = cli.Command{ } func thumbnailsAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - if err := conf.CreateDirectories(); err != nil { + if err := app.CreateDirectories(); err != nil { return err } - log.Infof("creating thumbnails in %s", conf.ThumbnailsPath()) + log.Infof("creating thumbnails in \"%s\"", app.ThumbnailsPath()) sizes := ctx.IntSlice("size") @@ -49,7 +49,7 @@ func thumbnailsAction(ctx *cli.Context) error { } for _, size := range sizes { - photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), size, ctx.Bool("square")) + photoprism.CreateThumbnailsFromOriginals(app.OriginalsPath(), app.ThumbnailsPath(), size, ctx.Bool("square")) } log.Info("thumbnails created") diff --git a/internal/commands/version.go b/internal/commands/version.go index 830ec0332..57868aea8 100644 --- a/internal/commands/version.go +++ b/internal/commands/version.go @@ -15,9 +15,9 @@ var VersionCommand = cli.Command{ } func versionAction(ctx *cli.Context) error { - conf := context.NewConfig(ctx) + app := context.NewContext(ctx) - fmt.Println(conf.AppVersion()) + fmt.Println(app.Version()) return nil } diff --git a/internal/test/capture.go b/internal/context/capture.go similarity index 87% rename from internal/test/capture.go rename to internal/context/capture.go index 164ec3275..64947f1ec 100644 --- a/internal/test/capture.go +++ b/internal/context/capture.go @@ -1,4 +1,4 @@ -package test +package context import ( "bytes" @@ -7,7 +7,7 @@ import ( ) // Returns output to stdout and stderr for testing -func Capture(f func()) string { +func CaptureOutput(f func()) string { r, w, err := os.Pipe() if err != nil { panic(err) diff --git a/internal/test/capture_test.go b/internal/context/capture_test.go similarity index 66% rename from internal/test/capture_test.go rename to internal/context/capture_test.go index a0258be75..b646a4fbd 100644 --- a/internal/test/capture_test.go +++ b/internal/context/capture_test.go @@ -1,4 +1,4 @@ -package test +package context import ( "fmt" @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCapture(t *testing.T) { - result := Capture(func() { +func TestCaptureOutput(t *testing.T) { + result := CaptureOutput(func() { fmt.Fprint(os.Stdout, "foo") fmt.Fprint(os.Stderr, "bar") }) diff --git a/internal/context/client_config.go b/internal/context/client_config.go new file mode 100644 index 000000000..e7cd35d0e --- /dev/null +++ b/internal/context/client_config.go @@ -0,0 +1,4 @@ +package context + +// HTTP client / Web UI config values +type ClientConfig map[string]interface{} diff --git a/internal/context/config.go b/internal/context/config.go index 8e9a418b9..352ac30c4 100644 --- a/internal/context/config.go +++ b/internal/context/config.go @@ -1,18 +1,10 @@ package context import ( - "os" - "time" - - "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" "github.com/kylelemons/go-gypsy/yaml" - "github.com/photoprism/photoprism/internal/frontend" "github.com/photoprism/photoprism/internal/fsutil" - "github.com/photoprism/photoprism/internal/models" - "github.com/photoprism/photoprism/internal/tidb" - log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -30,66 +22,28 @@ const ( // // See https://github.com/photoprism/photoprism/issues/50#issuecomment-433856358 type Config struct { - appName string - appVersion string - appCopyright string - debug bool - logLevel string - configFile string - assetsPath string - cachePath string - originalsPath string - importPath string - exportPath string - sqlServerHost string - sqlServerPort uint - sqlServerPath string - sqlServerPassword string - httpServerHost string - httpServerPort int - httpServerMode string - httpServerPassword string - darktableCli string - databaseDriver string - databaseDsn string - db *gorm.DB -} - -// NewConfig() creates a new configuration entity by using two methods: -// -// 1. SetValuesFromFile: This will initialize values from a yaml config file. -// -// 2. SetValuesFromCliContext: Which comes after SetValuesFromFile and overrides -// any previous values giving an option two override file configs through the CLI. -func NewConfig(ctx *cli.Context) *Config { - c := &Config{} - - if ctx.GlobalBool("debug") { - c.debug = ctx.GlobalBool("debug") - } - - c.appName = ctx.App.Name - c.appCopyright = ctx.App.Copyright - c.appVersion = ctx.App.Version - - log.SetLevel(c.LogLevel()) - - log.SetFormatter(&log.TextFormatter{ - DisableColors: false, - FullTimestamp: true, - }) - - if err := c.SetValuesFromFile(fsutil.ExpandedFilename(ctx.GlobalString("config-file"))); err != nil { - log.Info(err) - } - - if err := c.SetValuesFromCliContext(ctx); err != nil { - log.Error(err) - } - - log.SetLevel(c.LogLevel()) - - return c + Name string + Version string + Copyright string + Debug bool + LogLevel string + ConfigFile string + AssetsPath string + CachePath string + OriginalsPath string + ImportPath string + ExportPath string + SqlServerHost string + SqlServerPort uint + SqlServerPath string + SqlServerPassword string + HttpServerHost string + HttpServerPort int + HttpServerMode string + HttpServerPassword string + DarktableCli string + DatabaseDriver string + DatabaseDsn string } // SetValuesFromFile uses a yaml config file to initiate the configuration entity. @@ -100,77 +54,77 @@ func (c *Config) SetValuesFromFile(fileName string) error { return err } - c.configFile = fileName + c.ConfigFile = fileName if debug, err := yamlConfig.GetBool("debug"); err == nil { - c.debug = debug + c.Debug = debug } if logLevel, err := yamlConfig.Get("log-level"); err == nil { - c.logLevel = logLevel + c.LogLevel = logLevel } if sqlServerHost, err := yamlConfig.Get("sql-host"); err == nil { - c.sqlServerHost = sqlServerHost + c.SqlServerHost = sqlServerHost } if sqlServerPort, err := yamlConfig.GetInt("sql-port"); err == nil { - c.sqlServerPort = uint(sqlServerPort) + c.SqlServerPort = uint(sqlServerPort) } if sqlServerPassword, err := yamlConfig.Get("sql-password"); err == nil { - c.sqlServerPassword = sqlServerPassword + c.SqlServerPassword = sqlServerPassword } if sqlServerPath, err := yamlConfig.Get("sql-path"); err == nil { - c.sqlServerPath = sqlServerPath + c.SqlServerPath = sqlServerPath } if httpServerHost, err := yamlConfig.Get("http-host"); err == nil { - c.httpServerHost = httpServerHost + c.HttpServerHost = httpServerHost } if httpServerPort, err := yamlConfig.GetInt("http-port"); err == nil { - c.httpServerPort = int(httpServerPort) + c.HttpServerPort = int(httpServerPort) } if httpServerMode, err := yamlConfig.Get("http-mode"); err == nil { - c.httpServerMode = httpServerMode + c.HttpServerMode = httpServerMode } if httpServerPassword, err := yamlConfig.Get("http-password"); err == nil { - c.httpServerPassword = httpServerPassword + c.HttpServerPassword = httpServerPassword } if assetsPath, err := yamlConfig.Get("assets-path"); err == nil { - c.assetsPath = fsutil.ExpandedFilename(assetsPath) + c.AssetsPath = fsutil.ExpandedFilename(assetsPath) } if cachePath, err := yamlConfig.Get("cache-path"); err == nil { - c.cachePath = fsutil.ExpandedFilename(cachePath) + c.CachePath = fsutil.ExpandedFilename(cachePath) } if originalsPath, err := yamlConfig.Get("originals-path"); err == nil { - c.originalsPath = fsutil.ExpandedFilename(originalsPath) + c.OriginalsPath = fsutil.ExpandedFilename(originalsPath) } if importPath, err := yamlConfig.Get("import-path"); err == nil { - c.importPath = fsutil.ExpandedFilename(importPath) + c.ImportPath = fsutil.ExpandedFilename(importPath) } if exportPath, err := yamlConfig.Get("export-path"); err == nil { - c.exportPath = fsutil.ExpandedFilename(exportPath) + c.ExportPath = fsutil.ExpandedFilename(exportPath) } if darktableCli, err := yamlConfig.Get("darktable-cli"); err == nil { - c.darktableCli = fsutil.ExpandedFilename(darktableCli) + c.DarktableCli = fsutil.ExpandedFilename(darktableCli) } if databaseDriver, err := yamlConfig.Get("database-driver"); err == nil { - c.databaseDriver = databaseDriver + c.DatabaseDriver = databaseDriver } if databaseDsn, err := yamlConfig.Get("database-dsn"); err == nil { - c.databaseDsn = databaseDsn + c.DatabaseDsn = databaseDsn } return nil @@ -180,376 +134,72 @@ func (c *Config) SetValuesFromFile(fileName string) error { // for the entity. func (c *Config) SetValuesFromCliContext(ctx *cli.Context) error { if ctx.GlobalBool("debug") { - c.debug = ctx.GlobalBool("debug") + c.Debug = ctx.GlobalBool("debug") } - if ctx.GlobalIsSet("log-level") || c.logLevel == "" { - c.logLevel = ctx.GlobalString("log-level") + if ctx.GlobalIsSet("log-level") || c.LogLevel == "" { + c.LogLevel = ctx.GlobalString("log-level") } - if ctx.GlobalIsSet("assets-path") || c.assetsPath == "" { - c.assetsPath = fsutil.ExpandedFilename(ctx.GlobalString("assets-path")) + if ctx.GlobalIsSet("assets-path") || c.AssetsPath == "" { + c.AssetsPath = fsutil.ExpandedFilename(ctx.GlobalString("assets-path")) } - if ctx.GlobalIsSet("cache-path") || c.cachePath == "" { - c.cachePath = fsutil.ExpandedFilename(ctx.GlobalString("cache-path")) + if ctx.GlobalIsSet("cache-path") || c.CachePath == "" { + c.CachePath = fsutil.ExpandedFilename(ctx.GlobalString("cache-path")) } - if ctx.GlobalIsSet("originals-path") || c.originalsPath == "" { - c.originalsPath = fsutil.ExpandedFilename(ctx.GlobalString("originals-path")) + if ctx.GlobalIsSet("originals-path") || c.OriginalsPath == "" { + c.OriginalsPath = fsutil.ExpandedFilename(ctx.GlobalString("originals-path")) } - if ctx.GlobalIsSet("import-path") || c.importPath == "" { - c.importPath = fsutil.ExpandedFilename(ctx.GlobalString("import-path")) + if ctx.GlobalIsSet("import-path") || c.ImportPath == "" { + c.ImportPath = fsutil.ExpandedFilename(ctx.GlobalString("import-path")) } - if ctx.GlobalIsSet("export-path") || c.exportPath == "" { - c.exportPath = fsutil.ExpandedFilename(ctx.GlobalString("export-path")) + if ctx.GlobalIsSet("export-path") || c.ExportPath == "" { + c.ExportPath = fsutil.ExpandedFilename(ctx.GlobalString("export-path")) } - if ctx.GlobalIsSet("darktable-cli") || c.darktableCli == "" { - c.darktableCli = fsutil.ExpandedFilename(ctx.GlobalString("darktable-cli")) + if ctx.GlobalIsSet("darktable-cli") || c.DarktableCli == "" { + c.DarktableCli = fsutil.ExpandedFilename(ctx.GlobalString("darktable-cli")) } - if ctx.GlobalIsSet("database-driver") || c.databaseDriver == "" { - c.databaseDriver = ctx.GlobalString("database-driver") + if ctx.GlobalIsSet("database-driver") || c.DatabaseDriver == "" { + c.DatabaseDriver = ctx.GlobalString("database-driver") } - if ctx.GlobalIsSet("database-dsn") || c.databaseDsn == "" { - c.databaseDsn = ctx.GlobalString("database-dsn") + if ctx.GlobalIsSet("database-dsn") || c.DatabaseDsn == "" { + c.DatabaseDsn = ctx.GlobalString("database-dsn") } - if ctx.GlobalIsSet("sql-host") || c.sqlServerHost == "" { - c.sqlServerHost = ctx.GlobalString("sql-host") + if ctx.GlobalIsSet("sql-host") || c.SqlServerHost == "" { + c.SqlServerHost = ctx.GlobalString("sql-host") } - if ctx.GlobalIsSet("sql-port") || c.sqlServerPort == 0 { - c.sqlServerPort = ctx.GlobalUint("sql-port") + if ctx.GlobalIsSet("sql-port") || c.SqlServerPort == 0 { + c.SqlServerPort = ctx.GlobalUint("sql-port") } - if ctx.GlobalIsSet("sql-path") || c.sqlServerPath == "" { - c.sqlServerPath = ctx.GlobalString("sql-path") + if ctx.GlobalIsSet("sql-password") || c.SqlServerPassword == "" { + c.SqlServerPassword = ctx.GlobalString("sql-password") } - if ctx.GlobalIsSet("http-host") || c.httpServerHost == "" { - c.httpServerHost = ctx.GlobalString("http-host") + if ctx.GlobalIsSet("sql-path") || c.SqlServerPath == "" { + c.SqlServerPath = ctx.GlobalString("sql-path") } - if ctx.GlobalIsSet("http-port") || c.httpServerPort == 0 { - c.httpServerPort = ctx.GlobalInt("http-port") + if ctx.GlobalIsSet("http-host") || c.HttpServerHost == "" { + c.HttpServerHost = ctx.GlobalString("http-host") } - if ctx.GlobalIsSet("http-mode") || c.httpServerMode == "" { - c.httpServerMode = ctx.GlobalString("http-mode") + if ctx.GlobalIsSet("http-port") || c.HttpServerPort == 0 { + c.HttpServerPort = ctx.GlobalInt("http-port") + } + + if ctx.GlobalIsSet("http-mode") || c.HttpServerMode == "" { + c.HttpServerMode = ctx.GlobalString("http-mode") } return nil } - -// CreateDirectories creates all the folders that photoprism needs. These are: -// originalsPath -// ThumbnailsPath -// importPath -// exportPath -func (c *Config) CreateDirectories() error { - if err := os.MkdirAll(c.OriginalsPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.ImportPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.ExportPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.ThumbnailsPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.SqlServerPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.TensorFlowModelPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.HttpPublicBuildPath(), os.ModePerm); err != nil { - return err - } - - return nil -} - -// connectToDatabase establishes a database connection. -// When used with the internal driver, it may create a new database server instance. -// It tries to do this 12 times with a 5 second sleep interval in between. -func (c *Config) connectToDatabase() error { - dbDriver := c.DatabaseDriver() - dbDsn := c.DatabaseDsn() - - isTiDB := false - initSuccess := false - - if dbDriver == DbTiDB { - isTiDB = true - dbDriver = DbMySQL - } - - db, err := gorm.Open(dbDriver, dbDsn) - - if err != nil || db == nil { - if isTiDB { - log.Infof("starting database server at %s:%d\n", c.SqlServerHost(), c.SqlServerPort()) - - go tidb.Start(c.SqlServerPath(), c.SqlServerPort(), c.SqlServerHost(), c.Debug()) - } - - for i := 1; i <= 12; i++ { - time.Sleep(5 * time.Second) - - db, err = gorm.Open(dbDriver, dbDsn) - - if db != nil && err == nil { - break - } - - if isTiDB && !initSuccess { - err = tidb.InitDatabase(c.SqlServerPort(), c.SqlServerPassword()) - - if err != nil { - log.Debug(err) - } else { - initSuccess = true - } - } - } - - if err != nil || db == nil { - log.Fatal(err) - } - } - - c.db = db - - return err -} - -// AppName returns the application name. -func (c *Config) AppName() string { - return c.appName -} - -// AppVersion returns the application version. -func (c *Config) AppVersion() string { - return c.appVersion -} - -// AppCopyright returns the application copyright. -func (c *Config) AppCopyright() string { - return c.appCopyright -} - -// Debug returns true if debug mode is on. -func (c *Config) Debug() bool { - return c.debug -} - -// LogLevel returns the logrus log level. -func (c *Config) LogLevel() log.Level { - if c.Debug() { - c.logLevel = "debug" - } - - if logLevel, err := log.ParseLevel(c.logLevel); err == nil { - return logLevel - } else { - return log.ErrorLevel - } -} - -// ConfigFile returns the config file name. -func (c *Config) ConfigFile() string { - return c.configFile -} - -// SqlServerHost returns the built-in SQL server host name or IP address (empty for all interfaces). -func (c *Config) SqlServerHost() string { - return c.sqlServerHost -} - -// SqlServerPort returns the built-in SQL server port. -func (c *Config) SqlServerPort() uint { - return c.sqlServerPort -} - -// SqlServerPath returns the database storage path for TiDB. -func (c *Config) SqlServerPath() string { - if c.sqlServerPath != "" { - return c.sqlServerPath - } - - return c.ServerPath() + "/database" -} - -// SqlServerPassword returns the password for the built-in database server. -func (c *Config) SqlServerPassword() string { - return c.sqlServerPassword -} - -// HttpServerHost returns the built-in HTTP server host name or IP address (empty for all interfaces). -func (c *Config) HttpServerHost() string { - if c.httpServerHost == "" { - return "0.0.0.0" - } - - return c.httpServerHost -} - -// HttpServerPort returns the built-in HTTP server port. -func (c *Config) HttpServerPort() int { - return c.httpServerPort -} - -// HttpServerMode returns the server mode. -func (c *Config) HttpServerMode() string { - return c.httpServerMode -} - -// HttpServerPassword returns the password for the user interface (optional). -func (c *Config) HttpServerPassword() string { - return c.httpServerPassword -} - -// OriginalsPath returns the originals. -func (c *Config) OriginalsPath() string { - return c.originalsPath -} - -// ImportPath returns the import directory. -func (c *Config) ImportPath() string { - return c.importPath -} - -// ExportPath returns the export directory. -func (c *Config) ExportPath() string { - return c.exportPath -} - -// DarktableCli returns the darktable-cli binary file name. -func (c *Config) DarktableCli() string { - return c.darktableCli -} - -// DatabaseDriver returns the database driver name. -func (c *Config) DatabaseDriver() string { - return c.databaseDriver -} - -// DatabaseDsn returns the database data source name (DSN). -func (c *Config) DatabaseDsn() string { - return c.databaseDsn -} - -// CachePath returns the path to the cache. -func (c *Config) CachePath() string { - return c.cachePath -} - -// ThumbnailsPath returns the path to the cached thumbnails. -func (c *Config) ThumbnailsPath() string { - return c.CachePath() + "/thumbnails" -} - -// AssetsPath returns the path to the assets. -func (c *Config) AssetsPath() string { - return c.assetsPath -} - -// TensorFlowModelPath returns the tensorflow model path. -func (c *Config) TensorFlowModelPath() string { - return c.AssetsPath() + "/tensorflow" -} - -// ServerPath returns the server assets path (public files, favicons, templates,...). -func (c *Config) ServerPath() string { - return c.AssetsPath() + "/server" -} - -// HttpTemplatesPath returns the server templates path. -func (c *Config) HttpTemplatesPath() string { - return c.ServerPath() + "/templates" -} - -// HttpFaviconsPath returns the favicons path. -func (c *Config) HttpFaviconsPath() string { - return c.HttpPublicPath() + "/favicons" -} - -// HttpPublicPath returns the public server path (//server/assets/*). -func (c *Config) HttpPublicPath() string { - return c.ServerPath() + "/public" -} - -// HttpPublicBuildPath returns the public build path (//server/assets/build/*). -func (c *Config) HttpPublicBuildPath() string { - return c.HttpPublicPath() + "/build" -} - -// Db returns the db connection. -func (c *Config) Db() *gorm.DB { - if c.db == nil { - c.connectToDatabase() - } - - return c.db -} - -// MigrateDb will start a migration process. -func (c *Config) MigrateDb() { - db := c.Db() - - db.AutoMigrate(&models.File{}, - &models.Photo{}, - &models.Tag{}, - &models.Album{}, - &models.Location{}, - &models.Camera{}, - &models.Lens{}, - &models.Country{}) -} - -// ClientConfig returns a loaded and set configuration entity. -func (c *Config) ClientConfig() frontend.Config { - db := c.Db() - - var cameras []*models.Camera - - type country struct { - LocCountry string - LocCountryCode string - } - - var countries []country - - db.Model(&models.Location{}).Select("DISTINCT loc_country_code, loc_country").Scan(&countries) - - db.Where("deleted_at IS NULL").Limit(1000).Order("camera_model").Find(&cameras) - - jsHash := fsutil.Hash(c.HttpPublicBuildPath() + "/app.js") - cssHash := fsutil.Hash(c.HttpPublicBuildPath() + "/app.css") - - result := frontend.Config{ - "appName": c.AppName(), - "appVersion": c.AppVersion(), - "debug": c.Debug(), - "cameras": cameras, - "countries": countries, - "jsHash": jsHash, - "cssHash": cssHash, - } - - return result -} diff --git a/internal/context/config_test.go b/internal/context/config_test.go index c7e12f358..749b30a5d 100644 --- a/internal/context/config_test.go +++ b/internal/context/config_test.go @@ -4,38 +4,21 @@ import ( "testing" "github.com/photoprism/photoprism/internal/fsutil" - "github.com/photoprism/photoprism/internal/test" - "github.com/stretchr/testify/assert" ) -func TestNewConfig(t *testing.T) { - ctx := test.CliContext() - - assert.True(t, ctx.IsSet("assets-path")) - assert.False(t, ctx.Bool("debug")) - - c := NewConfig(ctx) - - assert.IsType(t, new(Config), c) - - assert.Equal(t, fsutil.ExpandedFilename("../../assets"), c.AssetsPath()) - assert.False(t, c.Debug()) -} - func TestConfig_SetValuesFromFile(t *testing.T) { - c := NewConfig(test.CliContext()) + c := NewConfig(CliTestContext()) err := c.SetValuesFromFile(fsutil.ExpandedFilename("../../configs/photoprism.yml")) assert.Nil(t, err) - assert.Equal(t, "/srv/photoprism", c.AssetsPath()) - assert.Equal(t, "/srv/photoprism/cache", c.CachePath()) - assert.Equal(t, "/srv/photoprism/cache/thumbnails", c.ThumbnailsPath()) - assert.Equal(t, "/srv/photoprism/photos/originals", c.OriginalsPath()) - assert.Equal(t, "/srv/photoprism/photos/import", c.ImportPath()) - assert.Equal(t, "/srv/photoprism/photos/export", c.ExportPath()) - assert.Equal(t, "internal", c.DatabaseDriver()) - assert.Equal(t, "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true", c.DatabaseDsn()) + assert.Equal(t, "/srv/photoprism", c.AssetsPath) + assert.Equal(t, "/srv/photoprism/cache", c.CachePath) + assert.Equal(t, "/srv/photoprism/photos/originals", c.OriginalsPath) + assert.Equal(t, "/srv/photoprism/photos/import", c.ImportPath) + assert.Equal(t, "/srv/photoprism/photos/export", c.ExportPath) + assert.Equal(t, "internal", c.DatabaseDriver) + assert.Equal(t, "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true", c.DatabaseDsn) } diff --git a/internal/context/context.go b/internal/context/context.go new file mode 100644 index 000000000..66b36ef58 --- /dev/null +++ b/internal/context/context.go @@ -0,0 +1,373 @@ +package context + +import ( + "os" + "time" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/sqlite" + "github.com/photoprism/photoprism/internal/fsutil" + "github.com/photoprism/photoprism/internal/models" + "github.com/photoprism/photoprism/internal/tidb" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +type Context struct { + db *gorm.DB + config *Config +} + +// NewConfig() creates a new configuration entity by using two methods: +// +// 1. SetValuesFromFile: This will initialize values from a yaml config file. +// +// 2. SetValuesFromCliContext: Which comes after SetValuesFromFile and overrides +// any previous values giving an option two override file configs through the CLI. +func NewConfig(ctx *cli.Context) *Config { + c := &Config{} + + c.Name = ctx.App.Name + c.Copyright = ctx.App.Copyright + c.Version = ctx.App.Version + + log.SetFormatter(&log.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + }) + + if err := c.SetValuesFromFile(fsutil.ExpandedFilename(ctx.GlobalString("config-file"))); err != nil { + log.Debug(err) + } + + if err := c.SetValuesFromCliContext(ctx); err != nil { + log.Error(err) + } + + return c +} + +func NewContext(ctx *cli.Context) *Context { + if ctx.GlobalBool("debug") { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(log.ErrorLevel) + } + + c := &Context{config: NewConfig(ctx)} + + log.SetLevel(c.LogLevel()) + + return c +} + + +// CreateDirectories creates all the folders that photoprism needs. These are: +// OriginalsPath +// ThumbnailsPath +// ImportPath +// ExportPath +func (c *Context) CreateDirectories() error { + if err := os.MkdirAll(c.OriginalsPath(), os.ModePerm); err != nil { + return err + } + + if err := os.MkdirAll(c.ImportPath(), os.ModePerm); err != nil { + return err + } + + if err := os.MkdirAll(c.ExportPath(), os.ModePerm); err != nil { + return err + } + + if err := os.MkdirAll(c.ThumbnailsPath(), os.ModePerm); err != nil { + return err + } + + if err := os.MkdirAll(c.SqlServerPath(), os.ModePerm); err != nil { + return err + } + + if err := os.MkdirAll(c.TensorFlowModelPath(), os.ModePerm); err != nil { + return err + } + + if err := os.MkdirAll(c.HttpPublicBuildPath(), os.ModePerm); err != nil { + return err + } + + return nil +} + +// connectToDatabase establishes a database connection. +// When used with the internal driver, it may create a new database server instance. +// It tries to do this 12 times with a 5 second sleep interval in between. +func (c *Context) connectToDatabase() error { + dbDriver := c.DatabaseDriver() + dbDsn := c.DatabaseDsn() + + isTiDB := false + initSuccess := false + + if dbDriver == DbTiDB { + isTiDB = true + dbDriver = DbMySQL + } + + db, err := gorm.Open(dbDriver, dbDsn) + + if err != nil || db == nil { + if isTiDB { + log.Infof("starting database server at %s:%d\n", c.SqlServerHost(), c.SqlServerPort()) + + go tidb.Start(c.SqlServerPath(), c.SqlServerPort(), c.SqlServerHost(), c.Debug()) + } + + for i := 1; i <= 12; i++ { + time.Sleep(5 * time.Second) + + db, err = gorm.Open(dbDriver, dbDsn) + + if db != nil && err == nil { + break + } + + if isTiDB && !initSuccess { + err = tidb.InitDatabase(c.SqlServerPort(), c.SqlServerPassword()) + + if err != nil { + log.Debug(err) + } else { + initSuccess = true + } + } + } + + if err != nil || db == nil { + log.Fatal(err) + } + } + + c.db = db + + return err +} + +// Name returns the application name. +func (c *Context) Name() string { + return c.config.Name +} + +// Version returns the application version. +func (c *Context) Version() string { + return c.config.Version +} + +// Copyright returns the application copyright. +func (c *Context) Copyright() string { + return c.config.Copyright +} + +// Debug returns true if Debug mode is on. +func (c *Context) Debug() bool { + return c.config.Debug +} + +// LogLevel returns the logrus log level. +func (c *Context) LogLevel() log.Level { + if c.Debug() { + c.config.LogLevel = "debug" + } + + if logLevel, err := log.ParseLevel(c.config.LogLevel); err == nil { + return logLevel + } else { + return log.ErrorLevel + } +} + +// TestConfigFile returns the config file name. +func (c *Context) ConfigFile() string { + return c.config.ConfigFile +} + +// SqlServerHost returns the built-in SQL server host name or IP address (empty for all interfaces). +func (c *Context) SqlServerHost() string { + return c.config.SqlServerHost +} + +// SqlServerPort returns the built-in SQL server port. +func (c *Context) SqlServerPort() uint { + return c.config.SqlServerPort +} + +// SqlServerPath returns the database storage path for TiDB. +func (c *Context) SqlServerPath() string { + if c.config.SqlServerPath != "" { + return c.config.SqlServerPath + } + + return c.ServerPath() + "/database" +} + +// SqlServerPassword returns the password for the built-in database server. +func (c *Context) SqlServerPassword() string { + return c.config.SqlServerPassword +} + +// HttpServerHost returns the built-in HTTP server host name or IP address (empty for all interfaces). +func (c *Context) HttpServerHost() string { + if c.config.HttpServerHost == "" { + return "0.0.0.0" + } + + return c.config.HttpServerHost +} + +// HttpServerPort returns the built-in HTTP server port. +func (c *Context) HttpServerPort() int { + return c.config.HttpServerPort +} + +// HttpServerMode returns the server mode. +func (c *Context) HttpServerMode() string { + return c.config.HttpServerMode +} + +// HttpServerPassword returns the password for the user interface (optional). +func (c *Context) HttpServerPassword() string { + return c.config.HttpServerPassword +} + +// OriginalsPath returns the originals. +func (c *Context) OriginalsPath() string { + return c.config.OriginalsPath +} + +// ImportPath returns the import directory. +func (c *Context) ImportPath() string { + return c.config.ImportPath +} + +// ExportPath returns the export directory. +func (c *Context) ExportPath() string { + return c.config.ExportPath +} + +// DarktableCli returns the darktable-cli binary file name. +func (c *Context) DarktableCli() string { + return c.config.DarktableCli +} + +// DatabaseDriver returns the database driver name. +func (c *Context) DatabaseDriver() string { + return c.config.DatabaseDriver +} + +// DatabaseDsn returns the database data source name (DSN). +func (c *Context) DatabaseDsn() string { + return c.config.DatabaseDsn +} + +// CachePath returns the path to the cache. +func (c *Context) CachePath() string { + return c.config.CachePath +} + +// ThumbnailsPath returns the path to the cached thumbnails. +func (c *Context) ThumbnailsPath() string { + return c.CachePath() + "/thumbnails" +} + +// AssetsPath returns the path to the assets. +func (c *Context) AssetsPath() string { + return c.config.AssetsPath +} + +// TensorFlowModelPath returns the tensorflow model path. +func (c *Context) TensorFlowModelPath() string { + return c.AssetsPath() + "/tensorflow" +} + +// ServerPath returns the server assets path (public files, favicons, templates,...). +func (c *Context) ServerPath() string { + return c.AssetsPath() + "/server" +} + +// HttpTemplatesPath returns the server templates path. +func (c *Context) HttpTemplatesPath() string { + return c.ServerPath() + "/templates" +} + +// HttpFaviconsPath returns the favicons path. +func (c *Context) HttpFaviconsPath() string { + return c.HttpPublicPath() + "/favicons" +} + +// HttpPublicPath returns the public server path (//server/assets/*). +func (c *Context) HttpPublicPath() string { + return c.ServerPath() + "/public" +} + +// HttpPublicBuildPath returns the public build path (//server/assets/build/*). +func (c *Context) HttpPublicBuildPath() string { + return c.HttpPublicPath() + "/build" +} + +// Db returns the db connection. +func (c *Context) Db() *gorm.DB { + if c.db == nil { + c.connectToDatabase() + } + + return c.db +} + +// MigrateDb will start a migration process. +func (c *Context) MigrateDb() { + db := c.Db() + + db.AutoMigrate(&models.File{}, + &models.Photo{}, + &models.Tag{}, + &models.Album{}, + &models.Location{}, + &models.Camera{}, + &models.Lens{}, + &models.Country{}) +} + +// ClientConfig returns a loaded and set configuration entity. +func (c *Context) ClientConfig() ClientConfig { + db := c.Db() + + var cameras []*models.Camera + + type country struct { + LocCountry string + LocCountryCode string + } + + var countries []country + + db.Model(&models.Location{}).Select("DISTINCT loc_country_code, loc_country").Scan(&countries) + + db.Where("deleted_at IS NULL").Limit(1000).Order("camera_model").Find(&cameras) + + jsHash := fsutil.Hash(c.HttpPublicBuildPath() + "/app.js") + cssHash := fsutil.Hash(c.HttpPublicBuildPath() + "/app.css") + + result := ClientConfig{ + "name": c.Name(), + "version": c.Version(), + "copyright": c.Copyright(), + "debug": c.Debug(), + "cameras": cameras, + "countries": countries, + "jsHash": jsHash, + "cssHash": cssHash, + } + + return result +} diff --git a/internal/context/context_test.go b/internal/context/context_test.go new file mode 100644 index 000000000..335885a9a --- /dev/null +++ b/internal/context/context_test.go @@ -0,0 +1,22 @@ +package context + +import ( + "testing" + + "github.com/photoprism/photoprism/internal/fsutil" + "github.com/stretchr/testify/assert" +) + +func TestNewAppConfig(t *testing.T) { + ctx := CliTestContext() + + assert.True(t, ctx.IsSet("assets-path")) + assert.False(t, ctx.Bool("debug")) + + c := NewConfig(ctx) + + assert.IsType(t, new(Config), c) + + assert.Equal(t, fsutil.ExpandedFilename("../../assets"), c.AssetsPath) + assert.False(t, c.Debug) +} diff --git a/internal/context/test_context.go b/internal/context/test_context.go new file mode 100644 index 000000000..0cb33ca91 --- /dev/null +++ b/internal/context/test_context.go @@ -0,0 +1,129 @@ +package context + +import ( + "flag" + "fmt" + "os" + "testing" + + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/sqlite" + "github.com/photoprism/photoprism/internal/fsutil" + "github.com/urfave/cli" + + log "github.com/sirupsen/logrus" +) + +const ( + TestDataZip = "/tmp/photoprism/testdata.zip" + TestDataURL = "https://dl.photoprism.org/fixtures/testdata-20190501.zip" + TestDataHash = "1a59b358b80221ab3e76efb683ad72402f0b0844" +) + +var testContext *Context + +func testDataPath(assetsPath string) string { + return assetsPath + "/testdata" +} + +func NewTestConfig() *Config { + assetsPath := fsutil.ExpandedFilename("../../assets") + testDataPath := testDataPath(assetsPath) + + c := &Config{ + ConfigFile: "../../configs/photoprism.yml", + DarktableCli: "/usr/bin/darktable-cli", + AssetsPath: assetsPath, + CachePath: testDataPath + "/cache", + OriginalsPath: testDataPath + "/originals", + ImportPath: testDataPath + "/import", + ExportPath: testDataPath + "/export", + DatabaseDriver: "mysql", + DatabaseDsn: "photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true", + } + + return c +} + +func TestContext() *Context { + if testContext == nil { + testContext = NewTestContext() + } + + return testContext +} + +func NewTestContext() *Context { + log.SetLevel(log.FatalLevel) + + c := &Context{config: NewTestConfig()} + + c.MigrateDb() + + return c +} + +// Returns example cli context for testing +func CliTestContext() *cli.Context { + config := NewTestConfig() + + globalSet := flag.NewFlagSet("test", 0) + globalSet.Bool("debug", false, "doc") + globalSet.String("config-file", config.ConfigFile, "doc") + globalSet.String("assets-path", config.AssetsPath, "doc") + globalSet.String("originals-path", config.OriginalsPath, "doc") + globalSet.String("darktable-cli", config.DarktableCli, "doc") + + app := cli.NewApp() + + c := cli.NewContext(app, globalSet, nil) + + c.Set("config-file", config.ConfigFile) + c.Set("assets-path", config.AssetsPath) + c.Set("originals-path", config.OriginalsPath) + c.Set("darktable-cli", config.DarktableCli) + + return c +} + +func (c *Context) RemoveTestData(t *testing.T) { + os.RemoveAll(c.ImportPath()) + os.RemoveAll(c.ExportPath()) + os.RemoveAll(c.OriginalsPath()) + os.RemoveAll(c.CachePath()) +} + +func (c *Context) DownloadTestData(t *testing.T) { + if fsutil.Exists(TestDataZip) { + hash := fsutil.Hash(TestDataZip) + + if hash != TestDataHash { + os.Remove(TestDataZip) + t.Logf("Removed outdated test data zip file (fingerprint %s)\n", hash) + } + } + + if !fsutil.Exists(TestDataZip) { + fmt.Printf("Downloading latest test data zip file from %s\n", TestDataURL) + + if err := fsutil.Download(TestDataZip, TestDataURL); err != nil { + fmt.Printf("Download failed: %s\n", err.Error()) + } + } +} + +func (c *Context) UnzipTestData(t *testing.T) { + if _, err := fsutil.Unzip(TestDataZip, testDataPath(c.AssetsPath())); err != nil { + t.Logf("Could not unzip test data: %s\n", err.Error()) + } +} + +func (c *Context) InitializeTestData(t *testing.T) { + t.Log("Initializing test data") + + c.RemoveTestData(t) + + c.DownloadTestData(t) + + c.UnzipTestData(t) +} diff --git a/internal/context/test_context_test.go b/internal/context/test_context_test.go new file mode 100644 index 000000000..6d41d8aa7 --- /dev/null +++ b/internal/context/test_context_test.go @@ -0,0 +1,39 @@ +package context + +import ( + "testing" + + "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/internal/fsutil" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestTestCliContext(t *testing.T) { + result := CliTestContext() + + assert.IsType(t, new(cli.Context), result) +} + +func TestTestContext(t *testing.T) { + result := TestContext() + + assert.IsType(t, new(Context), result) +} + +func TestNewTestConfig(t *testing.T) { + c := NewTestConfig() + + assert.IsType(t, new(Config), c) + + assert.Equal(t, fsutil.ExpandedFilename("../../assets"), c.AssetsPath) + assert.False(t, c.Debug) +} + +func TestNewTestContext_Db(t *testing.T) { + c := NewTestContext() + + db := c.Db() + + assert.IsType(t, &gorm.DB{}, db) +} diff --git a/internal/forms/photo-search.go b/internal/forms/photo_search.go similarity index 100% rename from internal/forms/photo-search.go rename to internal/forms/photo_search.go diff --git a/internal/forms/photo_search_test.go b/internal/forms/photo_search_test.go new file mode 100644 index 000000000..cef4e1d56 --- /dev/null +++ b/internal/forms/photo_search_test.go @@ -0,0 +1,13 @@ +package forms + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPhotoSearchForm(t *testing.T) { + form := &PhotoSearchForm{} + + assert.IsType(t, new(PhotoSearchForm), form) +} diff --git a/internal/frontend/config.go b/internal/frontend/config.go deleted file mode 100644 index 72dd15647..000000000 --- a/internal/frontend/config.go +++ /dev/null @@ -1,4 +0,0 @@ -package frontend - -// HTTP client / Web UI config values -type Config map[string]interface{} diff --git a/internal/frontend/doc.go b/internal/frontend/doc.go deleted file mode 100644 index 28b4facb8..000000000 --- a/internal/frontend/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -/* -Package frontend contains user interface related code. - -Additional information can be found in our Developer Guide: - -https://github.com/photoprism/photoprism/wiki -*/ -package frontend diff --git a/internal/fsutil/file_test.go b/internal/fsutil/file_test.go new file mode 100644 index 000000000..978e25e6c --- /dev/null +++ b/internal/fsutil/file_test.go @@ -0,0 +1,20 @@ +package fsutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExists(t *testing.T) { + assert.True(t, Exists("./_fixtures/test.jpg")) + assert.False(t, Exists("./foo.jpg")) +} + +func TestExpandedFilename(t *testing.T) { + filename := ExpandedFilename("./_fixtures/test.jpg") + + assert.IsType(t, "", filename) + + t.Logf("ExpandedFilename: %s", filename) +} diff --git a/internal/fsutil/hash_test.go b/internal/fsutil/hash_test.go new file mode 100644 index 000000000..5cd232656 --- /dev/null +++ b/internal/fsutil/hash_test.go @@ -0,0 +1,12 @@ +package fsutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHash(t *testing.T) { + hash := Hash("_fixtures/test.jpg") + assert.Equal(t, "516cb1fefbfd9fa66f1db50b94503a480cee30db", hash) +} diff --git a/internal/models/camera.go b/internal/models/camera.go index c7b826572..8e3c22332 100644 --- a/internal/models/camera.go +++ b/internal/models/camera.go @@ -22,7 +22,13 @@ func NewCamera(modelName string, makeName string) *Camera { modelName = "Unknown" } - cameraSlug := slug.MakeLang(modelName, "en") + var cameraSlug string + + if makeName != "" { + cameraSlug = slug.MakeLang(makeName+" "+modelName, "en") + } else { + cameraSlug = slug.MakeLang(modelName, "en") + } result := &Camera{ CameraModel: modelName, diff --git a/internal/models/camera_test.go b/internal/models/camera_test.go new file mode 100644 index 000000000..6a09e0a96 --- /dev/null +++ b/internal/models/camera_test.go @@ -0,0 +1,18 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewCamera(t *testing.T) { + camera := NewCamera("EOS 6D", "Canon") + + expected := &Camera{ + CameraModel: "EOS 6D", + CameraMake: "Canon", + CameraSlug: "canon-eos-6d", + } + assert.Equal(t, expected, camera) +} diff --git a/internal/photoprism/colors_test.go b/internal/photoprism/colors_test.go index 34b5c2c50..c5cf4b037 100644 --- a/internal/photoprism/colors_test.go +++ b/internal/photoprism/colors_test.go @@ -3,17 +3,17 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestMediaFile_GetColors(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) t.Run("dog.jpg", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ImportPath() + "/dog.jpg"); err == nil { + if mediaFile, err := NewMediaFile(ctx.ImportPath() + "/dog.jpg"); err == nil { colors, main, l, s, err := mediaFile.Colors() t.Log(colors, main, l, s, err) @@ -30,7 +30,7 @@ func TestMediaFile_GetColors(t *testing.T) { }) t.Run("ape.jpeg", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ImportPath() + "/ape.jpeg"); err == nil { + if mediaFile, err := NewMediaFile(ctx.ImportPath() + "/ape.jpeg"); err == nil { colors, main, l, s, err := mediaFile.Colors() t.Log(colors, main, l, s, err) @@ -47,7 +47,7 @@ func TestMediaFile_GetColors(t *testing.T) { }) t.Run("iphone/IMG_6788.JPG", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG"); err == nil { + if mediaFile, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG"); err == nil { colors, main, l, s, err := mediaFile.Colors() t.Log(colors, main, l, s, err) @@ -63,7 +63,7 @@ func TestMediaFile_GetColors(t *testing.T) { }) t.Run("raw/20140717_154212_1EC48F8489.jpg", func(t *testing.T) { - if mediaFile, err := NewMediaFile(conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg"); err == nil { + if mediaFile, err := NewMediaFile(ctx.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg"); err == nil { colors, main, l, s, err := mediaFile.Colors() t.Log(colors, main, l, s, err) diff --git a/internal/photoprism/config.go b/internal/photoprism/config.go deleted file mode 100644 index b3d4b7a5d..000000000 --- a/internal/photoprism/config.go +++ /dev/null @@ -1,54 +0,0 @@ -package photoprism - -import ( - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/mysql" - _ "github.com/jinzhu/gorm/dialects/sqlite" - "github.com/photoprism/photoprism/internal/frontend" - "github.com/sirupsen/logrus" -) - -// Config interface implemented in context (cli) and test packages -type Config interface { - Debug() bool - LogLevel() logrus.Level - Db() *gorm.DB - - CreateDirectories() error - MigrateDb() - - ClientConfig() frontend.Config - ConfigFile() string - - AppName() string - AppVersion() string - AppCopyright() string - - SqlServerHost() string - SqlServerPort() uint - SqlServerPath() string - SqlServerPassword() string - - HttpServerHost() string - HttpServerPort() int - HttpServerMode() string - HttpServerPassword() string - HttpTemplatesPath() string - HttpFaviconsPath() string - HttpPublicPath() string - HttpPublicBuildPath() string - - DatabaseDriver() string - DatabaseDsn() string - - AssetsPath() string - ServerPath() string - OriginalsPath() string - ImportPath() string - ExportPath() string - CachePath() string - ThumbnailsPath() string - TensorFlowModelPath() string - - DarktableCli() string -} diff --git a/internal/photoprism/config_test.go b/internal/photoprism/config_test.go deleted file mode 100644 index a794afdd3..000000000 --- a/internal/photoprism/config_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package photoprism - -import ( - "flag" - "testing" - - "github.com/jinzhu/gorm" - "github.com/photoprism/photoprism/internal/context" - "github.com/photoprism/photoprism/internal/fsutil" - "github.com/photoprism/photoprism/internal/test" - "github.com/urfave/cli" - - "github.com/stretchr/testify/assert" -) - -func getTestCliContext() *cli.Context { - globalSet := flag.NewFlagSet("test", 0) - globalSet.Bool("debug", false, "doc") - globalSet.String("config-file", test.ConfigFile, "doc") - globalSet.String("assets-path", test.AssetsPath, "doc") - globalSet.String("originals-path", test.OriginalsPath, "doc") - globalSet.String("darktable-cli", test.DarktableCli, "doc") - - app := cli.NewApp() - - c := cli.NewContext(app, globalSet, nil) - - c.Set("config-file", test.ConfigFile) - c.Set("assets-path", test.AssetsPath) - c.Set("originals-path", test.OriginalsPath) - c.Set("darktable-cli", test.DarktableCli) - - return c -} - -func TestNewConfig(t *testing.T) { - ctx := getTestCliContext() - - assert.True(t, ctx.IsSet("assets-path")) - assert.False(t, ctx.Bool("debug")) - - c := context.NewConfig(ctx) - - assert.IsType(t, new(context.Config), c) - - assert.Equal(t, test.AssetsPath, c.AssetsPath()) - assert.False(t, c.Debug()) -} - -func TestContextConfig_SetValuesFromFile(t *testing.T) { - c := context.NewConfig(getTestCliContext()) - - c.SetValuesFromFile(fsutil.ExpandedFilename(test.ConfigFile)) - - assert.Equal(t, "/srv/photoprism", c.AssetsPath()) - assert.Equal(t, "/srv/photoprism/cache", c.CachePath()) - assert.Equal(t, "/srv/photoprism/cache/thumbnails", c.ThumbnailsPath()) - assert.Equal(t, "/srv/photoprism/photos/originals", c.OriginalsPath()) - assert.Equal(t, "/srv/photoprism/photos/import", c.ImportPath()) - assert.Equal(t, "/srv/photoprism/photos/export", c.ExportPath()) - assert.Equal(t, "internal", c.DatabaseDriver()) - assert.Equal(t, "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true", c.DatabaseDsn()) -} - -func TestTestConfig_ConnectToDatabase(t *testing.T) { - c := test.NewConfig() - - db := c.Db() - - assert.IsType(t, &gorm.DB{}, db) -} diff --git a/internal/photoprism/converter.go b/internal/photoprism/converter.go index 194bd7ab9..ff7429436 100644 --- a/internal/photoprism/converter.go +++ b/internal/photoprism/converter.go @@ -32,7 +32,7 @@ func (c *Converter) ConvertAll(path string) { err := filepath.Walk(path, func(filename string, fileInfo os.FileInfo, err error) error { if err != nil { - log.Error(err.Error()) + log.Error("Walk", err.Error()) return nil } @@ -47,7 +47,7 @@ func (c *Converter) ConvertAll(path string) { } if _, err := c.ConvertToJpeg(mediaFile); err != nil { - log.Error(err.Error()) + log.Warnf("file could not be converted to JPEG: \"%s\"", filename) } return nil @@ -78,7 +78,7 @@ func (c *Converter) ConvertToJpeg(image *MediaFile) (*MediaFile, error) { return mediaFile, nil } - log.Errorf("converting \"%s\" to \"%s\"", image.filename, jpegFilename) + log.Infof("converting \"%s\" to \"%s\"", image.filename, jpegFilename) xmpFilename := baseFilename + ".xmp" diff --git a/internal/photoprism/converter_test.go b/internal/photoprism/converter_test.go index 32ee0abdd..2c044a94e 100644 --- a/internal/photoprism/converter_test.go +++ b/internal/photoprism/converter_test.go @@ -4,15 +4,15 @@ import ( "os" "testing" + "github.com/photoprism/photoprism/internal/context" "github.com/photoprism/photoprism/internal/fsutil" - "github.com/photoprism/photoprism/internal/test" "github.com/stretchr/testify/assert" ) func TestNewConverter(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - converter := NewConverter(conf.DarktableCli()) + converter := NewConverter(ctx.DarktableCli()) assert.IsType(t, &Converter{}, converter) } @@ -22,13 +22,13 @@ func TestConverter_ConvertToJpeg(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - converter := NewConverter(conf.DarktableCli()) + converter := NewConverter(ctx.DarktableCli()) - jpegFilename := conf.ImportPath() + "/iphone/IMG_6788.JPG" + jpegFilename := ctx.ImportPath() + "/iphone/IMG_6788.JPG" assert.Truef(t, fsutil.Exists(jpegFilename), "file does not exist: %s", jpegFilename) @@ -52,7 +52,7 @@ func TestConverter_ConvertToJpeg(t *testing.T) { assert.Equal(t, "iPhone SE", infoJpeg.CameraModel) - rawFilemame := conf.ImportPath() + "/raw/IMG_1435.CR2" + rawFilemame := ctx.ImportPath() + "/raw/IMG_1435.CR2" t.Logf("Testing RAW to JPEG converter with %s", rawFilemame) @@ -62,7 +62,7 @@ func TestConverter_ConvertToJpeg(t *testing.T) { imageRaw, _ := converter.ConvertToJpeg(rawMediaFile) - assert.True(t, fsutil.Exists(conf.ImportPath()+"/raw/IMG_1435.jpg"), "Jpeg file was not found - is Darktable installed?") + assert.True(t, fsutil.Exists(ctx.ImportPath()+"/raw/IMG_1435.jpg"), "Jpeg file was not found - is Darktable installed?") assert.NotEqual(t, rawFilemame, imageRaw.filename) @@ -78,15 +78,15 @@ func TestConverter_ConvertAll(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - converter := NewConverter(conf.DarktableCli()) + converter := NewConverter(ctx.DarktableCli()) - converter.ConvertAll(conf.ImportPath()) + converter.ConvertAll(ctx.ImportPath()) - jpegFilename := conf.ImportPath() + "/raw/IMG_1435.jpg" + jpegFilename := ctx.ImportPath() + "/raw/IMG_1435.jpg" assert.True(t, fsutil.Exists(jpegFilename), "Jpeg file was not found - is Darktable installed?") @@ -102,13 +102,13 @@ func TestConverter_ConvertAll(t *testing.T) { assert.Equal(t, "Canon EOS M10", infoRaw.CameraModel, "Camera model should be Canon EOS M10") - existingJpegFilename := conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg" + existingJpegFilename := ctx.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg" oldHash := fsutil.Hash(existingJpegFilename) os.Remove(existingJpegFilename) - converter.ConvertAll(conf.ImportPath()) + converter.ConvertAll(ctx.ImportPath()) newHash := fsutil.Hash(existingJpegFilename) diff --git a/internal/photoprism/exif.go b/internal/photoprism/exif.go index 221e5d3e4..cc77c1c56 100644 --- a/internal/photoprism/exif.go +++ b/internal/photoprism/exif.go @@ -2,6 +2,7 @@ package photoprism import ( "errors" + "fmt" "math" "strings" "time" @@ -32,7 +33,7 @@ type ExifData struct { // ExifData return ExifData given a single mediaFile. func (m *MediaFile) ExifData() (*ExifData, error) { if m == nil { - return nil, errors.New("media file is null") + return nil, errors.New("can't parse Exif data: file instance is null") } if m.exifData != nil { @@ -40,7 +41,7 @@ func (m *MediaFile) ExifData() (*ExifData, error) { } if !m.IsPhoto() { - return nil, errors.New("not a JPEG or Raw file") + return nil, errors.New(fmt.Sprintf("file not compatible with Exif: \"%s\"", m.Filename())) } m.exifData = &ExifData{} diff --git a/internal/photoprism/exif_test.go b/internal/photoprism/exif_test.go index faa47af63..fbcc99c17 100644 --- a/internal/photoprism/exif_test.go +++ b/internal/photoprism/exif_test.go @@ -3,16 +3,16 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestMediaFile_GetExifData(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - image1, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + image1, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) @@ -30,11 +30,11 @@ func TestMediaFile_GetExifData_Slow(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - image2, err := NewMediaFile(conf.ImportPath() + "/raw/IMG_1435.CR2") + image2, err := NewMediaFile(ctx.ImportPath() + "/raw/IMG_1435.CR2") assert.Nil(t, err) diff --git a/internal/photoprism/export.go b/internal/photoprism/export.go index 6580c918c..29915c469 100644 --- a/internal/photoprism/export.go +++ b/internal/photoprism/export.go @@ -10,7 +10,7 @@ import ( log "github.com/sirupsen/logrus" ) -// FindOriginalsByDate searches the originalsPath given a time frame in the format of +// FindOriginalsByDate searches the OriginalsPath given a time frame in the format of // after <=> before and returns a list of results. func FindOriginalsByDate(originalsPath string, after time.Time, before time.Time) (result []*MediaFile) { filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error { diff --git a/internal/photoprism/importer_test.go b/internal/photoprism/importer_test.go index 6d35df7db..993230f84 100644 --- a/internal/photoprism/importer_test.go +++ b/internal/photoprism/importer_test.go @@ -3,37 +3,38 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestNewImporter(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - indexer := NewIndexer(conf.OriginalsPath(), tensorFlow, conf.Db()) + indexer := NewIndexer(ctx.OriginalsPath(), tensorFlow, ctx.Db()) - converter := NewConverter(conf.DarktableCli()) + converter := NewConverter(ctx.DarktableCli()) - importer := NewImporter(conf.OriginalsPath(), indexer, converter) + importer := NewImporter(ctx.OriginalsPath(), indexer, converter) assert.IsType(t, &Importer{}, importer) } func TestImporter_GetDestinationFilename(t *testing.T) { - conf := test.NewConfig() - conf.InitializeTestData(t) + ctx := context.TestContext() - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + ctx.InitializeTestData(t) - indexer := NewIndexer(conf.OriginalsPath(), tensorFlow, conf.Db()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - converter := NewConverter(conf.DarktableCli()) + indexer := NewIndexer(ctx.OriginalsPath(), tensorFlow, ctx.Db()) - importer := NewImporter(conf.OriginalsPath(), indexer, converter) + converter := NewConverter(ctx.DarktableCli()) - rawFile, err := NewMediaFile(conf.ImportPath() + "/raw/IMG_1435.CR2") + importer := NewImporter(ctx.OriginalsPath(), indexer, converter) + + rawFile, err := NewMediaFile(ctx.ImportPath() + "/raw/IMG_1435.CR2") assert.Nil(t, err) @@ -41,7 +42,7 @@ func TestImporter_GetDestinationFilename(t *testing.T) { assert.Nil(t, err) - assert.Equal(t, conf.OriginalsPath()+"/2018/02/20180204_170813_863A6248DCCA.cr2", filename) + assert.Equal(t, ctx.OriginalsPath()+"/2018/02/20180204_170813_863A6248DCCA.cr2", filename) } func TestImporter_ImportPhotosFromDirectory(t *testing.T) { @@ -49,17 +50,17 @@ func TestImporter_ImportPhotosFromDirectory(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - indexer := NewIndexer(conf.OriginalsPath(), tensorFlow, conf.Db()) + indexer := NewIndexer(ctx.OriginalsPath(), tensorFlow, ctx.Db()) - converter := NewConverter(conf.DarktableCli()) + converter := NewConverter(ctx.DarktableCli()) - importer := NewImporter(conf.OriginalsPath(), indexer, converter) + importer := NewImporter(ctx.OriginalsPath(), indexer, converter) - importer.ImportPhotosFromDirectory(conf.ImportPath()) + importer.ImportPhotosFromDirectory(ctx.ImportPath()) } diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go index 99accfc61..ebfb48d53 100644 --- a/internal/photoprism/mediafile.go +++ b/internal/photoprism/mediafile.go @@ -542,12 +542,14 @@ func (m *MediaFile) decodeDimensions() error { m.width = size.Width m.height = size.Height } else { - if exif, err := m.ExifData(); err == nil { - m.width = exif.Width - m.height = exif.Height - } else { + exif, err := m.ExifData() + + if err != nil { return err } + + m.width = exif.Width + m.height = exif.Height } return nil @@ -555,6 +557,10 @@ func (m *MediaFile) decodeDimensions() error { // Width return the width dimension of a mediafile. func (m *MediaFile) Width() int { + if !m.IsPhoto() { + return 0 + } + if m.width <= 0 { if err := m.decodeDimensions(); err != nil { log.Error(err) @@ -566,6 +572,10 @@ func (m *MediaFile) Width() int { // Height returns the height dimension of a mediafile. func (m *MediaFile) Height() int { + if !m.IsPhoto() { + return 0 + } + if m.height <= 0 { if err := m.decodeDimensions(); err != nil { log.Error(err) diff --git a/internal/photoprism/mediafile_test.go b/internal/photoprism/mediafile_test.go index 846554c99..6fd4d0c17 100644 --- a/internal/photoprism/mediafile_test.go +++ b/internal/photoprism/mediafile_test.go @@ -3,20 +3,20 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestMediaFile_GetRelatedFiles(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - mediaFile, err := NewMediaFile(conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.cr2") + mediaFile, err := NewMediaFile(ctx.ImportPath() + "/raw/20140717_154212_1EC48F8489.cr2") assert.Nil(t, err) - expectedBaseFilename := conf.ImportPath() + "/raw/20140717_154212_1EC48F8489" + expectedBaseFilename := ctx.ImportPath() + "/raw/20140717_154212_1EC48F8489" related, _, err := mediaFile.RelatedFiles() @@ -38,11 +38,11 @@ func TestMediaFile_GetRelatedFiles(t *testing.T) { } func TestMediaFile_GetRelatedFiles_Ordering(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - mediaFile, err := NewMediaFile(conf.ImportPath() + "/20130203_193332_0AE340D280.jpg") + mediaFile, err := NewMediaFile(ctx.ImportPath() + "/20130203_193332_0AE340D280.jpg") assert.Nil(t, err) @@ -59,13 +59,13 @@ func TestMediaFile_GetRelatedFiles_Ordering(t *testing.T) { } func TestMediaFile_GetEditedFilename(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - mediaFile1, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + mediaFile1, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) - assert.Equal(t, conf.ImportPath()+"/iphone/IMG_E6788.JPG", mediaFile1.EditedFilename()) + assert.Equal(t, ctx.ImportPath()+"/iphone/IMG_E6788.JPG", mediaFile1.EditedFilename()) /* TODO: Add example files to import.zip mediaFile2, err := NewMediaFile("/foo/bar/IMG_E1234.jpg") @@ -73,34 +73,34 @@ func TestMediaFile_GetEditedFilename(t *testing.T) { assert.Equal(t, "", mediaFile2.EditedFilename()) */ - mediaFile3, err := NewMediaFile(conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg") + mediaFile3, err := NewMediaFile(ctx.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg") assert.Nil(t, err) assert.Equal(t, "", mediaFile3.EditedFilename()) } func TestMediaFile_GetMimeType(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - image1, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + image1, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) assert.Equal(t, "image/jpeg", image1.MimeType()) - image2, err := NewMediaFile(conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.cr2") + image2, err := NewMediaFile(ctx.ImportPath() + "/raw/20140717_154212_1EC48F8489.cr2") assert.Nil(t, err) assert.Equal(t, "application/octet-stream", image2.MimeType()) } func TestMediaFile_Exists(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - mediaFile, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + mediaFile, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) assert.NotNil(t, mediaFile) assert.True(t, mediaFile.Exists()) - mediaFile, err = NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788_XYZ.JPG") + mediaFile, err = NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788_XYZ.JPG") assert.NotNil(t, err) assert.Nil(t, mediaFile) } diff --git a/internal/photoprism/search_test.go b/internal/photoprism/search_test.go index 741666a8b..a0764cf3d 100644 --- a/internal/photoprism/search_test.go +++ b/internal/photoprism/search_test.go @@ -3,18 +3,18 @@ package photoprism import ( "testing" + "github.com/photoprism/photoprism/internal/context" "github.com/photoprism/photoprism/internal/forms" - "github.com/photoprism/photoprism/internal/test" ) func TestSearch_Photos_Query(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.CreateDirectories() + ctx.CreateDirectories() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - search := NewSearch(conf.OriginalsPath(), conf.Db()) + search := NewSearch(ctx.OriginalsPath(), ctx.Db()) var form forms.PhotoSearchForm @@ -41,13 +41,13 @@ func TestSearch_Photos_Query(t *testing.T) { } func TestSearch_Photos_Camera(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.CreateDirectories() + ctx.CreateDirectories() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - search := NewSearch(conf.OriginalsPath(), conf.Db()) + search := NewSearch(ctx.OriginalsPath(), ctx.Db()) var form forms.PhotoSearchForm diff --git a/internal/photoprism/tensorflow_test.go b/internal/photoprism/tensorflow_test.go index 3eece8b22..438b452ec 100644 --- a/internal/photoprism/tensorflow_test.go +++ b/internal/photoprism/tensorflow_test.go @@ -4,18 +4,18 @@ import ( "io/ioutil" "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestTensorFlow_GetImageTagsFromFile(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - result, err := tensorFlow.GetImageTagsFromFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + result, err := tensorFlow.GetImageTagsFromFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) @@ -42,13 +42,13 @@ func TestTensorFlow_GetImageTags(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - if imageBuffer, err := ioutil.ReadFile(conf.ImportPath() + "/iphone/IMG_6788.JPG"); err != nil { + if imageBuffer, err := ioutil.ReadFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG"); err != nil { t.Error(err) } else { result, err := tensorFlow.GetImageTags(imageBuffer) @@ -74,13 +74,13 @@ func TestTensorFlow_GetImageTags_Dog(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - if imageBuffer, err := ioutil.ReadFile(conf.ImportPath() + "/dog.jpg"); err != nil { + if imageBuffer, err := ioutil.ReadFile(ctx.ImportPath() + "/dog.jpg"); err != nil { t.Error(err) } else { result, err := tensorFlow.GetImageTags(imageBuffer) diff --git a/internal/photoprism/thumbnails.go b/internal/photoprism/thumbnails.go index 1a6321655..739045dbc 100644 --- a/internal/photoprism/thumbnails.go +++ b/internal/photoprism/thumbnails.go @@ -29,13 +29,13 @@ func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, if thumbnail, err := mediaFile.SquareThumbnail(thumbnailsPath, size); err != nil { log.Errorf("could not create thumbnail: %s", err.Error()) } else { - log.Infof("created %dx%d px thumbnail for \"%s\"", thumbnail.Width(), thumbnail.Height(), mediaFile.RelativeFilename(originalsPath)) + log.Infof("created %dx%d thumbnail for \"%s\"", thumbnail.Width(), thumbnail.Height(), mediaFile.RelativeFilename(originalsPath)) } } else { if thumbnail, err := mediaFile.Thumbnail(thumbnailsPath, size); err != nil { log.Errorf("could not create thumbnail: %s", err.Error()) } else { - log.Infof("created %dx%d px thumbnail for \"%s\"", thumbnail.Width(), thumbnail.Height(), mediaFile.RelativeFilename(originalsPath)) + log.Infof("created %dx%d thumbnail for \"%s\"", thumbnail.Width(), thumbnail.Height(), mediaFile.RelativeFilename(originalsPath)) } } diff --git a/internal/photoprism/thumbnails_test.go b/internal/photoprism/thumbnails_test.go index 3f19e49b9..376d7fe55 100644 --- a/internal/photoprism/thumbnails_test.go +++ b/internal/photoprism/thumbnails_test.go @@ -3,21 +3,21 @@ package photoprism import ( "testing" - "github.com/photoprism/photoprism/internal/test" + "github.com/photoprism/photoprism/internal/context" "github.com/stretchr/testify/assert" ) func TestMediaFile_GetThumbnail(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.CreateDirectories() + ctx.CreateDirectories() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - image1, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + image1, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) - thumbnail1, err := image1.Thumbnail(conf.ThumbnailsPath(), 350) + thumbnail1, err := image1.Thumbnail(ctx.ThumbnailsPath(), 350) assert.Empty(t, err) @@ -25,16 +25,16 @@ func TestMediaFile_GetThumbnail(t *testing.T) { } func TestMediaFile_GetSquareThumbnail(t *testing.T) { - conf := test.NewConfig() + ctx := context.TestContext() - conf.CreateDirectories() + ctx.CreateDirectories() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - image1, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG") + image1, err := NewMediaFile(ctx.ImportPath() + "/iphone/IMG_6788.JPG") assert.Nil(t, err) - thumbnail1, err := image1.SquareThumbnail(conf.ThumbnailsPath(), 350) + thumbnail1, err := image1.SquareThumbnail(ctx.ThumbnailsPath(), 350) assert.Empty(t, err) @@ -46,23 +46,23 @@ func TestCreateThumbnailsFromOriginals(t *testing.T) { t.Skip("skipping test in short mode.") } - conf := test.NewConfig() + ctx := context.TestContext() - conf.CreateDirectories() + ctx.CreateDirectories() - conf.InitializeTestData(t) + ctx.InitializeTestData(t) - tensorFlow := NewTensorFlow(conf.TensorFlowModelPath()) + tensorFlow := NewTensorFlow(ctx.TensorFlowModelPath()) - indexer := NewIndexer(conf.OriginalsPath(), tensorFlow, conf.Db()) + indexer := NewIndexer(ctx.OriginalsPath(), tensorFlow, ctx.Db()) - converter := NewConverter(conf.DarktableCli()) + converter := NewConverter(ctx.DarktableCli()) - importer := NewImporter(conf.OriginalsPath(), indexer, converter) + importer := NewImporter(ctx.OriginalsPath(), indexer, converter) - importer.ImportPhotosFromDirectory(conf.ImportPath()) + importer.ImportPhotosFromDirectory(ctx.ImportPath()) - CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), 600, false) + CreateThumbnailsFromOriginals(ctx.OriginalsPath(), ctx.ThumbnailsPath(), 600, false) - CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), 300, true) + CreateThumbnailsFromOriginals(ctx.OriginalsPath(), ctx.ThumbnailsPath(), 300, true) } diff --git a/internal/server/routes.go b/internal/server/routes.go index 7f30c6c30..c07fbd72b 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -5,27 +5,27 @@ import ( "github.com/gin-gonic/gin" "github.com/photoprism/photoprism/internal/api" - "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/context" ) -func registerRoutes(app *gin.Engine, conf photoprism.Config) { +func registerRoutes(app *gin.Engine, ctx *context.Context) { // Favicon - app.StaticFile("/favicon.ico", conf.HttpFaviconsPath()+"/favicon.ico") + app.StaticFile("/favicon.ico", ctx.HttpFaviconsPath()+"/favicon.ico") // Static assets like js and css files - app.Static("/assets", conf.HttpPublicPath()) + app.Static("/assets", ctx.HttpPublicPath()) // JSON-REST API Version 1 v1 := app.Group("/api/v1") { - api.GetPhotos(v1, conf) - api.GetThumbnail(v1, conf) - api.LikePhoto(v1, conf) - api.DislikePhoto(v1, conf) + api.GetPhotos(v1, ctx) + api.GetThumbnail(v1, ctx) + api.LikePhoto(v1, ctx) + api.DislikePhoto(v1, ctx) } // Default HTML page (client-side routing implemented via Vue.js) app.NoRoute(func(c *gin.Context) { - c.HTML(http.StatusOK, "index.tmpl", conf.ClientConfig()) + c.HTML(http.StatusOK, "index.tmpl", ctx.ClientConfig()) }) } diff --git a/internal/server/server.go b/internal/server/server.go index 14b5743aa..61d4b0844 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,15 +4,15 @@ import ( "fmt" "github.com/gin-gonic/gin" - "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/context" log "github.com/sirupsen/logrus" ) // Start the REST API server using the configuration provided -func Start(conf photoprism.Config) { - if conf.HttpServerMode() != "" { - gin.SetMode(conf.HttpServerMode()) - } else if conf.Debug() == false { +func Start(ctx *context.Context) { + if ctx.HttpServerMode() != "" { + gin.SetMode(ctx.HttpServerMode()) + } else if ctx.Debug() == false { gin.SetMode(gin.ReleaseMode) } @@ -20,11 +20,11 @@ func Start(conf photoprism.Config) { app.Use(gin.Logger(), gin.Recovery()) // Set template directory - app.LoadHTMLGlob(conf.HttpTemplatesPath() + "/*") + app.LoadHTMLGlob(ctx.HttpTemplatesPath() + "/*") - registerRoutes(app, conf) + registerRoutes(app, ctx) - if err := app.Run(fmt.Sprintf("%s:%d", conf.HttpServerHost(), conf.HttpServerPort())); err != nil { + if err := app.Run(fmt.Sprintf("%s:%d", ctx.HttpServerHost(), ctx.HttpServerPort())); err != nil { log.Error(err) } } diff --git a/internal/test/config.go b/internal/test/config.go deleted file mode 100644 index 246a02425..000000000 --- a/internal/test/config.go +++ /dev/null @@ -1,356 +0,0 @@ -package test - -import ( - "fmt" - "os" - "testing" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/mysql" - _ "github.com/jinzhu/gorm/dialects/sqlite" - "github.com/photoprism/photoprism/internal/frontend" - "github.com/photoprism/photoprism/internal/fsutil" - "github.com/photoprism/photoprism/internal/models" -) - -const ( - DataURL = "https://dl.photoprism.org/fixtures/test.zip" - DataHash = "1a59b358b80221ab3e76efb683ad72402f0b0844" - ConfigFile = "../../configs/photoprism.yml" -) - -var DarktableCli = "/usr/bin/darktable-cli" -var DataZip = "/tmp/photoprism/testdata.zip" -var AssetsPath = fsutil.ExpandedFilename("../../assets") -var DataPath = AssetsPath + "/testdata" -var CachePath = fsutil.ExpandedFilename(DataPath + "/cache") -var OriginalsPath = fsutil.ExpandedFilename(DataPath + "/originals") -var ImportPath = fsutil.ExpandedFilename(DataPath + "/import") -var ExportPath = fsutil.ExpandedFilename(DataPath + "/export") -var DatabaseDriver = "mysql" -var DatabaseDsn = "photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true" - -func init() { - conf := NewConfig() - conf.MigrateDb() -} - -type Config struct { - db *gorm.DB -} - -func (c *Config) RemoveTestData(t *testing.T) { - os.RemoveAll(c.ImportPath()) - os.RemoveAll(c.ExportPath()) - os.RemoveAll(c.OriginalsPath()) - os.RemoveAll(c.CachePath()) -} - -func (c *Config) DownloadTestData(t *testing.T) { - if fsutil.Exists(DataZip) { - hash := fsutil.Hash(DataZip) - - if hash != DataHash { - os.Remove(DataZip) - t.Logf("Removed outdated test data zip file (fingerprint %s)\n", hash) - } - } - - if !fsutil.Exists(DataZip) { - fmt.Printf("Downloading latest test data zip file from %s\n", DataURL) - - if err := fsutil.Download(DataZip, DataURL); err != nil { - fmt.Printf("Download failed: %s\n", err.Error()) - } - } -} - -func (c *Config) UnzipTestData(t *testing.T) { - if _, err := fsutil.Unzip(DataZip, DataPath); err != nil { - t.Logf("Could not unzip test data: %s\n", err.Error()) - } -} - -func (c *Config) InitializeTestData(t *testing.T) { - t.Log("Initializing test data") - - c.RemoveTestData(t) - - c.DownloadTestData(t) - - c.UnzipTestData(t) -} - -func NewConfig() *Config { - return &Config{} -} - -// CreateDirectories creates all the folders that photoprism needs. These are: -// OriginalsPath -// ThumbnailsPath -// ImportPath -// ExportPath -func (c *Config) CreateDirectories() error { - if err := os.MkdirAll(c.OriginalsPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.ImportPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.ExportPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.ThumbnailsPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.SqlServerPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.TensorFlowModelPath(), os.ModePerm); err != nil { - return err - } - - if err := os.MkdirAll(c.HttpPublicBuildPath(), os.ModePerm); err != nil { - return err - } - - return nil -} - -// connectToDatabase estabilishes a connection to a database given a driver. -// It tries to do this 12 times with a 5 second sleep intervall in between. -func (c *Config) connectToDatabase() error { - dbDriver := c.DatabaseDriver() - dbDsn := c.DatabaseDsn() - - db, err := gorm.Open(dbDriver, dbDsn) - - if err != nil || db == nil { - for i := 1; i <= 12; i++ { - time.Sleep(5 * time.Second) - - db, err = gorm.Open(dbDriver, dbDsn) - - if db != nil && err == nil { - break - } - } - - if err != nil || db == nil { - log.Fatal(err) - } - } - - c.db = db - - return err -} - -// AppName returns the application name. -func (c *Config) AppName() string { - return "PhotoPrism" -} - -// AppVersion returns the application version. -func (c *Config) AppVersion() string { - return "DEVELOPMENT" -} - -// AppCopyright returns the application copyright. -func (c *Config) AppCopyright() string { - return "The PhotoPrism contributors " -} - -// Debug returns true if debug mode is on. -func (c *Config) Debug() bool { - return false -} - -// LogLevel returns the logrus log level. -func (c *Config) LogLevel() log.Level { - return log.DebugLevel -} - -// ConfigFile returns the config file name. -func (c *Config) ConfigFile() string { - return ConfigFile -} - -// HttpServerHost returns the server IP address (empty for all). -func (c *Config) HttpServerHost() string { - return "127.0.0.1" -} - -// HttpServerPort returns the server port. -func (c *Config) HttpServerPort() int { - return 80 -} - -// SqlServerHost returns the database server IP address (empty for all). -func (c *Config) SqlServerHost() string { - return "127.0.0.1" -} - -// SqlServerPort returns the database server port. -func (c *Config) SqlServerPort() uint { - return 4001 -} - -// SqlServerPassword returns the password for the built-in database server. -func (c *Config) SqlServerPassword() string { - return "photoprism" -} - -// HttpServerMode returns the server mode. -func (c *Config) HttpServerMode() string { - return "test" -} - -// HttpServerPassword returns the password for the Web UI. -func (c *Config) HttpServerPassword() string { - return "" -} - -// OriginalsPath returns the originals. -func (c *Config) OriginalsPath() string { - return OriginalsPath -} - -// ImportPath returns the import directory. -func (c *Config) ImportPath() string { - return ImportPath -} - -// ExportPath returns the export directory. -func (c *Config) ExportPath() string { - return ExportPath -} - -// DarktableCli returns the darktable-cli binary file name. -func (c *Config) DarktableCli() string { - return DarktableCli -} - -// DatabaseDriver returns the database driver name. -func (c *Config) DatabaseDriver() string { - return DatabaseDriver -} - -// DatabaseDsn returns the database data source name (DSN). -func (c *Config) DatabaseDsn() string { - return DatabaseDsn -} - -// CachePath returns the path to the cache. -func (c *Config) CachePath() string { - return CachePath -} - -// ThumbnailsPath returns the path to the cached thumbnails. -func (c *Config) ThumbnailsPath() string { - return c.CachePath() + "/thumbnails" -} - -// AssetsPath returns the path to the assets. -func (c *Config) AssetsPath() string { - return AssetsPath -} - -// TensorFlowModelPath returns the tensorflow model path. -func (c *Config) TensorFlowModelPath() string { - return c.AssetsPath() + "/tensorflow" -} - -// SqlServerPath returns the database storage path (e.g. for SQLite or Bleve). -func (c *Config) SqlServerPath() string { - return c.ServerPath() + "/database" -} - -// ServerPath returns the server assets path (public files, favicons, templates,...). -func (c *Config) ServerPath() string { - return c.AssetsPath() + "/server" -} - -// HttpTemplatesPath returns the server templates path. -func (c *Config) HttpTemplatesPath() string { - return c.ServerPath() + "/templates" -} - -// HttpFaviconsPath returns the favicons path. -func (c *Config) HttpFaviconsPath() string { - return c.HttpPublicPath() + "/favicons" -} - -// HttpPublicPath returns the public server path (//server/assets/*). -func (c *Config) HttpPublicPath() string { - return c.ServerPath() + "/public" -} - -// HttpPublicBuildPath returns the public build path (//server/assets/build/*). -func (c *Config) HttpPublicBuildPath() string { - return c.HttpPublicPath() + "/build" -} - -// Db gets a db connection. If it already is estabilished it will return that. -func (c *Config) Db() *gorm.DB { - if c.db == nil { - c.connectToDatabase() - } - - return c.db -} - -// MigrateDb will start a migration process. -func (c *Config) MigrateDb() { - db := c.Db() - - db.AutoMigrate(&models.File{}, - &models.Photo{}, - &models.Tag{}, - &models.Album{}, - &models.Location{}, - &models.Camera{}, - &models.Lens{}, - &models.Country{}) -} - -// ClientConfig returns a loaded and set configuration entity. -func (c *Config) ClientConfig() frontend.Config { - db := c.Db() - - var cameras []*models.Camera - - type country struct { - LocCountry string - LocCountryCode string - } - - var countries []country - - db.Model(&models.Location{}).Select("DISTINCT loc_country_code, loc_country").Scan(&countries) - - db.Where("deleted_at IS NULL").Limit(1000).Order("camera_model").Find(&cameras) - - jsHash := fsutil.Hash(c.HttpPublicBuildPath() + "/app.js") - cssHash := fsutil.Hash(c.HttpPublicBuildPath() + "/app.css") - - result := frontend.Config{ - "appName": c.AppName(), - "appVersion": c.AppVersion(), - "debug": c.Debug(), - "cameras": cameras, - "countries": countries, - "jsHash": jsHash, - "cssHash": cssHash, - } - - return result -} diff --git a/internal/test/config_test.go b/internal/test/config_test.go deleted file mode 100644 index 10c5d460e..000000000 --- a/internal/test/config_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package test - -import ( - "testing" - - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" -) - -func TestNewConfig(t *testing.T) { - c := NewConfig() - - assert.IsType(t, new(Config), c) - - assert.Equal(t, AssetsPath, c.AssetsPath()) - assert.False(t, c.Debug()) -} - -func TestConfig_ConnectToDatabase(t *testing.T) { - c := NewConfig() - - db := c.Db() - - assert.IsType(t, &gorm.DB{}, db) -} diff --git a/internal/test/context.go b/internal/test/context.go deleted file mode 100644 index f003141c4..000000000 --- a/internal/test/context.go +++ /dev/null @@ -1,28 +0,0 @@ -package test - -import ( - "flag" - - "github.com/urfave/cli" -) - -// Returns example cli context for testing -func CliContext() *cli.Context { - globalSet := flag.NewFlagSet("test", 0) - globalSet.Bool("debug", false, "doc") - globalSet.String("config-file", ConfigFile, "doc") - globalSet.String("assets-path", AssetsPath, "doc") - globalSet.String("originals-path", OriginalsPath, "doc") - globalSet.String("darktable-cli", DarktableCli, "doc") - - app := cli.NewApp() - - c := cli.NewContext(app, globalSet, nil) - - c.Set("config-file", ConfigFile) - c.Set("assets-path", AssetsPath) - c.Set("originals-path", OriginalsPath) - c.Set("darktable-cli", DarktableCli) - - return c -} diff --git a/internal/test/context_test.go b/internal/test/context_test.go deleted file mode 100644 index 57a0b3fa4..000000000 --- a/internal/test/context_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/urfave/cli" -) - -func TestCliContext(t *testing.T) { - result := CliContext() - - assert.IsType(t, new(cli.Context), result) -} diff --git a/internal/test/doc.go b/internal/test/doc.go deleted file mode 100644 index a1c524d04..000000000 --- a/internal/test/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -/* -Package context contains test configuration. - -Additional information can be found in our Developer Guide: - -https://github.com/photoprism/photoprism/wiki -*/ -package test