Add environment variables to disable features

Face detection still needs to be hidden on the frontend if it's disabled
This commit is contained in:
viktorstrate
2021-08-31 11:34:55 +02:00
parent fca5a640c8
commit 9bcfdbb2cd
10 changed files with 188 additions and 12 deletions

View File

@@ -45,6 +45,7 @@ type ResolverRoot interface {
Mutation() MutationResolver Mutation() MutationResolver
Query() QueryResolver Query() QueryResolver
ShareToken() ShareTokenResolver ShareToken() ShareTokenResolver
SiteInfo() SiteInfoResolver
Subscription() SubscriptionResolver Subscription() SubscriptionResolver
User() UserResolver User() UserResolver
} }
@@ -220,6 +221,7 @@ type ComplexityRoot struct {
SiteInfo struct { SiteInfo struct {
ConcurrentWorkers func(childComplexity int) int ConcurrentWorkers func(childComplexity int) int
FaceDetectionEnabled func(childComplexity int) int
InitialSetup func(childComplexity int) int InitialSetup func(childComplexity int) int
PeriodicScanInterval func(childComplexity int) int PeriodicScanInterval func(childComplexity int) int
} }
@@ -338,6 +340,9 @@ type QueryResolver interface {
type ShareTokenResolver interface { type ShareTokenResolver interface {
HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error) HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error)
} }
type SiteInfoResolver interface {
FaceDetectionEnabled(ctx context.Context, obj *models.SiteInfo) (bool, error)
}
type SubscriptionResolver interface { type SubscriptionResolver interface {
Notification(ctx context.Context) (<-chan *models.Notification, error) Notification(ctx context.Context) (<-chan *models.Notification, error)
} }
@@ -1369,6 +1374,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.SiteInfo.ConcurrentWorkers(childComplexity), true return e.complexity.SiteInfo.ConcurrentWorkers(childComplexity), true
case "SiteInfo.faceDetectionEnabled":
if e.complexity.SiteInfo.FaceDetectionEnabled == nil {
break
}
return e.complexity.SiteInfo.FaceDetectionEnabled(childComplexity), true
case "SiteInfo.initialSetup": case "SiteInfo.initialSetup":
if e.complexity.SiteInfo.InitialSetup == nil { if e.complexity.SiteInfo.InitialSetup == nil {
break break
@@ -1821,7 +1833,10 @@ type ShareToken {
"General information about the site" "General information about the site"
type SiteInfo { type SiteInfo {
"Whether or not the initial setup wizard should be shown"
initialSetup: Boolean! initialSetup: Boolean!
"Whether or not face detection is enabled and working"
faceDetectionEnabled: Boolean!
"How often automatic scans should be initiated in seconds" "How often automatic scans should be initiated in seconds"
periodicScanInterval: Int! @isAdmin periodicScanInterval: Int! @isAdmin
"How many max concurrent scanner jobs that should run at once" "How many max concurrent scanner jobs that should run at once"
@@ -7862,6 +7877,41 @@ func (ec *executionContext) _SiteInfo_initialSetup(ctx context.Context, field gr
return ec.marshalNBoolean2bool(ctx, field.Selections, res) return ec.marshalNBoolean2bool(ctx, field.Selections, res)
} }
func (ec *executionContext) _SiteInfo_faceDetectionEnabled(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "SiteInfo",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.SiteInfo().FaceDetectionEnabled(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _SiteInfo_periodicScanInterval(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) { func (ec *executionContext) _SiteInfo_periodicScanInterval(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -11133,17 +11183,31 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
case "initialSetup": case "initialSetup":
out.Values[i] = ec._SiteInfo_initialSetup(ctx, field, obj) out.Values[i] = ec._SiteInfo_initialSetup(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
case "faceDetectionEnabled":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._SiteInfo_faceDetectionEnabled(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "periodicScanInterval": case "periodicScanInterval":
out.Values[i] = ec._SiteInfo_periodicScanInterval(ctx, field, obj) out.Values[i] = ec._SiteInfo_periodicScanInterval(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
case "concurrentWorkers": case "concurrentWorkers":
out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj) out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ atomic.AddUint32(&invalids, 1)
} }
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))

View File

@@ -32,6 +32,10 @@ func (r imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace)
return obj.FaceGroup, nil return obj.FaceGroup, nil
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
var faceGroup models.FaceGroup var faceGroup models.FaceGroup
if err := r.Database.Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil { if err := r.Database.Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil {
return nil, err return nil, err
@@ -48,6 +52,10 @@ func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
if err := user.FillAlbums(r.Database); err != nil { if err := user.FillAlbums(r.Database); err != nil {
return nil, err return nil, err
} }
@@ -78,6 +86,10 @@ func (r faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceG
return -1, errors.New("unauthorized") return -1, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return -1, errors.New("face detector not initialized")
}
if err := user.FillAlbums(r.Database); err != nil { if err := user.FillAlbums(r.Database); err != nil {
return -1, err return -1, err
} }
@@ -107,6 +119,10 @@ func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGrou
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
if err := user.FillAlbums(r.Database); err != nil { if err := user.FillAlbums(r.Database); err != nil {
return nil, err return nil, err
} }
@@ -135,6 +151,10 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
if err := user.FillAlbums(r.Database); err != nil { if err := user.FillAlbums(r.Database); err != nil {
return nil, err return nil, err
} }
@@ -168,6 +188,10 @@ func (r *mutationResolver) SetFaceGroupLabel(ctx context.Context, faceGroupID in
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
faceGroup, err := userOwnedFaceGroup(r.Database, user, faceGroupID) faceGroup, err := userOwnedFaceGroup(r.Database, user, faceGroupID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -186,6 +210,10 @@ func (r *mutationResolver) CombineFaceGroups(ctx context.Context, destinationFac
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
destinationFaceGroup, err := userOwnedFaceGroup(r.Database, user, destinationFaceGroupID) destinationFaceGroup, err := userOwnedFaceGroup(r.Database, user, destinationFaceGroupID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -223,6 +251,10 @@ func (r *mutationResolver) MoveImageFaces(ctx context.Context, imageFaceIDs []in
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
userOwnedImageFaceIDs := make([]int, 0) userOwnedImageFaceIDs := make([]int, 0)
var destFaceGroup *models.FaceGroup var destFaceGroup *models.FaceGroup
@@ -290,6 +322,10 @@ func (r *mutationResolver) RecognizeUnlabeledFaces(ctx context.Context) ([]*mode
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
var updatedImageFaces []*models.ImageFace var updatedImageFaces []*models.ImageFace
transactionError := r.Database.Transaction(func(tx *gorm.DB) error { transactionError := r.Database.Transaction(func(tx *gorm.DB) error {
@@ -312,6 +348,10 @@ func (r *mutationResolver) DetachImageFaces(ctx context.Context, imageFaceIDs []
return nil, errors.New("unauthorized") return nil, errors.New("unauthorized")
} }
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
userOwnedImageFaceIDs := make([]int, 0) userOwnedImageFaceIDs := make([]int, 0)
newFaceGroup := models.FaceGroup{} newFaceGroup := models.FaceGroup{}

View File

@@ -0,0 +1,21 @@
package resolvers
import (
"context"
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/scanner/face_detection"
)
type SiteInfoResolver struct {
*Resolver
}
func (r *Resolver) SiteInfo() api.SiteInfoResolver {
return &SiteInfoResolver{r}
}
func (SiteInfoResolver) FaceDetectionEnabled(ctx context.Context, obj *models.SiteInfo) (bool, error) {
return face_detection.GlobalFaceDetector != nil, nil
}

View File

@@ -343,10 +343,12 @@ func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int,
} }
// Reload faces as media might have been deleted // Reload faces as media might have been deleted
if face_detection.GlobalFaceDetector == nil {
if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(r.Database); err != nil { if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(r.Database); err != nil {
return nil, err return nil, err
} }
} }
}
return &album, nil return &album, nil
} }

View File

@@ -201,7 +201,10 @@ type ShareToken {
"General information about the site" "General information about the site"
type SiteInfo { type SiteInfo {
"Whether or not the initial setup wizard should be shown"
initialSetup: Boolean! initialSetup: Boolean!
"Whether or not face detection is enabled and working"
faceDetectionEnabled: Boolean!
"How often automatic scans should be initiated in seconds" "How often automatic scans should be initiated in seconds"
periodicScanInterval: Int! @isAdmin periodicScanInterval: Int! @isAdmin
"How many max concurrent scanner jobs that should run at once" "How many max concurrent scanner jobs that should run at once"

View File

@@ -53,10 +53,12 @@ func CleanupMedia(db *gorm.DB, albumId int, albumMedia []*models.Media) []error
} }
// Reload faces after deleting media // Reload faces after deleting media
if face_detection.GlobalFaceDetector != nil {
if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil { if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil {
deleteErrors = append(deleteErrors, errors.Wrap(err, "reload faces from database")) deleteErrors = append(deleteErrors, errors.Wrap(err, "reload faces from database"))
} }
} }
}
return deleteErrors return deleteErrors
} }
@@ -124,9 +126,11 @@ func deleteOldUserAlbums(db *gorm.DB, scannedAlbums []*models.Album, user *model
} }
// Reload faces after deleting albums // Reload faces after deleting albums
if face_detection.GlobalFaceDetector == nil {
if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil { if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(db); err != nil {
deleteErrors = append(deleteErrors, err) deleteErrors = append(deleteErrors, err)
} }
}
return deleteErrors return deleteErrors
} }

View File

@@ -19,9 +19,13 @@ type FaceDetector struct {
imageFaceIDs []int imageFaceIDs []int
} }
var GlobalFaceDetector FaceDetector var GlobalFaceDetector *FaceDetector = nil
func InitializeFaceDetector(db *gorm.DB) error { func InitializeFaceDetector(db *gorm.DB) error {
if utils.EnvDisableRawProcessing.GetBool() {
log.Printf("Face detection disabled (%s=1)\n", utils.EnvDisableRawProcessing.GetName())
return nil
}
log.Println("Initializing face detector") log.Println("Initializing face detector")
@@ -35,7 +39,7 @@ func InitializeFaceDetector(db *gorm.DB) error {
return errors.Wrap(err, "get face detection samples from database") return errors.Wrap(err, "get face detection samples from database")
} }
GlobalFaceDetector = FaceDetector{ GlobalFaceDetector = &FaceDetector{
rec: rec, rec: rec,
faceDescriptors: faceDescriptors, faceDescriptors: faceDescriptors,
faceGroupIDs: faceGroupIDs, faceGroupIDs: faceGroupIDs,

View File

@@ -8,6 +8,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/photoview/photoview/api/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/vansante/go-ffprobe.v2" "gopkg.in/vansante/go-ffprobe.v2"
) )
@@ -33,6 +34,11 @@ type FfmpegWorker struct {
} }
func newDarktableWorker() *DarktableWorker { func newDarktableWorker() *DarktableWorker {
if utils.EnvDisableRawProcessing.GetBool() {
log.Printf("Executable worker disabled (%s=1): darktable\n", utils.EnvDisableRawProcessing.GetName())
return nil
}
path, err := exec.LookPath("darktable-cli") path, err := exec.LookPath("darktable-cli")
if err != nil { if err != nil {
log.Println("Executable worker not found: darktable") log.Println("Executable worker not found: darktable")
@@ -54,6 +60,11 @@ func newDarktableWorker() *DarktableWorker {
} }
func newFfmpegWorker() *FfmpegWorker { func newFfmpegWorker() *FfmpegWorker {
if utils.EnvDisableVideoEncoding.GetBool() {
log.Printf("Executable worker disabled (%s=1): ffmpeg\n", utils.EnvDisableVideoEncoding.GetName())
return nil
}
path, err := exec.LookPath("ffmpeg") path, err := exec.LookPath("ffmpeg")
if err != nil { if err != nil {
log.Println("Executable worker not found: ffmpeg") log.Println("Executable worker not found: ffmpeg")

View File

@@ -141,6 +141,9 @@ func scanAlbum(album *models.Album, cache *scanner_cache.AlbumScannerCache, db *
if processing_was_needed && media.Type == models.MediaTypePhoto { if processing_was_needed && media.Type == models.MediaTypePhoto {
go func(media *models.Media) { go func(media *models.Media) {
if face_detection.GlobalFaceDetector == nil {
return
}
if err := face_detection.GlobalFaceDetector.DetectFaces(db, media); err != nil { if err := face_detection.GlobalFaceDetector.DetectFaces(db, media); err != nil {
scanner_utils.ScannerError("Error detecting faces in image (%s): %s", media.Path, err) scanner_utils.ScannerError("Error detecting faces in image (%s): %s", media.Path, err)
} }

View File

@@ -1,6 +1,9 @@
package utils package utils
import "os" import (
"os"
"strings"
)
// EnvironmentVariable represents the name of an environment variable used to configure Photoview // EnvironmentVariable represents the name of an environment variable used to configure Photoview
type EnvironmentVariable string type EnvironmentVariable string
@@ -30,6 +33,13 @@ const (
EnvSqlitePath EnvironmentVariable = "PHOTOVIEW_SQLITE_PATH" EnvSqlitePath EnvironmentVariable = "PHOTOVIEW_SQLITE_PATH"
) )
// Feature related
const (
EnvDisableFaceRecognition EnvironmentVariable = "PHOTOVIEW_DISABLE_FACE_RECOGNITION"
EnvDisableVideoEncoding EnvironmentVariable = "PHOTOVIEW_DISABLE_VIDEO_ENCODING"
EnvDisableRawProcessing EnvironmentVariable = "PHOTOVIEW_DISABLE_RAW_PROCESSING"
)
// GetName returns the name of the environment variable itself // GetName returns the name of the environment variable itself
func (v EnvironmentVariable) GetName() string { func (v EnvironmentVariable) GetName() string {
return string(v) return string(v)
@@ -40,6 +50,20 @@ func (v EnvironmentVariable) GetValue() string {
return os.Getenv(string(v)) return os.Getenv(string(v))
} }
// GetBool returns the environment variable as a boolean (defaults to false if not defined)
func (v EnvironmentVariable) GetBool() bool {
value := strings.ToLower(os.Getenv(string(v)))
trueValues := []string{"1", "true"}
for _, x := range trueValues {
if value == x {
return true
}
}
return false
}
// ShouldServeUI whether or not the "serve ui" option is enabled // ShouldServeUI whether or not the "serve ui" option is enabled
func ShouldServeUI() bool { func ShouldServeUI() bool {
return EnvServeUI.GetValue() == "1" return EnvServeUI.GetValue() == "1"