From 107da91700f5d1d81304140874087bbc95b99f4c Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Sun, 17 Jan 2021 16:50:48 +0100
Subject: [PATCH] Refactor handling of environment variables

---
 .gitignore                               |  2 +-
 api/database/database.go                 | 24 +++++++-----
 api/database/drivers/database_drivers.go |  6 ++-
 api/example.env                          |  3 ++
 api/graphql/resolvers/album.go           |  4 ++
 api/graphql/resolvers/user.go            |  2 +-
 api/routes/photos.go                     |  2 +-
 api/routes/videos.go                     |  2 +-
 api/scanner/cleanup_media.go             |  4 +-
 api/scanner/process_photo.go             |  6 +--
 api/scanner/scanner_user.go              |  7 ++--
 api/server.go                            |  7 ++--
 api/utils/Endpoints.go                   | 25 ++++++------
 api/utils/environment_variables.go       | 50 ++++++++++++++++++++++++
 docker-compose.example.yml               |  2 +-
 15 files changed, 105 insertions(+), 41 deletions(-)
 create mode 100644 api/utils/environment_variables.go

diff --git a/.gitignore b/.gitignore
index 20f06d0..3418fec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
 # See https://help.github.com/ignore-files/ for more about ignoring files.
 
 cache/
-photo_cache/
+media_cache/
 /photos_path
 photoview.db
 
diff --git a/api/database/database.go b/api/database/database.go
index 78d29d4..1a11c50 100644
--- a/api/database/database.go
+++ b/api/database/database.go
@@ -2,13 +2,14 @@ package database
 
 import (
 	"context"
+	"fmt"
 	"log"
 	"net/url"
-	"os"
 	"time"
 
 	"github.com/photoview/photoview/api/database/drivers"
 	"github.com/photoview/photoview/api/graphql/models"
+	"github.com/photoview/photoview/api/utils"
 	"github.com/pkg/errors"
 
 	"gorm.io/driver/mysql"
@@ -18,13 +19,14 @@ import (
 )
 
 func getMysqlAddress() (*url.URL, error) {
-	address, err := url.Parse(os.Getenv("PHOTOVIEW_MYSQL_URL"))
-	if err != nil {
-		return nil, errors.Wrap(err, "Could not parse mysql url")
+	addressString := utils.EnvMysqlURL.GetValue()
+	if addressString == "" {
+		return nil, errors.New(fmt.Sprintf("Environment variable %s missing, exiting", utils.EnvMysqlURL.GetName()))
 	}
 
-	if address.String() == "" {
-		return nil, errors.New("Environment variable PHOTOVIEW_MYSQL_URL missing, exiting")
+	address, err := url.Parse(addressString)
+	if err != nil {
+		return nil, errors.Wrap(err, "Could not parse mysql url")
 	}
 
 	queryValues := address.Query()
@@ -36,7 +38,7 @@ func getMysqlAddress() (*url.URL, error) {
 }
 
 func getSqliteAddress() (*url.URL, error) {
-	path := os.Getenv("PHOTOVIEW_SQLITE_PATH")
+	path := utils.EnvSqlitePath.GetValue()
 	if path == "" {
 		path = "photoview.db"
 	}
@@ -62,8 +64,12 @@ func SetupDatabase() (*gorm.DB, error) {
 
 	config := gorm.Config{}
 
-	// Enable database debug logging
-	config.Logger = logger.Default.LogMode(logger.Info)
+	// Configure database logging
+	if utils.DevelopmentMode() {
+		config.Logger = logger.Default.LogMode(logger.Info)
+	} else {
+		config.Logger = logger.Default.LogMode(logger.Warn)
+	}
 
 	var databaseDialect gorm.Dialector
 	switch drivers.DatabaseDriver() {
diff --git a/api/database/drivers/database_drivers.go b/api/database/drivers/database_drivers.go
index a4e1211..eab45ef 100644
--- a/api/database/drivers/database_drivers.go
+++ b/api/database/drivers/database_drivers.go
@@ -1,10 +1,12 @@
 package drivers
 
 import (
-	"os"
 	"strings"
+
+	"github.com/photoview/photoview/api/utils"
 )
 
+// DatabaseDriverType represents the name of a database driver
 type DatabaseDriverType string
 
 const (
@@ -15,7 +17,7 @@ const (
 func DatabaseDriver() DatabaseDriverType {
 
 	var driver DatabaseDriverType
-	driverString := strings.ToLower(os.Getenv("PHOTOVIEW_DATABASE_DRIVER"))
+	driverString := strings.ToLower(utils.EnvDatabaseDriver.GetValue())
 
 	switch driverString {
 	case "mysql":
diff --git a/api/example.env b/api/example.env
index 7e6a25a..27227f7 100644
--- a/api/example.env
+++ b/api/example.env
@@ -13,6 +13,9 @@ PHOTOVIEW_LISTEN_PORT=4001
 PHOTOVIEW_API_ENDPOINT=http://localhost:4001/
 PHOTOVIEW_UI_ENDPOINT=http://localhost:1234/
 
+# Path where media should be cached, defaults to ./media_cache
+# PHOTOVIEW_MEDIA_CACHE=./media_cache
+
 # Set to 1 for the server to also serve the built static ui files
 PHOTOVIEW_SERVE_UI=0
 # When PHOTOVIEW_SERVE_UI is 1, PHOTOVIEW_PUBLIC_ENDPOINT is used instead of PHOTOVIEW_API_ENDPOINT and PHOTOVIEW_UI_ENDPOINT
diff --git a/api/graphql/resolvers/album.go b/api/graphql/resolvers/album.go
index c19998e..b6a6a82 100644
--- a/api/graphql/resolvers/album.go
+++ b/api/graphql/resolvers/album.go
@@ -20,6 +20,10 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onl
 		return nil, err
 	}
 
+	if len(user.Albums) == 0 {
+		return nil, nil
+	}
+
 	userAlbumIDs := make([]int, len(user.Albums))
 	for i, album := range user.Albums {
 		userAlbumIDs[i] = album.ID
diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go
index 67ae200..d7ed591 100644
--- a/api/graphql/resolvers/user.go
+++ b/api/graphql/resolvers/user.go
@@ -335,7 +335,7 @@ func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int,
 	if deletedAlbumIDs != nil {
 		// Delete albums from cache
 		for _, id := range deletedAlbumIDs {
-			cacheAlbumPath := path.Join(scanner.PhotoCache(), strconv.Itoa(id))
+			cacheAlbumPath := path.Join(scanner.MediaCachePath(), strconv.Itoa(id))
 
 			if err := os.RemoveAll(cacheAlbumPath); err != nil {
 				return nil, err
diff --git a/api/routes/photos.go b/api/routes/photos.go
index 06b0072..9da3826 100644
--- a/api/routes/photos.go
+++ b/api/routes/photos.go
@@ -41,7 +41,7 @@ func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) {
 		var cachedPath string
 
 		if mediaURL.Purpose == models.PhotoThumbnail || mediaURL.Purpose == models.PhotoHighRes || mediaURL.Purpose == models.VideoThumbnail {
-			cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(int(media.AlbumID)), strconv.Itoa(int(mediaURL.MediaID)), mediaURL.MediaName)
+			cachedPath = path.Join(scanner.MediaCachePath(), strconv.Itoa(int(media.AlbumID)), strconv.Itoa(int(mediaURL.MediaID)), mediaURL.MediaName)
 		} else if mediaURL.Purpose == models.MediaOriginal {
 			cachedPath = media.Path
 		} else {
diff --git a/api/routes/videos.go b/api/routes/videos.go
index b193c43..c774537 100644
--- a/api/routes/videos.go
+++ b/api/routes/videos.go
@@ -40,7 +40,7 @@ func RegisterVideoRoutes(db *gorm.DB, router *mux.Router) {
 		var cachedPath string
 
 		if mediaURL.Purpose == models.VideoWeb {
-			cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(int(media.AlbumID)), strconv.Itoa(int(mediaURL.MediaID)), mediaURL.MediaName)
+			cachedPath = path.Join(scanner.MediaCachePath(), strconv.Itoa(int(media.AlbumID)), strconv.Itoa(int(mediaURL.MediaID)), mediaURL.MediaName)
 		} else {
 			log.Printf("ERROR: Can not handle media_purpose for video: %s\n", mediaURL.Purpose)
 			w.WriteHeader(http.StatusInternalServerError)
diff --git a/api/scanner/cleanup_media.go b/api/scanner/cleanup_media.go
index e89b2d1..f5693a0 100644
--- a/api/scanner/cleanup_media.go
+++ b/api/scanner/cleanup_media.go
@@ -36,7 +36,7 @@ func CleanupMedia(db *gorm.DB, albumId int, albumMedia []*models.Media) []error
 	for _, media := range mediaList {
 
 		mediaIDs = append(mediaIDs, media.ID)
-		cachePath := path.Join(PhotoCache(), strconv.Itoa(int(albumId)), strconv.Itoa(int(media.ID)))
+		cachePath := path.Join(MediaCachePath(), strconv.Itoa(int(albumId)), strconv.Itoa(int(media.ID)))
 		err := os.RemoveAll(cachePath)
 		if err != nil {
 			deleteErrors = append(deleteErrors, errors.Wrapf(err, "delete unused cache folder (%s)", cachePath))
@@ -84,7 +84,7 @@ func deleteOldUserAlbums(db *gorm.DB, scannedAlbums []*models.Album, user *model
 	deleteAlbumIDs := make([]int, len(albums))
 	for i, album := range albums {
 		deleteAlbumIDs[i] = album.ID
-		cachePath := path.Join(PhotoCache(), strconv.Itoa(int(album.ID)))
+		cachePath := path.Join(MediaCachePath(), strconv.Itoa(int(album.ID)))
 		err := os.RemoveAll(cachePath)
 		if err != nil {
 			deleteErrors = append(deleteErrors, errors.Wrapf(err, "delete unused cache folder (%s)", cachePath))
diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go
index 6f6f7d6..9c4b4db 100644
--- a/api/scanner/process_photo.go
+++ b/api/scanner/process_photo.go
@@ -202,14 +202,14 @@ func processPhoto(tx *gorm.DB, imageData *EncodeMediaData, photoCachePath *strin
 func makeMediaCacheDir(media *models.Media) (*string, error) {
 
 	// Make root cache dir if not exists
-	if _, err := os.Stat(PhotoCache()); os.IsNotExist(err) {
-		if err := os.Mkdir(PhotoCache(), os.ModePerm); err != nil {
+	if _, err := os.Stat(MediaCachePath()); os.IsNotExist(err) {
+		if err := os.Mkdir(MediaCachePath(), os.ModePerm); err != nil {
 			return nil, errors.Wrap(err, "could not make root image cache directory")
 		}
 	}
 
 	// Make album cache dir if not exists
-	albumCachePath := path.Join(PhotoCache(), strconv.Itoa(int(media.AlbumID)))
+	albumCachePath := path.Join(MediaCachePath(), strconv.Itoa(int(media.AlbumID)))
 	if _, err := os.Stat(albumCachePath); os.IsNotExist(err) {
 		if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil {
 			return nil, errors.Wrap(err, "could not make album image cache directory")
diff --git a/api/scanner/scanner_user.go b/api/scanner/scanner_user.go
index bee7c3d..0b3f081 100644
--- a/api/scanner/scanner_user.go
+++ b/api/scanner/scanner_user.go
@@ -207,10 +207,11 @@ func ScannerError(format string, args ...interface{}) {
 	})
 }
 
-func PhotoCache() string {
-	photoCache := os.Getenv("PHOTO_CACHE")
+// MediaCachePath returns the path for where the media cache is located on the file system
+func MediaCachePath() string {
+	photoCache := utils.EnvMediaCachePath.GetValue()
 	if photoCache == "" {
-		photoCache = "./photo_cache"
+		photoCache = "./media_cache"
 	}
 
 	return photoCache
diff --git a/api/server.go b/api/server.go
index f2223b4..506d2b8 100644
--- a/api/server.go
+++ b/api/server.go
@@ -3,7 +3,6 @@ package main
 import (
 	"log"
 	"net/http"
-	"os"
 	"path"
 
 	"github.com/gorilla/handlers"
@@ -29,7 +28,7 @@ func main() {
 		log.Println("No .env file found")
 	}
 
-	devMode := os.Getenv("PHOTOVIEW_DEVELOPMENT_MODE") == "1"
+	devMode := utils.DevelopmentMode()
 
 	db, err := database.SetupDatabase()
 	if err != nil {
@@ -92,7 +91,7 @@ func main() {
 	videoRouter := endpointRouter.PathPrefix("/video").Subrouter()
 	routes.RegisterVideoRoutes(db, videoRouter)
 
-	shouldServeUI := os.Getenv("PHOTOVIEW_SERVE_UI") == "1"
+	shouldServeUI := utils.ShouldServeUI()
 
 	if shouldServeUI {
 		spa := routes.NewSpaHandler("/ui", "index.html")
@@ -111,7 +110,7 @@ func main() {
 		log.Printf("Photoview UI public endpoint ready at %s\n", uiEndpoint.String())
 
 		if !shouldServeUI {
-			log.Printf("Notice: UI is not served by the the api (PHOTOVIEW_SERVE_UI=0)")
+			log.Printf("Notice: UI is not served by the the api (%s=0)", utils.EnvServeUI.GetName())
 		}
 
 	}
diff --git a/api/utils/Endpoints.go b/api/utils/Endpoints.go
index af5500b..30ec5f7 100644
--- a/api/utils/Endpoints.go
+++ b/api/utils/Endpoints.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"log"
 	"net/url"
-	"os"
 	"path"
 	"strconv"
 )
@@ -12,7 +11,7 @@ import (
 func ApiListenUrl() *url.URL {
 	const defaultPort = "4001"
 
-	shouldServeUI := os.Getenv("PHOTOVIEW_SERVE_UI") == "1"
+	shouldServeUI := ShouldServeUI()
 
 	apiPrefix := "/"
 	if shouldServeUI {
@@ -21,19 +20,19 @@ func ApiListenUrl() *url.URL {
 
 	var listenAddr string
 
-	listenAddr = os.Getenv("PHOTOVIEW_LISTEN_IP")
+	listenAddr = EnvListenIP.GetValue()
 	if listenAddr == "" {
 		listenAddr = "127.0.0.1"
 	}
 
-	listenPortStr := os.Getenv("PHOTOVIEW_LISTEN_PORT")
+	listenPortStr := EnvListenPort.GetValue()
 	if listenPortStr == "" {
 		listenPortStr = defaultPort
 	}
 
 	listenPort, err := strconv.Atoi(listenPortStr)
 	if err != nil {
-		log.Fatalf("PHOTOVIEW_LISTEN_PORT must be a number: '%s'\n%s", listenPortStr, err)
+		log.Fatalf("%s must be a number: '%s'\n%s", EnvListenPort.GetName(), listenPortStr, err)
 	}
 
 	apiUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", listenAddr, listenPort))
@@ -46,16 +45,16 @@ func ApiListenUrl() *url.URL {
 }
 
 func ApiEndpointUrl() *url.URL {
-	apiEndpointStr := os.Getenv("PHOTOVIEW_API_ENDPOINT")
+	apiEndpointStr := EnvAPIEndpoint.GetValue()
 
-	shouldServeUI := os.Getenv("PHOTOVIEW_SERVE_UI") == "1"
+	shouldServeUI := ShouldServeUI()
 	if shouldServeUI {
-		apiEndpointStr = os.Getenv("PHOTOVIEW_PUBLIC_ENDPOINT")
+		apiEndpointStr = EnvPublicEndpoint.GetValue()
 	}
 
 	apiEndpointUrl, err := url.Parse(apiEndpointStr)
 	if err != nil {
-		log.Fatalf("ERROR: Environment variable PHOTOVIEW_API_ENDPOINT is not a proper url")
+		log.Fatalf("ERROR: Environment variable %s is not a proper url (%s)", EnvAPIEndpoint.GetName(), EnvAPIEndpoint.GetValue())
 	}
 
 	if shouldServeUI {
@@ -66,16 +65,16 @@ func ApiEndpointUrl() *url.URL {
 }
 
 func UiEndpointUrl() *url.URL {
-	uiEndpointStr := os.Getenv("PHOTOVIEW_UI_ENDPOINT")
+	uiEndpointStr := EnvUIEndpoint.GetValue()
 
-	shouldServeUI := os.Getenv("PHOTOVIEW_SERVE_UI") == "1"
+	shouldServeUI := ShouldServeUI()
 	if shouldServeUI {
-		uiEndpointStr = os.Getenv("PHOTOVIEW_PUBLIC_ENDPOINT")
+		uiEndpointStr = EnvPublicEndpoint.GetValue()
 	}
 
 	uiEndpointUrl, err := url.Parse(uiEndpointStr)
 	if err != nil {
-		log.Fatalf("ERROR: Environment variable PHOTOVIEW_UI_ENDPOINT is not a proper url")
+		log.Fatalf("ERROR: Environment variable %s is not a proper url (%s)", EnvUIEndpoint.GetName(), EnvUIEndpoint.GetValue())
 	}
 
 	return uiEndpointUrl
diff --git a/api/utils/environment_variables.go b/api/utils/environment_variables.go
new file mode 100644
index 0000000..15f5a2e
--- /dev/null
+++ b/api/utils/environment_variables.go
@@ -0,0 +1,50 @@
+package utils
+
+import "os"
+
+// EnvironmentVariable represents the name of an environment variable used to configure Photoview
+type EnvironmentVariable string
+
+// General options
+const (
+	EnvDevelopmentMode EnvironmentVariable = "PHOTOVIEW_DEVELOPMENT_MODE"
+	EnvServeUI         EnvironmentVariable = "PHOTOVIEW_SERVE_UI"
+	EnvMediaCachePath  EnvironmentVariable = "PHOTOVIEW_MEDIA_CACHE"
+)
+
+// Network related
+const (
+	EnvListenIP       EnvironmentVariable = "PHOTOVIEW_LISTEN_IP"
+	EnvListenPort     EnvironmentVariable = "PHOTOVIEW_LISTEN_PORT"
+	EnvAPIEndpoint    EnvironmentVariable = "PHOTOVIEW_API_ENDPOINT"
+	EnvUIEndpoint     EnvironmentVariable = "PHOTOVIEW_UI_ENDPOINT"
+	EnvPublicEndpoint EnvironmentVariable = "PHOTOVIEW_PUBLIC_ENDPOINT"
+)
+
+// Database related
+const (
+	EnvDatabaseDriver EnvironmentVariable = "PHOTOVIEW_DATABASE_DRIVER"
+	EnvMysqlURL       EnvironmentVariable = "PHOTOVIEW_MYSQL_URL"
+	EnvSqlitePath     EnvironmentVariable = "PHOTOVIEW_SQLITE_PATH"
+)
+
+// GetName returns the name of the environment variable itself
+func (v EnvironmentVariable) GetName() string {
+	return string(v)
+}
+
+// GetValue returns the value of the environment
+func (v EnvironmentVariable) GetValue() string {
+	return os.Getenv(string(v))
+}
+
+// ShouldServeUI whether or not the "serve ui" option is enabled
+func ShouldServeUI() bool {
+	return EnvServeUI.GetValue() == "1"
+}
+
+// DevelopmentMode describes whether or not the server is running in development mode,
+// and should thus print debug informations and enable other features related to developing.
+func DevelopmentMode() bool {
+	return EnvDevelopmentMode.GetValue() == "1"
+}
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index 481fb60..a3cb8b6 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -24,7 +24,7 @@ services:
       - PHOTOVIEW_MYSQL_URL=photoview:photosecret@tcp(db)/photoview
       - PHOTOVIEW_LISTEN_IP=photoview
       - PHOTOVIEW_LISTEN_PORT=80
-      - PHOTO_CACHE=/app/cache
+      - PHOTOVIEW_MEDIA_CACHE=/app/cache
 
       # Change This: The publicly exposed url
       # For example if the server is available from the domain example.com,
