diff --git a/frontend/src/component/p-navigation.vue b/frontend/src/component/p-navigation.vue index 010c57256..4ab44b7ba 100644 --- a/frontend/src/component/p-navigation.vue +++ b/frontend/src/component/p-navigation.vue @@ -225,7 +225,7 @@ - + settings diff --git a/internal/api/settings.go b/internal/api/settings.go index 04f4d04d5..30717a4e3 100644 --- a/internal/api/settings.go +++ b/internal/api/settings.go @@ -26,7 +26,7 @@ func GetSettings(router *gin.RouterGroup, conf *config.Config) { // POST /api/v1/settings func SaveSettings(router *gin.RouterGroup, conf *config.Config) { router.POST("/settings", func(c *gin.Context) { - if Unauthorized(c, conf) { + if conf.DisableSettings() || Unauthorized(c, conf) { c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) return } diff --git a/internal/classify/tensorflow_test.go b/internal/classify/tensorflow_test.go index 618ae0b2a..06d764d5e 100644 --- a/internal/classify/tensorflow_test.go +++ b/internal/classify/tensorflow_test.go @@ -14,7 +14,7 @@ func TestTensorFlow_LabelsFromFile(t *testing.T) { t.Run("chameleon_lime.jpg", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) result, err := tensorFlow.File(conf.ExamplesPath() + "/chameleon_lime.jpg") @@ -38,7 +38,7 @@ func TestTensorFlow_LabelsFromFile(t *testing.T) { t.Run("not existing file", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) result, err := tensorFlow.File(conf.ExamplesPath() + "/notexisting.jpg") assert.Contains(t, err.Error(), "no such file or directory") @@ -54,7 +54,7 @@ func TestTensorFlow_Labels(t *testing.T) { t.Run("chameleon_lime.jpg", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/chameleon_lime.jpg"); err != nil { t.Error(err) @@ -77,7 +77,7 @@ func TestTensorFlow_Labels(t *testing.T) { t.Run("dog_orange.jpg", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/dog_orange.jpg"); err != nil { t.Error(err) @@ -100,7 +100,7 @@ func TestTensorFlow_Labels(t *testing.T) { t.Run("Random.docx", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/Random.docx"); err != nil { t.Error(err) @@ -113,7 +113,7 @@ func TestTensorFlow_Labels(t *testing.T) { t.Run("6720px_white.jpg", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) if imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/6720px_white.jpg"); err != nil { t.Error(err) @@ -129,7 +129,7 @@ func TestTensorFlow_LoadModel(t *testing.T) { t.Run("model path exists", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) result := tensorFlow.loadModel() assert.Nil(t, result) @@ -137,7 +137,7 @@ func TestTensorFlow_LoadModel(t *testing.T) { t.Run("model path does not exist", func(t *testing.T) { conf := config.NewTestErrorConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) result := tensorFlow.loadModel() assert.Contains(t, result.Error(), "Could not find SavedModel") @@ -148,7 +148,7 @@ func TestTensorFlow_BestLabels(t *testing.T) { t.Run("labels not loaded", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) p := make([]float32, 1000) @@ -160,7 +160,7 @@ func TestTensorFlow_BestLabels(t *testing.T) { t.Run("labels loaded", func(t *testing.T) { conf := config.TestConfig() path := conf.TensorFlowModelPath() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) tensorFlow.loadLabels(path) p := make([]float32, 1000) @@ -183,7 +183,7 @@ func TestTensorFlow_MakeTensor(t *testing.T) { t.Run("cat_brown.jpg", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/cat_brown.jpg") assert.Nil(t, err) @@ -195,7 +195,7 @@ func TestTensorFlow_MakeTensor(t *testing.T) { t.Run("Random.docx", func(t *testing.T) { conf := config.TestConfig() - tensorFlow := New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tensorFlow := New(conf.ResourcesPath(), conf.DisableTensorFlow()) imageBuffer, err := ioutil.ReadFile(conf.ExamplesPath() + "/Random.docx") assert.Nil(t, err) diff --git a/internal/commands/config.go b/internal/commands/config.go index 371f98fc5..fece55bae 100644 --- a/internal/commands/config.go +++ b/internal/commands/config.go @@ -52,7 +52,6 @@ func configAction(ctx *cli.Context) error { fmt.Printf("resources-path %s\n", conf.ResourcesPath()) fmt.Printf("tf-version %s\n", conf.TensorFlowVersion()) fmt.Printf("tf-model-path %s\n", conf.TensorFlowModelPath()) - fmt.Printf("tf-disabled %t\n", conf.TensorFlowDisabled()) fmt.Printf("templates-path %s\n", conf.HttpTemplatesPath()) fmt.Printf("favicons-path %s\n", conf.HttpFaviconsPath()) fmt.Printf("static-path %s\n", conf.HttpStaticPath()) @@ -83,5 +82,8 @@ func configAction(ctx *cli.Context) error { fmt.Printf("thumb-limit %d\n", conf.ThumbLimit()) fmt.Printf("thumb-filter %s\n", conf.ThumbFilter()) + fmt.Printf("disable-tf %t\n", conf.DisableTensorFlow()) + fmt.Printf("disable-settings %t\n", conf.DisableSettings()) + return nil } diff --git a/internal/config/client.go b/internal/config/client.go index 36b6a34b0..aea29d347 100644 --- a/internal/config/client.go +++ b/internal/config/client.go @@ -12,6 +12,31 @@ import ( // ClientConfig contains HTTP client / Web UI config values type ClientConfig map[string]interface{} +// Flags returns config flags as string slice. +func (c *Config) Flags() (flags []string) { + if c.Public() { + flags = append(flags, "public") + } + + if c.Debug() { + flags = append(flags, "debug") + } + + if c.Experimental() { + flags = append(flags, "experimental") + } + + if c.ReadOnly() { + flags = append(flags, "readonly") + } + + if !c.DisableSettings() { + flags = append(flags, "settings") + } + + return flags +} + // PublicClientConfig returns reduced config values for non-public sites. func (c *Config) PublicClientConfig() ClientConfig { if c.Public() { @@ -20,22 +45,7 @@ func (c *Config) PublicClientConfig() ClientConfig { jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js") cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css") - - // Feature Flags - var flags []string - - if c.Public() { - flags = append(flags, "public") - } - if c.Debug() { - flags = append(flags, "debug") - } - if c.Experimental() { - flags = append(flags, "experimental") - } - if c.ReadOnly() { - flags = append(flags, "readonly") - } + configFlags := c.Flags() var noPos = struct { PhotoUUID string `json:"photo"` @@ -57,34 +67,35 @@ func (c *Config) PublicClientConfig() ClientConfig { }{} result := ClientConfig{ - "settings": c.Settings(), - "flags": strings.Join(flags, " "), - "name": c.Name(), - "url": c.Url(), - "title": c.Title(), - "subtitle": c.Subtitle(), - "description": c.Description(), - "author": c.Author(), - "twitter": c.Twitter(), - "version": c.Version(), - "copyright": c.Copyright(), - "debug": c.Debug(), - "readonly": c.ReadOnly(), - "uploadNSFW": c.UploadNSFW(), - "public": c.Public(), - "experimental": c.Experimental(), - "albums": []string{}, - "cameras": []string{}, - "lenses": []string{}, - "countries": []string{}, - "thumbnails": Thumbnails, - "jsHash": jsHash, - "cssHash": cssHash, - "count": count, - "pos": noPos, - "years": []int{}, - "colors": colors.All.List(), - "categories": []string{}, + "settings": c.Settings(), + "flags": strings.Join(configFlags, " "), + "name": c.Name(), + "url": c.Url(), + "title": c.Title(), + "subtitle": c.Subtitle(), + "description": c.Description(), + "author": c.Author(), + "twitter": c.Twitter(), + "version": c.Version(), + "copyright": c.Copyright(), + "debug": c.Debug(), + "readonly": c.ReadOnly(), + "uploadNSFW": c.UploadNSFW(), + "public": c.Public(), + "experimental": c.Experimental(), + "disableSettings": c.DisableSettings(), + "albums": []string{}, + "cameras": []string{}, + "lenses": []string{}, + "countries": []string{}, + "thumbnails": Thumbnails, + "jsHash": jsHash, + "cssHash": cssHash, + "count": count, + "pos": noPos, + "years": []int{}, + "colors": colors.All.List(), + "categories": []string{}, } return result @@ -198,52 +209,38 @@ func (c *Config) ClientConfig() ClientConfig { jsHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.js") cssHash := fs.Checksum(c.HttpStaticBuildPath() + "/app.css") - - // Feature Flags - var flags []string - - if c.Public() { - flags = append(flags, "public") - } - if c.Debug() { - flags = append(flags, "debug") - } - if c.Experimental() { - flags = append(flags, "experimental") - } - if c.ReadOnly() { - flags = append(flags, "readonly") - } + configFlags := c.Flags() result := ClientConfig{ - "flags": strings.Join(flags, " "), - "name": c.Name(), - "url": c.Url(), - "title": c.Title(), - "subtitle": c.Subtitle(), - "description": c.Description(), - "author": c.Author(), - "twitter": c.Twitter(), - "version": c.Version(), - "copyright": c.Copyright(), - "debug": c.Debug(), - "readonly": c.ReadOnly(), - "uploadNSFW": c.UploadNSFW(), - "public": c.Public(), - "experimental": c.Experimental(), - "albums": albums, - "cameras": cameras, - "lenses": lenses, - "countries": countries, - "thumbnails": Thumbnails, - "jsHash": jsHash, - "cssHash": cssHash, - "settings": c.Settings(), - "count": count, - "pos": position, - "years": years, - "colors": colors.All.List(), - "categories": categories, + "flags": strings.Join(configFlags, " "), + "name": c.Name(), + "url": c.Url(), + "title": c.Title(), + "subtitle": c.Subtitle(), + "description": c.Description(), + "author": c.Author(), + "twitter": c.Twitter(), + "version": c.Version(), + "copyright": c.Copyright(), + "debug": c.Debug(), + "readonly": c.ReadOnly(), + "uploadNSFW": c.UploadNSFW(), + "public": c.Public(), + "experimental": c.Experimental(), + "disableSettings": c.DisableSettings(), + "albums": albums, + "cameras": cameras, + "lenses": lenses, + "countries": countries, + "thumbnails": Thumbnails, + "jsHash": jsHash, + "cssHash": cssHash, + "settings": c.Settings(), + "count": count, + "pos": position, + "years": years, + "colors": colors.All.List(), + "categories": categories, } return result diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c170379fc..c5bb1632f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -51,7 +51,7 @@ func TestConfig_TensorFlowDisabled(t *testing.T) { ctx := CliTestContext() c := NewConfig(ctx) - version := c.TensorFlowDisabled() + version := c.DisableTensorFlow() assert.Equal(t, false, version) } diff --git a/internal/config/flags.go b/internal/config/flags.go index 5436f6e8e..7530b60f2 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -230,11 +230,6 @@ var GlobalFlags = []cli.Flag{ Usage: "allow uploads that may contain offensive content", EnvVar: "PHOTOPRISM_UPLOAD_NSFW", }, - cli.BoolFlag{ - Name: "tf-disabled, t", - Usage: "don't use TensorFlow for image classification", - EnvVar: "PHOTOPRISM_TF_DISABLED", - }, cli.StringFlag{ Name: "geocoding-api, g", Usage: "geocoding api (none, osm or places)", @@ -265,4 +260,14 @@ var GlobalFlags = []cli.Flag{ Value: "lanczos", EnvVar: "PHOTOPRISM_THUMB_FILTER", }, + cli.BoolFlag{ + Name: "disable-tf", + Usage: "don't use TensorFlow for image classification", + EnvVar: "PHOTOPRISM_DISABLE_TF", + }, + cli.BoolFlag{ + Name: "disable-settings", + Usage: "user can not change settings", + EnvVar: "PHOTOPRISM_DISABLE_SETTINGS", + }, } diff --git a/internal/config/params.go b/internal/config/params.go index 6d49b5f43..e52b3ecbc 100644 --- a/internal/config/params.go +++ b/internal/config/params.go @@ -73,12 +73,13 @@ type Params struct { DetachServer bool `yaml:"detach-server" flag:"detach-server"` DetectNSFW bool `yaml:"detect-nsfw" flag:"detect-nsfw"` UploadNSFW bool `yaml:"upload-nsfw" flag:"upload-nsfw"` - DisableTensorFlow bool `yaml:"tf-disabled" flag:"tf-disabled"` GeoCodingApi string `yaml:"geocoding-api" flag:"geocoding-api"` ThumbQuality int `yaml:"thumb-quality" flag:"thumb-quality"` ThumbSize int `yaml:"thumb-size" flag:"thumb-size"` ThumbLimit int `yaml:"thumb-limit" flag:"thumb-limit"` ThumbFilter string `yaml:"thumb-filter" flag:"thumb-filter"` + DisableTensorFlow bool `yaml:"disable-tf" flag:"disable-tf"` + DisableSettings bool `yaml:"disable-settings" flag:"disable-settings"` } // NewParams creates a new configuration entity by using two methods: diff --git a/internal/config/settings.go b/internal/config/settings.go index 961797a30..59f99d2a0 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -9,16 +9,33 @@ import ( "gopkg.in/yaml.v2" ) +// DisableSettings returns true if the user is not allowed to change settings. +func (c *Config) DisableSettings() bool { + return c.config.DisableSettings +} + type MapsSettings struct { Animate int `json:"animate" yaml:"animate"` Style string `json:"style" yaml:"style"` } +type FeatureFlags struct { + Upload bool `json:"upload" yaml:"upload"` + Import bool `json:"import" yaml:"import"` + Labels bool `json:"labels" yaml:"labels"` + Places bool `json:"places" yaml:"places"` + Archive bool `json:"archive" yaml:"archive"` + Download bool `json:"download" yaml:"download"` + Edit bool `json:"edit" yaml:"edit"` + Share bool `json:"share" yaml:"share"` +} + // Settings contains Web UI settings type Settings struct { Theme string `json:"theme" yaml:"theme"` Language string `json:"language" yaml:"language"` Maps MapsSettings `json:"maps" yaml:"maps"` + Features FeatureFlags `json:"features" yaml:"features"` } // NewSettings returns a empty Settings @@ -30,6 +47,16 @@ func NewSettings() *Settings { Animate: 0, Style: "streets", }, + Features: FeatureFlags{ + Upload: true, + Import: true, + Labels: true, + Places: true, + Archive: true, + Download: true, + Edit: true, + Share: true, + }, } } diff --git a/internal/config/tensorflow.go b/internal/config/tensorflow.go index 9e2b24530..693327976 100644 --- a/internal/config/tensorflow.go +++ b/internal/config/tensorflow.go @@ -7,7 +7,7 @@ func (c *Config) TensorFlowVersion() string { return tf.Version() } -// TensorFlowDisabled returns true if the use of TensorFlow is disabled for image classification. -func (c *Config) TensorFlowDisabled() bool { +// DisableTensorFlow returns true if the use of TensorFlow is disabled for image classification. +func (c *Config) DisableTensorFlow() bool { return c.config.DisableTensorFlow } diff --git a/internal/config/testdata/configEmpty.yml b/internal/config/testdata/configEmpty.yml index f01b3679b..b3d3ded9b 100644 --- a/internal/config/testdata/configEmpty.yml +++ b/internal/config/testdata/configEmpty.yml @@ -3,3 +3,12 @@ language: german maps: animate: 0 style: streets +features: + upload: true + import: true + labels: true + places: true + archive: true + download: true + edit: true + share: true diff --git a/internal/photoprism/import_test.go b/internal/photoprism/import_test.go index 5f307b5fd..d3c0916b2 100644 --- a/internal/photoprism/import_test.go +++ b/internal/photoprism/import_test.go @@ -12,7 +12,7 @@ import ( func TestNewImport(t *testing.T) { conf := config.TestConfig() - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tf := classify.New(conf.ResourcesPath(), conf.DisableTensorFlow()) nd := nsfw.New(conf.NSFWModelPath()) ind := NewIndex(conf, tf, nd) @@ -29,7 +29,7 @@ func TestImport_DestinationFilename(t *testing.T) { conf.InitializeTestData(t) - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tf := classify.New(conf.ResourcesPath(), conf.DisableTensorFlow()) nd := nsfw.New(conf.NSFWModelPath()) ind := NewIndex(conf, tf, nd) @@ -58,7 +58,7 @@ func TestImport_Start(t *testing.T) { conf.InitializeTestData(t) - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tf := classify.New(conf.ResourcesPath(), conf.DisableTensorFlow()) nd := nsfw.New(conf.NSFWModelPath()) ind := NewIndex(conf, tf, nd) diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index ff7adb906..cd5855133 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -134,7 +134,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( if file.FilePrimary { primaryFile = file - if !ind.conf.TensorFlowDisabled() && (fileChanged || o.UpdateKeywords || o.UpdateLabels || o.UpdateTitle) { + if !ind.conf.DisableTensorFlow() && (fileChanged || o.UpdateKeywords || o.UpdateLabels || o.UpdateTitle) { // Image classification via TensorFlow labels = ind.classifyImage(m) photo.PhotoNSFW = ind.isNSFW(m) diff --git a/internal/photoprism/index_test.go b/internal/photoprism/index_test.go index 6d8bda2dc..35e260f38 100644 --- a/internal/photoprism/index_test.go +++ b/internal/photoprism/index_test.go @@ -17,7 +17,7 @@ func TestIndex_Start(t *testing.T) { conf.InitializeTestData(t) - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tf := classify.New(conf.ResourcesPath(), conf.DisableTensorFlow()) nd := nsfw.New(conf.NSFWModelPath()) ind := NewIndex(conf, tf, nd) diff --git a/internal/photoprism/resample_test.go b/internal/photoprism/resample_test.go index 21578a356..6c889d135 100644 --- a/internal/photoprism/resample_test.go +++ b/internal/photoprism/resample_test.go @@ -27,7 +27,7 @@ func TestResample_Start(t *testing.T) { conf.InitializeTestData(t) - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) + tf := classify.New(conf.ResourcesPath(), conf.DisableTensorFlow()) nd := nsfw.New(conf.NSFWModelPath()) ind := NewIndex(conf, tf, nd) diff --git a/internal/service/classify.go b/internal/service/classify.go index 5906a0359..0fc825987 100644 --- a/internal/service/classify.go +++ b/internal/service/classify.go @@ -9,7 +9,7 @@ import ( var onceClassify sync.Once func initClassify() { - services.Classify = classify.New(Config().ResourcesPath(), Config().TensorFlowDisabled()) + services.Classify = classify.New(Config().ResourcesPath(), Config().DisableTensorFlow()) } func Classify() *classify.TensorFlow {