1
Fork 0

Merge pull request #488 from photoview/feature-env-vars

Add environment variables to disable optional features
This commit is contained in:
Viktor Strate Kløvedal 2021-08-31 12:15:42 +02:00 committed by GitHub
commit 4af3d11db4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 235 additions and 51 deletions

View File

@ -45,6 +45,7 @@ type ResolverRoot interface {
Mutation() MutationResolver
Query() QueryResolver
ShareToken() ShareTokenResolver
SiteInfo() SiteInfoResolver
Subscription() SubscriptionResolver
User() UserResolver
}
@ -220,6 +221,7 @@ type ComplexityRoot struct {
SiteInfo struct {
ConcurrentWorkers func(childComplexity int) int
FaceDetectionEnabled func(childComplexity int) int
InitialSetup func(childComplexity int) int
PeriodicScanInterval func(childComplexity int) int
}
@ -338,6 +340,9 @@ type QueryResolver interface {
type ShareTokenResolver interface {
HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error)
}
type SiteInfoResolver interface {
FaceDetectionEnabled(ctx context.Context, obj *models.SiteInfo) (bool, error)
}
type SubscriptionResolver interface {
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
case "SiteInfo.faceDetectionEnabled":
if e.complexity.SiteInfo.FaceDetectionEnabled == nil {
break
}
return e.complexity.SiteInfo.FaceDetectionEnabled(childComplexity), true
case "SiteInfo.initialSetup":
if e.complexity.SiteInfo.InitialSetup == nil {
break
@ -1821,7 +1833,10 @@ type ShareToken {
"General information about the site"
type SiteInfo {
"Whether or not the initial setup wizard should be shown"
initialSetup: Boolean!
"Whether or not face detection is enabled and working"
faceDetectionEnabled: Boolean!
"How often automatic scans should be initiated in seconds"
periodicScanInterval: Int! @isAdmin
"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)
}
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) {
defer func() {
if r := recover(); r != nil {
@ -11133,17 +11183,31 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
case "initialSetup":
out.Values[i] = ec._SiteInfo_initialSetup(ctx, field, obj)
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":
out.Values[i] = ec._SiteInfo_periodicScanInterval(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "concurrentWorkers":
out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
default:
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
}
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
var faceGroup models.FaceGroup
if err := r.Database.Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil {
return nil, err
@ -48,6 +52,10 @@ func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup
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 {
return nil, err
}
@ -78,6 +86,10 @@ func (r faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceG
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 {
return -1, err
}
@ -107,6 +119,10 @@ func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGrou
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 {
return nil, err
}
@ -135,6 +151,10 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
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 {
return nil, err
}
@ -168,6 +188,10 @@ func (r *mutationResolver) SetFaceGroupLabel(ctx context.Context, faceGroupID in
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)
if err != nil {
return nil, err
@ -186,6 +210,10 @@ func (r *mutationResolver) CombineFaceGroups(ctx context.Context, destinationFac
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)
if err != nil {
return nil, err
@ -223,6 +251,10 @@ func (r *mutationResolver) MoveImageFaces(ctx context.Context, imageFaceIDs []in
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
userOwnedImageFaceIDs := make([]int, 0)
var destFaceGroup *models.FaceGroup
@ -290,6 +322,10 @@ func (r *mutationResolver) RecognizeUnlabeledFaces(ctx context.Context) ([]*mode
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
var updatedImageFaces []*models.ImageFace
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")
}
if face_detection.GlobalFaceDetector == nil {
return nil, errors.New("face detector not initialized")
}
userOwnedImageFaceIDs := make([]int, 0)
newFaceGroup := models.FaceGroup{}

View File

@ -8,6 +8,7 @@ import (
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/scanner/face_detection"
"github.com/pkg/errors"
"gorm.io/gorm/clause"
)
@ -228,6 +229,10 @@ func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favor
}
func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
if face_detection.GlobalFaceDetector == nil {
return []*models.ImageFace{}, nil
}
if media.Faces != nil {
return media.Faces, nil
}

View File

@ -1,10 +1,7 @@
package resolvers
import (
"context"
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/models"
"gorm.io/gorm"
)
@ -35,7 +32,3 @@ type queryResolver struct{ *Resolver }
type subscriptionResolver struct {
Resolver *Resolver
}
func (r *queryResolver) SiteInfo(ctx context.Context) (*models.SiteInfo, error) {
return models.GetSiteInfo(r.Database)
}

View File

@ -0,0 +1,25 @@
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"
)
func (r *queryResolver) SiteInfo(ctx context.Context) (*models.SiteInfo, error) {
return models.GetSiteInfo(r.Database)
}
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,8 +343,10 @@ func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int,
}
// Reload faces as media might have been deleted
if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(r.Database); err != nil {
return nil, err
if face_detection.GlobalFaceDetector == nil {
if err := face_detection.GlobalFaceDetector.ReloadFacesFromDatabase(r.Database); err != nil {
return nil, err
}
}
}

View File

@ -68,7 +68,7 @@ type Query {
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! @isAuthorized
"Get the mapbox api token, returns null if mapbox is not enabled"
mapboxToken: String
mapboxToken: String @isAuthorized
shareToken(credentials: ShareTokenCredentials!): ShareToken!
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
@ -201,7 +201,10 @@ type ShareToken {
"General information about the site"
type SiteInfo {
"Whether or not the initial setup wizard should be shown"
initialSetup: Boolean!
"Whether or not face detection is enabled and working"
faceDetectionEnabled: Boolean!
"How often automatic scans should be initiated in seconds"
periodicScanInterval: Int! @isAdmin
"How many max concurrent scanner jobs that should run at once"

View File

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

View File

@ -19,9 +19,13 @@ type FaceDetector struct {
imageFaceIDs []int
}
var GlobalFaceDetector FaceDetector
var GlobalFaceDetector *FaceDetector = nil
func InitializeFaceDetector(db *gorm.DB) error {
if utils.EnvDisableFaceRecognition.GetBool() {
log.Printf("Face detection disabled (%s=1)\n", utils.EnvDisableFaceRecognition.GetName())
return nil
}
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")
}
GlobalFaceDetector = FaceDetector{
GlobalFaceDetector = &FaceDetector{
rec: rec,
faceDescriptors: faceDescriptors,
faceGroupIDs: faceGroupIDs,

View File

@ -8,6 +8,7 @@ import (
"os/exec"
"strings"
"github.com/photoview/photoview/api/utils"
"github.com/pkg/errors"
"gopkg.in/vansante/go-ffprobe.v2"
)
@ -33,6 +34,11 @@ type FfmpegWorker struct {
}
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")
if err != nil {
log.Println("Executable worker not found: darktable")
@ -54,6 +60,11 @@ func newDarktableWorker() *DarktableWorker {
}
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")
if err != nil {
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 {
go func(media *models.Media) {
if face_detection.GlobalFaceDetector == nil {
return
}
if err := face_detection.GlobalFaceDetector.DetectFaces(db, media); err != nil {
scanner_utils.ScannerError("Error detecting faces in image (%s): %s", media.Path, err)
}

View File

@ -1,6 +1,9 @@
package utils
import "os"
import (
"os"
"strings"
)
// EnvironmentVariable represents the name of an environment variable used to configure Photoview
type EnvironmentVariable string
@ -30,6 +33,13 @@ const (
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
func (v EnvironmentVariable) GetName() string {
return string(v)
@ -40,6 +50,20 @@ func (v EnvironmentVariable) GetValue() string {
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
func ShouldServeUI() bool {
return EnvServeUI.GetValue() == "1"

View File

@ -3,6 +3,7 @@ import { NavLink } from 'react-router-dom'
import { useQuery, gql } from '@apollo/client'
import { authToken } from '../../helpers/authentication'
import { useTranslation } from 'react-i18next'
import { mapboxEnabledQuery } from '../../__generated__/mapboxEnabledQuery'
export const MAPBOX_QUERY = gql`
query mapboxEnabledQuery {
@ -10,6 +11,14 @@ export const MAPBOX_QUERY = gql`
}
`
export const FACE_DETECTION_ENABLED_QUERY = gql`
query faceDetectionEnabled {
siteInfo {
faceDetectionEnabled
}
}
`
type MenuButtonProps = {
to: string
exact: boolean
@ -56,9 +65,16 @@ const MenuSeparator = () => (
export const MainMenu = () => {
const { t } = useTranslation()
const mapboxQuery = authToken() ? useQuery(MAPBOX_QUERY) : null
const mapboxQuery = authToken()
? useQuery<mapboxEnabledQuery>(MAPBOX_QUERY)
: null
const faceDetectionEnabledQuery = authToken()
? useQuery(FACE_DETECTION_ENABLED_QUERY)
: null
const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken
const faceDetectionEnabled =
!!faceDetectionEnabledQuery?.data?.siteInfo?.faceDetectionEnabled
return (
<div className="fixed w-full bottom-0 lg:bottom-auto lg:top-[84px] z-30 bg-white shadow-separator lg:shadow-none lg:w-[240px] lg:ml-8 lg:mr-5 flex-shrink-0">
@ -104,19 +120,21 @@ export const MainMenu = () => {
}
/>
) : null}
<MenuButton
to="/people"
exact
label={t('sidemenu.people', 'People')}
background="#fbcd78"
activeClasses="ring-[#fff7e4] bg-[#fff7e4]"
className="outline-none focus:ring-2 focus:ring-yellow-100 focus:ring-offset-2"
icon={
<svg viewBox="0 0 24 24" fill="white">
<path d="M15.713873,14.2127622 C17.4283917,14.8986066 18.9087267,16.0457918 20.0014344,17.5008819 C20,19.1568542 18.6568542,20.5 17,20.5 L7,20.5 C5.34314575,20.5 4,19.1568542 4,17.5 L4.09169034,17.3788798 C5.17486154,15.981491 6.62020934,14.878942 8.28693513,14.2120314 C9.30685583,15.018595 10.5972088,15.5 12,15.5 C13.3092718,15.5 14.5205974,15.0806428 15.5069849,14.3689203 L15.713873,14.2127622 L15.713873,14.2127622 Z M12,4 C15.0375661,4 17.5,6.46243388 17.5,9.5 C17.5,12.5375661 15.0375661,15 12,15 C8.96243388,15 6.5,12.5375661 6.5,9.5 C6.5,6.46243388 8.96243388,4 12,4 Z"></path>
</svg>
}
/>
{faceDetectionEnabled ? (
<MenuButton
to="/people"
exact
label={t('sidemenu.people', 'People')}
background="#fbcd78"
activeClasses="ring-[#fff7e4] bg-[#fff7e4]"
className="outline-none focus:ring-2 focus:ring-yellow-100 focus:ring-offset-2"
icon={
<svg viewBox="0 0 24 24" fill="white">
<path d="M15.713873,14.2127622 C17.4283917,14.8986066 18.9087267,16.0457918 20.0014344,17.5008819 C20,19.1568542 18.6568542,20.5 17,20.5 L7,20.5 C5.34314575,20.5 4,19.1568542 4,17.5 L4.09169034,17.3788798 C5.17486154,15.981491 6.62020934,14.878942 8.28693513,14.2120314 C9.30685583,15.018595 10.5972088,15.5 12,15.5 C13.3092718,15.5 14.5205974,15.0806428 15.5069849,14.3689203 L15.713873,14.2127622 L15.713873,14.2127622 Z M12,4 C15.0375661,4 17.5,6.46243388 17.5,9.5 C17.5,12.5375661 15.0375661,15 12,15 C8.96243388,15 6.5,12.5375661 6.5,9.5 C6.5,6.46243388 8.96243388,4 12,4 Z"></path>
</svg>
}
/>
) : null}
<MenuSeparator />
<MenuButton
to="/settings"

View File

@ -330,18 +330,6 @@ const flashLookup = (t: TranslationFn): { [key: number]: string } => {
}
}
// From https://exiftool.org/TagNames/EXIF.html
// const orientation = {
// 1: 'Horizontal (normal)',
// 2: 'Mirror horizontal',
// 3: 'Rotate 180',
// 4: 'Mirror vertical',
// 5: 'Mirror horizontal and rotate 270 CW',
// 6: 'Rotate 90 CW',
// 7: 'Mirror horizontal and rotate 90 CW',
// 8: 'Rotate 270 CW',
// }
type SidebarContentProps = {
media: MediaSidebarMedia
hidePreview?: boolean

View File

@ -107,7 +107,7 @@
}
},
"photos_page": {
"title": "Billeder"
"title": "Tidslinje"
},
"places_page": {
"title": "Kort"
@ -285,13 +285,13 @@
"sidemenu": {
"albums": "Albums",
"people": "Personer",
"photos": "Billeder",
"photos": "Tidslinje",
"places": "Kort",
"settings": "Indstillinger"
},
"title": {
"loading_album": "Loader album",
"login": "Logind",
"login": "Log ind",
"people": "Personer",
"settings": "Indstillinger"
}

View File

@ -107,7 +107,7 @@
}
},
"photos_page": {
"title": "Photos"
"title": "Timeline"
},
"places_page": {
"title": "Places"
@ -285,7 +285,7 @@
"sidemenu": {
"albums": "Albums",
"people": "People",
"photos": "Photos",
"photos": "Timeline",
"places": "Places",
"settings": "Settings"
},