From 732ccd05ab90546f77deb750ecb469f6240aa613 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 20 Dec 2020 17:10:00 +0100 Subject: [PATCH] restructure user <-> albums relations in database --- api/database/database.go | 1 + api/go.sum | 5 + api/graphql/generated.go | 260 ++++++++++++--------------------- api/graphql/models/media.go | 22 +-- api/graphql/models/user.go | 69 ++------- api/graphql/resolvers/media.go | 42 +++++- api/graphql/resolvers/user.go | 46 +----- api/graphql/schema.graphql | 14 +- api/scanner/scanner_user.go | 42 +++--- 9 files changed, 193 insertions(+), 308 deletions(-) diff --git a/api/database/database.go b/api/database/database.go index 6855a0b7..1ea5e1e0 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -60,6 +60,7 @@ func MigrateDatabase(db *gorm.DB) error { &models.MediaEXIF{}, &models.VideoMetadata{}, &models.ShareToken{}, + &models.UserMediaData{}, ) return nil diff --git a/api/go.sum b/api/go.sum index 4217c8fb..0d6b6c94 100644 --- a/api/go.sum +++ b/api/go.sum @@ -9,6 +9,7 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -64,16 +65,19 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns= @@ -103,6 +107,7 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 071d41cc..b03b397f 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -115,20 +115,19 @@ type ComplexityRoot struct { Mutation struct { AuthorizeUser func(childComplexity int, username string, password string) int - CreateUser func(childComplexity int, username string, rootPath string, password *string, admin bool) int + CreateUser func(childComplexity int, username string, password *string, admin bool) int DeleteShareToken func(childComplexity int, token string) int DeleteUser func(childComplexity int, id int) int FavoriteMedia func(childComplexity int, mediaID int, favorite bool) int InitialSetupWizard func(childComplexity int, username string, password string, rootPath string) int ProtectShareToken func(childComplexity int, token string, password *string) int - RegisterUser func(childComplexity int, username string, password string, rootPath string) int ScanAll func(childComplexity int) int ScanUser func(childComplexity int, userID int) int SetPeriodicScanInterval func(childComplexity int, interval int) int SetScannerConcurrentWorkers func(childComplexity int, workers int) int ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int - UpdateUser func(childComplexity int, id int, username *string, rootPath *string, password *string, admin *bool) int + UpdateUser func(childComplexity int, id int, username *string, password *string, admin *bool) int } Notification struct { @@ -193,8 +192,8 @@ type ComplexityRoot struct { User struct { Admin func(childComplexity int) int + Albums func(childComplexity int) int ID func(childComplexity int) int - RootPath func(childComplexity int) int Username func(childComplexity int) int } @@ -225,12 +224,13 @@ type MediaResolver interface { HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) + Favorite(ctx context.Context, obj *models.Media) (bool, error) + Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error) Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error) } type MutationResolver interface { AuthorizeUser(ctx context.Context, username string, password string) (*models.AuthorizeResult, error) - RegisterUser(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) InitialSetupWizard(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) ScanAll(ctx context.Context) (*models.ScannerResult, error) ScanUser(ctx context.Context, userID int) (*models.ScannerResult, error) @@ -239,8 +239,8 @@ type MutationResolver interface { DeleteShareToken(ctx context.Context, token string) (*models.ShareToken, error) ProtectShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) - UpdateUser(ctx context.Context, id int, username *string, rootPath *string, password *string, admin *bool) (*models.User, error) - CreateUser(ctx context.Context, username string, rootPath string, password *string, admin bool) (*models.User, error) + UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) + CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error) DeleteUser(ctx context.Context, id int) (*models.User, error) SetPeriodicScanInterval(ctx context.Context, interval int) (int, error) SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) @@ -622,7 +622,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.CreateUser(childComplexity, args["username"].(string), args["rootPath"].(string), args["password"].(*string), args["admin"].(bool)), true + return e.complexity.Mutation.CreateUser(childComplexity, args["username"].(string), args["password"].(*string), args["admin"].(bool)), true case "Mutation.deleteShareToken": if e.complexity.Mutation.DeleteShareToken == nil { @@ -684,18 +684,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ProtectShareToken(childComplexity, args["token"].(string), args["password"].(*string)), true - case "Mutation.registerUser": - if e.complexity.Mutation.RegisterUser == nil { - break - } - - args, err := ec.field_Mutation_registerUser_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.RegisterUser(childComplexity, args["username"].(string), args["password"].(string), args["rootPath"].(string)), true - case "Mutation.scanAll": if e.complexity.Mutation.ScanAll == nil { break @@ -773,7 +761,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.UpdateUser(childComplexity, args["id"].(int), args["username"].(*string), args["rootPath"].(*string), args["password"].(*string), args["admin"].(*bool)), true + return e.complexity.Mutation.UpdateUser(childComplexity, args["id"].(int), args["username"].(*string), args["password"].(*string), args["admin"].(*bool)), true case "Notification.content": if e.complexity.Notification.Content == nil { @@ -1100,6 +1088,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.Admin(childComplexity), true + case "User.albums": + if e.complexity.User.Albums == nil { + break + } + + return e.complexity.User.Albums(childComplexity), true + case "User.id": if e.complexity.User.ID == nil { break @@ -1107,13 +1102,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.ID(childComplexity), true - case "User.rootPath": - if e.complexity.User.RootPath == nil { - break - } - - return e.complexity.User.RootPath(childComplexity), true - case "User.username": if e.complexity.User.Username == nil { break @@ -1332,13 +1320,6 @@ type Query { type Mutation { authorizeUser(username: String!, password: String!): AuthorizeResult! - "Registers a new user, must be admin to call" - registerUser( - username: String! - password: String! - rootPath: String! - ): AuthorizeResult! - "Registers the initial user, can only be called if initialSetup from SiteInfo is true" initialSetupWizard( username: String! @@ -1366,13 +1347,11 @@ type Mutation { updateUser( id: ID! username: String - rootPath: String password: String admin: Boolean ): User @isAdmin createUser( username: String! - rootPath: String! password: String admin: Boolean! ): User @isAdmin @@ -1454,8 +1433,9 @@ type User { id: ID! username: String! #albums: [Album] - "Local filepath for the user's photos" - rootPath: String! @isAdmin + # rootPath: String! @isAdmin + "Top level albums owned by this user" + albums: [Album!]! @isAdmin admin: Boolean! #shareTokens: [ShareToken] } @@ -1653,33 +1633,24 @@ func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, } } args["username"] = arg0 - var arg1 string - if tmp, ok := rawArgs["rootPath"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootPath")) - arg1, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["rootPath"] = arg1 - var arg2 *string + var arg1 *string if tmp, ok := rawArgs["password"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) - arg2, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) if err != nil { return nil, err } } - args["password"] = arg2 - var arg3 bool + args["password"] = arg1 + var arg2 bool if tmp, ok := rawArgs["admin"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("admin")) - arg3, err = ec.unmarshalNBoolean2bool(ctx, tmp) + arg2, err = ec.unmarshalNBoolean2bool(ctx, tmp) if err != nil { return nil, err } } - args["admin"] = arg3 + args["admin"] = arg2 return args, nil } @@ -1794,39 +1765,6 @@ func (ec *executionContext) field_Mutation_protectShareToken_args(ctx context.Co return args, nil } -func (ec *executionContext) field_Mutation_registerUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 string - if tmp, ok := rawArgs["username"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("username")) - arg0, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["username"] = arg0 - var arg1 string - if tmp, ok := rawArgs["password"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) - arg1, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["password"] = arg1 - var arg2 string - if tmp, ok := rawArgs["rootPath"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootPath")) - arg2, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["rootPath"] = arg2 - return args, nil -} - func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1960,32 +1898,23 @@ func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context, } args["username"] = arg1 var arg2 *string - if tmp, ok := rawArgs["rootPath"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootPath")) + if tmp, ok := rawArgs["password"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) arg2, err = ec.unmarshalOString2ᚖstring(ctx, tmp) if err != nil { return nil, err } } - args["rootPath"] = arg2 - var arg3 *string - if tmp, ok := rawArgs["password"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) - arg3, err = ec.unmarshalOString2ᚖstring(ctx, tmp) - if err != nil { - return nil, err - } - } - args["password"] = arg3 - var arg4 *bool + args["password"] = arg2 + var arg3 *bool if tmp, ok := rawArgs["admin"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("admin")) - arg4, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) + arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) if err != nil { return nil, err } } - args["admin"] = arg4 + args["admin"] = arg3 return args, nil } @@ -3011,14 +2940,14 @@ func (ec *executionContext) _Media_favorite(ctx context.Context, field graphql.C Object: "Media", Field: field, Args: nil, - IsMethod: false, - IsResolver: false, + 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 obj.Favorite, nil + return ec.resolvers.Media().Favorite(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -3782,48 +3711,6 @@ func (ec *executionContext) _Mutation_authorizeUser(ctx context.Context, field g return ec.marshalNAuthorizeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx, field.Selections, res) } -func (ec *executionContext) _Mutation_registerUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Mutation", - Field: field, - Args: nil, - IsMethod: true, - IsResolver: true, - } - - ctx = graphql.WithFieldContext(ctx, fc) - rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Mutation_registerUser_args(ctx, rawArgs) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - fc.Args = args - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().RegisterUser(rctx, args["username"].(string), args["password"].(string), args["rootPath"].(string)) - }) - 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.(*models.AuthorizeResult) - fc.Result = res - return ec.marshalNAuthorizeResult2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx, field.Selections, res) -} - func (ec *executionContext) _Mutation_initialSetupWizard(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4181,7 +4068,7 @@ func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field grap resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateUser(rctx, args["id"].(int), args["username"].(*string), args["rootPath"].(*string), args["password"].(*string), args["admin"].(*bool)) + return ec.resolvers.Mutation().UpdateUser(rctx, args["id"].(int), args["username"].(*string), args["password"].(*string), args["admin"].(*bool)) } directive1 := func(ctx context.Context) (interface{}, error) { if ec.directives.IsAdmin == nil { @@ -4240,7 +4127,7 @@ func (ec *executionContext) _Mutation_createUser(ctx context.Context, field grap resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateUser(rctx, args["username"].(string), args["rootPath"].(string), args["password"].(*string), args["admin"].(bool)) + return ec.resolvers.Mutation().CreateUser(rctx, args["username"].(string), args["password"].(*string), args["admin"].(bool)) } directive1 := func(ctx context.Context) (interface{}, error) { if ec.directives.IsAdmin == nil { @@ -6031,7 +5918,7 @@ func (ec *executionContext) _User_username(ctx context.Context, field graphql.Co return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { +func (ec *executionContext) _User_albums(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -6050,7 +5937,7 @@ func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.Co resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.RootPath, nil + return obj.Albums, nil } directive1 := func(ctx context.Context) (interface{}, error) { if ec.directives.IsAdmin == nil { @@ -6066,10 +5953,10 @@ func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.Co if tmp == nil { return nil, nil } - if data, ok := tmp.(string); ok { + if data, ok := tmp.([]models.Album); ok { return data, nil } - return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp) + return nil, fmt.Errorf(`unexpected type %T from directive, should be []github.com/photoview/photoview/api/graphql/models.Album`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6081,9 +5968,9 @@ func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.Co } return graphql.Null } - res := resTmp.(string) + res := resTmp.([]models.Album) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNAlbum2ᚕgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, field.Selections, res) } func (ec *executionContext) _User_admin(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { @@ -7809,10 +7696,19 @@ func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, ob case "videoMetadata": out.Values[i] = ec._Media_videoMetadata(ctx, field, obj) case "favorite": - out.Values[i] = ec._Media_favorite(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&invalids, 1) - } + 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._Media_favorite(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "type": out.Values[i] = ec._Media_type(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -8003,11 +7899,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } - case "registerUser": - out.Values[i] = ec._Mutation_registerUser(ctx, field) - if out.Values[i] == graphql.Null { - invalids++ - } case "initialSetupWizard": out.Values[i] = ec._Mutation_initialSetupWizard(ctx, field) case "scanAll": @@ -8530,8 +8421,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } - case "rootPath": - out.Values[i] = ec._User_rootPath(ctx, field, obj) + case "albums": + out.Values[i] = ec._User_albums(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } @@ -8857,6 +8748,43 @@ func (ec *executionContext) marshalNAlbum2githubᚗcomᚋphotoviewᚋphotoview return ec._Album(ctx, sel, &v) } +func (ec *executionContext) marshalNAlbum2ᚕgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx context.Context, sel ast.SelectionSet, v []models.Album) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNAlbum2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + func (ec *executionContext) marshalNAlbum2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.Album) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index 2fe9eb28..ecaf12e7 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -13,17 +13,17 @@ import ( type Media struct { Model - Title string `gorm:"not null"` - Path string `gorm:"not null"` - PathHash string `gorm:"not null"` - AlbumID int `gorm:"not null"` - Album Album - ExifID *int - Exif *MediaEXIF - MediaURL []MediaURL - DateShot time.Time `gorm:"not null"` - DateImported time.Time `gorm:"not null"` - Favorite bool `gorm:"not null, default:false"` + Title string `gorm:"not null"` + Path string `gorm:"not null"` + PathHash string `gorm:"not null"` + AlbumID int `gorm:"not null"` + Album Album + ExifID *int + Exif *MediaEXIF + MediaURL []MediaURL + DateShot time.Time `gorm:"not null"` + DateImported time.Time `gorm:"not null"` + // Favorite bool `gorm:"not null, default:false"` Type MediaType `gorm:"not null"` VideoMetadataID *int VideoMetadata *VideoMetadata diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go index 7b2f1432..94b1a1c4 100644 --- a/api/graphql/models/user.go +++ b/api/graphql/models/user.go @@ -16,13 +16,17 @@ type User struct { Model Username string `gorm:"unique,size:128"` Password *string `gorm:"size:256` - RootPath string `gorm:"size:512` - Admin bool `gorm:"default:false"` + // RootPath string `gorm:"size:512` + Albums []Album `gorm:"many2many:user_albums"` + Admin bool `gorm:"default:false"` } -// func (u *User) ID() int { -// return u.UserID -// } +type UserMediaData struct { + ModelTimestamps + UserID int `gorm:"primaryKey;autoIncrement:false"` + MediaID int `gorm:"primaryKey;autoIncrement:false"` + Favorite bool `gorm:"not null;default:false"` +} type AccessToken struct { Model @@ -34,35 +38,7 @@ type AccessToken struct { var ErrorInvalidUserCredentials = errors.New("invalid credentials") -// func NewUserFromRow(row *sql.Row) (*User, error) { -// user := User{} - -// if err := row.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil { -// return nil, errors.Wrap(err, "failed to scan user from database") -// } - -// return &user, nil -// } - -// func NewUsersFromRows(rows *sql.Rows) ([]*User, error) { -// users := make([]*User, 0) - -// for rows.Next() { -// var user User -// if err := rows.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil { -// return nil, errors.Wrap(err, "failed to scan users from database") -// } -// users = append(users, &user) -// } - -// rows.Close() - -// return users, nil -// } - func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) { - // row := database.QueryRow("SELECT * FROM user WHERE username = ?", username) - var user User result := db.Where("username = ?", username).First(&user) @@ -100,15 +76,15 @@ func ValidRootPath(rootPath string) bool { return true } -func RegisterUser(db *gorm.DB, username string, password *string, rootPath string, admin bool) (*User, error) { - if !ValidRootPath(rootPath) { - return nil, ErrorInvalidRootPath - } +func RegisterUser(db *gorm.DB, username string, password *string, admin bool) (*User, error) { + // if !ValidRootPath(rootPath) { + // return nil, ErrorInvalidRootPath + // } user := User{ Username: username, - RootPath: rootPath, - Admin: admin, + // RootPath: rootPath, + Admin: admin, } if password != nil { @@ -163,27 +139,12 @@ func (user *User) GenerateAccessToken(db *gorm.DB) (*AccessToken, error) { func VerifyTokenAndGetUser(db *gorm.DB, token string) (*User, error) { - // row := database.QueryRow("SELECT (user_id) FROM access_token WHERE expire > ? AND value = ?", now, token) - var accessToken AccessToken result := db.Where("expire > ? AND value = ?", time.Now(), token).First(&accessToken) if result.Error != nil { return nil, result.Error } - // var userId string - - // if err := row.Scan(&userId); err != nil { - // log.Println(err.Error()) - // return nil, err - // } - - // row = db.QueryRow("SELECT * FROM user WHERE user_id = ?", userId) - // user, err := NewUserFromRow(row) - // if err != nil { - // return nil, err - // } - var user User result = db.First(&user, accessToken.UserID) if result.Error != nil { diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go index d7d00024..a6b82313 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -9,6 +9,7 @@ import ( "github.com/photoview/photoview/api/scanner" "github.com/pkg/errors" "gorm.io/gorm" + "gorm.io/gorm/clause" ) func (r *queryResolver) MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error) { @@ -182,20 +183,45 @@ func (r *mediaResolver) VideoWeb(ctx context.Context, media *models.Media) (*mod return &url, nil } +func (r *mediaResolver) Favorite(ctx context.Context, media *models.Media) (bool, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return false, auth.ErrUnauthorized + } + + userMediaData := models.UserMediaData{ + UserID: user.ID, + MediaID: media.ID, + Favorite: false, + } + + if err := r.Database.FirstOrInit(&userMediaData).Error; err != nil { + return false, errors.Wrapf(err, "get user media data from database (user: %d, media: %d)", user.ID, media.ID) + } + + return userMediaData.Favorite, nil +} + func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) { user := auth.UserFromContext(ctx) - - var media models.Media - - if err := r.Database.Joins("Album").Where("Album.owner_id = ?", user.ID).First(&media, mediaID).Error; err != nil { - return nil, err + if user == nil { + return nil, auth.ErrUnauthorized } - media.Favorite = favorite + userMediaData := models.UserMediaData{ + UserID: user.ID, + MediaID: mediaID, + Favorite: favorite, + } - if err := r.Database.Save(&media).Error; err != nil { - return nil, errors.Wrap(err, "failed to update media favorite on database") + if err := r.Database.Clauses(clause.OnConflict{UpdateAll: true}).Create(&userMediaData).Error; err != nil { + return nil, errors.Wrapf(err, "update user favorite media in database") + } + + var media models.Media + if err := r.Database.First(&media, mediaID).Error; err != nil { + return nil, errors.Wrap(err, "get media from database after favorite update") } return &media, nil diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go index 50c0e986..f6d594d5 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -61,38 +61,6 @@ func (r *mutationResolver) AuthorizeUser(ctx context.Context, username string, p Token: &token.Value, }, nil } -func (r *mutationResolver) RegisterUser(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) { - - var token *models.AccessToken - - transactionError := r.Database.Transaction(func(tx *gorm.DB) error { - user, err := models.RegisterUser(tx, username, &password, rootPath, false) - if err != nil { - return err - } - - token, err = user.GenerateAccessToken(tx) - if err != nil { - tx.Rollback() - return err - } - - return nil - }) - - if transactionError != nil { - return &models.AuthorizeResult{ - Success: false, - Status: transactionError.Error(), - }, transactionError - } - - return &models.AuthorizeResult{ - Success: true, - Status: "ok", - Token: &token.Value, - }, nil -} func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) { siteInfo, err := models.GetSiteInfo(r.Database) @@ -111,7 +79,7 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri return err } - user, err := models.RegisterUser(tx, username, &password, rootPath, true) + user, err := models.RegisterUser(tx, username, &password, true) if err != nil { return err } @@ -139,9 +107,9 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri } // Admin queries -func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, rootPath *string, password *string, admin *bool) (*models.User, error) { +func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) { - if username == nil && rootPath == nil && password == nil && admin == nil { + if username == nil && password == nil && admin == nil { return nil, errors.New("no updates requested") } @@ -154,10 +122,6 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *str user.Username = *username } - if rootPath != nil { - user.RootPath = *rootPath - } - if password != nil { hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12) if err != nil { @@ -179,13 +143,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *str return &user, nil } -func (r *mutationResolver) CreateUser(ctx context.Context, username string, rootPath string, password *string, admin bool) (*models.User, error) { +func (r *mutationResolver) CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error) { var user *models.User transactionError := r.Database.Transaction(func(tx *gorm.DB) error { var err error - user, err = models.RegisterUser(tx, username, password, rootPath, admin) + user, err = models.RegisterUser(tx, username, password, admin) if err != nil { return err } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 3c8e95cf..38bd197c 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -58,13 +58,6 @@ type Query { type Mutation { authorizeUser(username: String!, password: String!): AuthorizeResult! - "Registers a new user, must be admin to call" - registerUser( - username: String! - password: String! - rootPath: String! - ): AuthorizeResult! - "Registers the initial user, can only be called if initialSetup from SiteInfo is true" initialSetupWizard( username: String! @@ -92,13 +85,11 @@ type Mutation { updateUser( id: ID! username: String - rootPath: String password: String admin: Boolean ): User @isAdmin createUser( username: String! - rootPath: String! password: String admin: Boolean! ): User @isAdmin @@ -180,8 +171,9 @@ type User { id: ID! username: String! #albums: [Album] - "Local filepath for the user's photos" - rootPath: String! @isAdmin + # rootPath: String! @isAdmin + "Top level albums owned by this user" + albums: [Album!]! @isAdmin admin: Boolean! #shareTokens: [ShareToken] } diff --git a/api/scanner/scanner_user.go b/api/scanner/scanner_user.go index 1b1a322e..ca1b157d 100644 --- a/api/scanner/scanner_user.go +++ b/api/scanner/scanner_user.go @@ -17,29 +17,37 @@ import ( func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScannerCache) ([]*models.Album, []error) { - // Check if user directory exists on the file system - if _, err := os.Stat(user.RootPath); err != nil { - if os.IsNotExist(err) { - return nil, []error{errors.Errorf("Photo directory for user '%s' does not exist '%s'\n", user.Username, user.RootPath)} - } else { - return nil, []error{errors.Errorf("Could not read photo directory for user '%s': %s\n", user.Username, user.RootPath)} - } + var userRootAlbums []*models.Album + if err := db.Model(&user).Association("Albums").Find(&userRootAlbums); err != nil { + return nil, []error{errors.Wrapf(err, "get albums of user (%s)", user.Username)} } + scanErrors := make([]error, 0) + type scanInfo struct { path string parentID *int } scanQueue := list.New() - scanQueue.PushBack(scanInfo{ - path: user.RootPath, - parentID: nil, - }) + + for _, album := range userRootAlbums { + // Check if user album directory exists on the file system + if _, err := os.Stat(album.Path); err != nil { + if os.IsNotExist(err) { + scanErrors = append(scanErrors, errors.Errorf("Album directory for user '%s' does not exist '%s'\n", user.Username, album.Path)) + } else { + scanErrors = append(scanErrors, errors.Errorf("Could not read album directory for user '%s': %s\n", user.Username, album.Path)) + } + } else { + scanQueue.PushBack(scanInfo{ + path: album.Path, + parentID: nil, + }) + } + } userAlbums := make([]*models.Album, 0) - albumErrors := make([]error, 0) - // newPhotos := make([]*models.Photo, 0) for scanQueue.Front() != nil { albumInfo := scanQueue.Front().Value.(scanInfo) @@ -51,7 +59,7 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner // Read path dirContent, err := ioutil.ReadDir(albumPath) if err != nil { - albumErrors = append(albumErrors, errors.Wrapf(err, "read directory (%s)", albumPath)) + scanErrors = append(scanErrors, errors.Wrapf(err, "read directory (%s)", albumPath)) continue } @@ -81,7 +89,7 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner }) if transErr != nil { - albumErrors = append(albumErrors, errors.Wrap(transErr, "begin database transaction")) + scanErrors = append(scanErrors, errors.Wrap(transErr, "begin database transaction")) continue } @@ -104,9 +112,9 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner } deleteErrors := deleteOldUserAlbums(db, userAlbums, user) - albumErrors = append(albumErrors, deleteErrors...) + scanErrors = append(scanErrors, deleteErrors...) - return userAlbums, albumErrors + return userAlbums, scanErrors } func directoryContainsPhotos(rootPath string, cache *AlbumScannerCache) bool {