1
Fork 0

Merge pull request #157 from photoview/v2/user-multi-paths

Multiple and shared user paths
This commit is contained in:
Viktor Strate Kløvedal 2021-01-06 17:28:49 +01:00 committed by GitHub
commit 8700f98fd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1416 additions and 703 deletions

View File

@ -60,6 +60,7 @@ func MigrateDatabase(db *gorm.DB) error {
&models.MediaEXIF{},
&models.VideoMetadata{},
&models.ShareToken{},
&models.UserMediaData{},
)
return nil

View File

@ -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=

View File

@ -24,6 +24,9 @@ models:
model: github.com/99designs/gqlgen/graphql.IntID
User:
model: github.com/photoview/photoview/api/graphql/models.User
fields:
albums:
resolver: true
Media:
model: github.com/photoview/photoview/api/graphql/models.Media
MediaURL:

View File

@ -44,6 +44,7 @@ type ResolverRoot interface {
Query() QueryResolver
ShareToken() ShareTokenResolver
Subscription() SubscriptionResolver
User() UserResolver
}
type DirectiveRoot struct {
@ -115,20 +116,21 @@ 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
UserAddRootPath func(childComplexity int, id int, rootPath string) int
UserRemoveRootAlbum func(childComplexity int, userID int, albumID int) int
}
Notification struct {
@ -192,10 +194,11 @@ type ComplexityRoot struct {
}
User struct {
Admin func(childComplexity int) int
ID func(childComplexity int) int
RootPath func(childComplexity int) int
Username func(childComplexity int) int
Admin func(childComplexity int) int
Albums func(childComplexity int) int
ID func(childComplexity int) int
RootAlbums func(childComplexity int) int
Username func(childComplexity int) int
}
VideoMetadata struct {
@ -216,6 +219,8 @@ type AlbumResolver interface {
Media(ctx context.Context, obj *models.Album, filter *models.Filter, onlyFavorites *bool) ([]*models.Media, error)
SubAlbums(ctx context.Context, obj *models.Album, filter *models.Filter) ([]*models.Album, error)
Owner(ctx context.Context, obj *models.Album) (*models.User, error)
Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error)
Path(ctx context.Context, obj *models.Album) ([]*models.Album, error)
Shares(ctx context.Context, obj *models.Album) ([]*models.ShareToken, error)
@ -225,12 +230,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,9 +245,11 @@ 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)
UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error)
UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error)
SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
}
@ -266,6 +274,10 @@ type ShareTokenResolver interface {
type SubscriptionResolver interface {
Notification(ctx context.Context) (<-chan *models.Notification, error)
}
type UserResolver interface {
Albums(ctx context.Context, obj *models.User) ([]*models.Album, error)
RootAlbums(ctx context.Context, obj *models.User) ([]*models.Album, error)
}
type executableSchema struct {
resolvers ResolverRoot
@ -622,7 +634,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 +696,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 +773,31 @@ 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 "Mutation.userAddRootPath":
if e.complexity.Mutation.UserAddRootPath == nil {
break
}
args, err := ec.field_Mutation_userAddRootPath_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UserAddRootPath(childComplexity, args["id"].(int), args["rootPath"].(string)), true
case "Mutation.userRemoveRootAlbum":
if e.complexity.Mutation.UserRemoveRootAlbum == nil {
break
}
args, err := ec.field_Mutation_userRemoveRootAlbum_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UserRemoveRootAlbum(childComplexity, args["userId"].(int), args["albumId"].(int)), true
case "Notification.content":
if e.complexity.Notification.Content == nil {
@ -1100,6 +1124,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,12 +1138,12 @@ 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 {
case "User.rootAlbums":
if e.complexity.User.RootAlbums == nil {
break
}
return e.complexity.User.RootPath(childComplexity), true
return e.complexity.User.RootAlbums(childComplexity), true
case "User.username":
if e.complexity.User.Username == nil {
@ -1332,13 +1363,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,18 +1390,20 @@ 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
deleteUser(id: ID!): User @isAdmin
"Add a root path from where to look for media for the given user"
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
userRemoveRootAlbum(userId: ID!, albumId: ID!): Album @isAdmin
"""
Set how often, in seconds, the server should automatically scan for new media,
a value of 0 will disable periodic scans
@ -1454,8 +1480,11 @@ type User {
id: ID!
username: String!
#albums: [Album]
"Local filepath for the user's photos"
rootPath: String! @isAdmin
# rootPath: String! @isAdmin
"All albums owned by this user"
albums: [Album!]! @isAdmin
"Top level albums owned by this user"
rootAlbums: [Album!]! @isAdmin
admin: Boolean!
#shareTokens: [ShareToken]
}
@ -1653,33 +1682,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 +1814,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 +1947,71 @@ 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
}
func (ec *executionContext) field_Mutation_userAddRootPath_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["id"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["id"] = 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
return args, nil
}
func (ec *executionContext) field_Mutation_userRemoveRootAlbum_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["userId"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userId"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["userId"] = arg0
var arg1 int
if tmp, ok := rawArgs["albumId"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("albumId"))
arg1, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["albumId"] = arg1
return args, nil
}
@ -2437,14 +2463,14 @@ func (ec *executionContext) _Album_owner(ctx context.Context, field graphql.Coll
Object: "Album",
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.Owner, nil
return ec.resolvers.Album().Owner(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@ -2456,9 +2482,9 @@ func (ec *executionContext) _Album_owner(ctx context.Context, field graphql.Coll
}
return graphql.Null
}
res := resTmp.(models.User)
res := resTmp.(*models.User)
fc.Result = res
return ec.marshalNUser2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
return ec.marshalNUser2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
}
func (ec *executionContext) _Album_filePath(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) {
@ -3011,14 +3037,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 +3808,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 +4165,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 +4224,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 {
@ -4332,6 +4316,124 @@ func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field grap
return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_userAddRootPath(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_userAddRootPath_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) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UserAddRootPath(rctx, args["id"].(int), args["rootPath"].(string))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.Album); ok {
return data, nil
}
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)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_userRemoveRootAlbum(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_userRemoveRootAlbum_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) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UserRemoveRootAlbum(rctx, args["userId"].(int), args["albumId"].(int))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.Album); ok {
return data, nil
}
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)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_setPeriodicScanInterval(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -6031,7 +6133,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))
@ -6042,15 +6144,15 @@ func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.Co
Object: "User",
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) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.RootPath, nil
return ec.resolvers.User().Albums(rctx, obj)
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
@ -6066,10 +6168,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 +6183,64 @@ 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_rootAlbums(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))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "User",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
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.User().RootAlbums(rctx, obj)
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, obj, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.([]*models.Album); ok {
return data, nil
}
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)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*models.Album)
fc.Result = 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) {
@ -7647,10 +7804,19 @@ func (ec *executionContext) _Album(ctx context.Context, sel ast.SelectionSet, ob
case "parentAlbum":
out.Values[i] = ec._Album_parentAlbum(ctx, field, obj)
case "owner":
out.Values[i] = ec._Album_owner(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._Album_owner(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "filePath":
out.Values[i] = ec._Album_filePath(ctx, field, obj)
if out.Values[i] == graphql.Null {
@ -7809,10 +7975,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 +8178,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":
@ -8036,6 +8206,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
out.Values[i] = ec._Mutation_createUser(ctx, field)
case "deleteUser":
out.Values[i] = ec._Mutation_deleteUser(ctx, field)
case "userAddRootPath":
out.Values[i] = ec._Mutation_userAddRootPath(ctx, field)
case "userRemoveRootAlbum":
out.Values[i] = ec._Mutation_userRemoveRootAlbum(ctx, field)
case "setPeriodicScanInterval":
out.Values[i] = ec._Mutation_setPeriodicScanInterval(ctx, field)
if out.Values[i] == graphql.Null {
@ -8523,22 +8697,45 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
case "id":
out.Values[i] = ec._User_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "username":
out.Values[i] = ec._User_username(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "rootPath":
out.Values[i] = ec._User_rootPath(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
case "albums":
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._User_albums(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "rootAlbums":
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._User_rootAlbums(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "admin":
out.Values[i] = ec._User_admin(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
atomic.AddUint32(&invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))

View File

@ -11,11 +11,12 @@ type Album struct {
Model
Title string `gorm:"not null"`
ParentAlbumID *int
ParentAlbum *Album
OwnerID int `gorm:"not null"`
Owner User
Path string `gorm:"not null"`
PathHash string `gorm:"unique"`
ParentAlbum *Album `gorm:"constraint:OnDelete:SET NULL;"`
// OwnerID int `gorm:"not null"`
// Owner User
Owners []User `gorm:"many2many:user_albums"`
Path string `gorm:"not null"`
PathHash string `gorm:"unique"`
}
func (a *Album) FilePath() string {
@ -27,3 +28,17 @@ func (a *Album) BeforeSave(tx *gorm.DB) (err error) {
a.PathHash = hex.EncodeToString(hash[:])
return nil
}
func (a *Album) GetChildren(db *gorm.DB) (children []*Album, err error) {
err = db.Raw(`
WITH recursive sub_albums AS (
SELECT * FROM albums AS root WHERE id = ?
UNION ALL
SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id
)
SELECT * FROM sub_albums
`, a.ID).Find(&children).Error
return children, err
}

View File

@ -2,8 +2,6 @@ package models
import (
"time"
"gorm.io/gorm"
)
type Model struct {
@ -14,5 +12,4 @@ type Model struct {
type ModelTimestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}

View File

@ -13,20 +13,20 @@ 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 `gorm:"constraint:OnDelete:CASCADE;"`
ExifID *int
Exif *MediaEXIF `gorm:"constraint:OnDelete:SET NULL;"`
MediaURL []MediaURL `gorm:"constraint:OnDelete:CASCADE;"`
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
VideoMetadata *VideoMetadata `gorm:"constraint:OnDelete:SET NULL;"`
SideCarPath *string
SideCarHash *string
@ -52,6 +52,18 @@ func (m *Media) BeforeSave(tx *gorm.DB) error {
return nil
}
func (m *Media) BeforeDelete(tx *gorm.DB) error {
if err := tx.Model(m).Association("Exif").Clear(); err != nil {
return err
}
if err := tx.Model(m).Association("MediaURL").Clear(); err != nil {
return err
}
return nil
}
type MediaPurpose string
const (
@ -64,8 +76,8 @@ const (
type MediaURL struct {
Model
MediaID int `gorm:"not null"`
Media Media
MediaID int `gorm:"not null"`
Media Media `gorm:"constraint:OnDelete:CASCADE;"`
MediaName string `gorm:"not null"`
Width int `gorm:"not null"`
Height int `gorm:"not null"`

View File

@ -8,13 +8,13 @@ type ShareToken struct {
Model
Value string `gorm:"not null"`
OwnerID int `gorm:"not null"`
Owner User
Owner User `gorm:"constraint:OnDelete:CASCADE;"`
Expire *time.Time
Password *string
AlbumID *int
Album *Album
Album *Album `gorm:"constraint:OnDelete:CASCADE;"`
MediaID *int
Media *Media
Media *Media `gorm:"constraint:OnDelete:CASCADE;"`
}
func (share *ShareToken) Token() string {

View File

@ -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 {
@ -192,3 +153,30 @@ func VerifyTokenAndGetUser(db *gorm.DB, token string) (*User, error) {
return &user, nil
}
// FillAlbums fill user.Albums with albums from database
func (user *User) FillAlbums(db *gorm.DB) error {
// Albums already present
if len(user.Albums) > 0 {
return nil
}
if err := db.Model(&user).Association("Albums").Find(&user.Albums); err != nil {
return errors.Wrap(err, "fill user albums")
}
return nil
}
func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) {
// user.QueryUserAlbums(db, db.Where("id = ?", album.ID))
// TODO: Implement this
return true, nil
}
func (user *User) OwnsMedia(db *gorm.DB, media *Media) (bool, error) {
// TODO: implement this
return true, nil
}

View File

@ -2,10 +2,12 @@ package resolvers
import (
"context"
"errors"
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"gorm.io/gorm"
)
func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
@ -14,10 +16,19 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onl
return nil, auth.ErrUnauthorized
}
query := r.Database.Where("owner_id = ?", user.ID)
if err := user.FillAlbums(r.Database); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := r.Database.Model(models.Album{}).Where("id IN (?)", userAlbumIDs)
if onlyRoot != nil && *onlyRoot == true {
query = query.Where("parent_album_id = (?)", r.Database.Model(&models.Album{}).Select("id").Where("parent_album_id IS NULL AND owner_id = ?", user.ID))
query = query.Where("parent_album_id IS NULL")
}
if showEmpty == nil || *showEmpty == false {
@ -33,7 +44,7 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter, onl
query = filter.FormatSQL(query)
var albums []*models.Album
if err := query.Find(&albums).Error; err != nil {
if err := query.Scan(&albums).Error; err != nil {
return nil, err
}
@ -47,10 +58,22 @@ func (r *queryResolver) Album(ctx context.Context, id int) (*models.Album, error
}
var album models.Album
if err := r.Database.Where("owner_id = ?", user.ID).First(&album, id).Error; err != nil {
if err := r.Database.First(&album, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("album not found")
}
return nil, err
}
ownsAlbum, err := user.OwnsAlbum(r.Database, &album)
if err != nil {
return nil, err
}
if !ownsAlbum {
return nil, errors.New("forbidden")
}
return &album, nil
}
@ -154,8 +177,24 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.
UNION
SELECT parent.* FROM path_albums child JOIN albums parent ON parent.id = child.parent_album_id
)
SELECT * FROM path_albums WHERE id != ? AND owner_id = ?
`, obj.ID, obj.ID, user.ID).Scan(&album_path).Error
SELECT * FROM path_albums WHERE id != ?
`, obj.ID, obj.ID).Scan(&album_path).Error
// Make sure to only return albums this user owns
for i := len(album_path) - 1; i >= 0; i-- {
album := album_path[i]
owns, err := user.OwnsAlbum(r.Database, album)
if err != nil {
return nil, err
}
if !owns {
album_path = album_path[i+1:]
break
}
}
if err != nil {
return nil, err

View File

@ -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) {
@ -17,11 +18,20 @@ func (r *queryResolver) MyMedia(ctx context.Context, filter *models.Filter) ([]*
return nil, errors.New("unauthorized")
}
if err := user.FillAlbums(r.Database); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
var media []*models.Media
query := r.Database.
Joins("Album").
Where("albums.owner_id = ?", user.ID).
Where("albums.id IN (?)", userAlbumIDs).
Where("media.id IN (?)", r.Database.Model(&models.MediaURL{}).Select("id").Where("media_url.media_id = media.id"))
query = filter.FormatSQL(query)
@ -44,7 +54,7 @@ func (r *queryResolver) Media(ctx context.Context, id int) (*models.Media, error
err := r.Database.
Joins("Album").
Where("media.id = ?", id).
Where("Album.owner_id = ?", user.ID).
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = Album.id AND user_albums.user_id = ?)", user.ID).
Where("media.id IN (?)", r.Database.Model(&models.MediaURL{}).Select("media_id").Where("media_urls.media_id = media.id")).
First(&media).Error
@ -182,20 +192,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

View File

@ -28,7 +28,6 @@ func (r *mutationResolver) ScanAll(ctx context.Context) (*models.ScannerResult,
func (r *mutationResolver) ScanUser(ctx context.Context, userID int) (*models.ScannerResult, error) {
var user models.User
if err := r.Database.First(&user, userID).Error; err != nil {
return nil, errors.Wrap(err, "get user from database")
}

View File

@ -100,7 +100,12 @@ func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *
}
var count int64
if err := r.Database.Model(&models.Album{}).Where("owner_id = ?", user.ID).Count(&count).Error; err != nil {
err := r.Database.
Model(&models.Album{}).
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = albums.id AND user_albums.user_id = ?)", user.ID).
Count(&count).Error
if err != nil {
return nil, errors.Wrap(err, "failed to validate album owner with database")
}
@ -142,7 +147,12 @@ func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *
var media models.Media
if err := r.Database.Joins("Album").Where("Album.owner_id = ?", user.ID).First(&media, mediaID).Error; err != nil {
err := r.Database.Joins("Album").
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = Album.id AND user_albums.user_id = ?)", user.ID).
First(&media, mediaID).
Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, auth.ErrUnauthorized
} else {
@ -150,17 +160,6 @@ func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *
}
}
var count int64
err := r.Database.Raw("SELECT owner_id FROM albums, media WHERE media.id = ? AND media.album_id = albums.id AND albums.owner_id = ?", mediaID, user.ID).Count(&count).Error
if err != nil {
return nil, errors.Wrap(err, "error validating owner of media with database")
}
if count == 0 {
return nil, auth.ErrUnauthorized
}
hashedPassword, err := hashSharePassword(password)
if err != nil {
return nil, err

View File

@ -2,14 +2,29 @@ package resolvers
import (
"context"
"fmt"
"os"
"path"
"strconv"
"strings"
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"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type userResolver struct {
*Resolver
}
func (r *Resolver) User() api.UserResolver {
return &userResolver{r}
}
func (r *queryResolver) User(ctx context.Context, filter *models.Filter) ([]*models.User, error) {
var users []*models.User
@ -21,6 +36,30 @@ func (r *queryResolver) User(ctx context.Context, filter *models.Filter) ([]*mod
return users, nil
}
func (r *userResolver) Albums(ctx context.Context, user *models.User) ([]*models.Album, error) {
user.FillAlbums(r.Database)
pointerAlbums := make([]*models.Album, len(user.Albums))
for i, album := range user.Albums {
pointerAlbums[i] = &album
}
return pointerAlbums, nil
}
func (r *userResolver) RootAlbums(ctx context.Context, user *models.User) (albums []*models.Album, err error) {
err = r.Database.Model(&user).
Where("albums.parent_album_id NOT IN (?)",
r.Database.Table("user_albums").
Select("albums.id").
Joins("JOIN albums ON albums.id = user_albums.album_id AND user_albums.user_id = ?", user.ID),
).Or("albums.parent_album_id IS NULL").
Association("Albums").Find(&albums)
return
}
func (r *queryResolver) MyUser(ctx context.Context) (*models.User, error) {
user := auth.UserFromContext(ctx)
@ -61,38 +100,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)
@ -104,6 +111,8 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri
return nil, errors.New("not initial setup")
}
rootPath = path.Clean(rootPath)
var token *models.AccessToken
transactionError := r.Database.Transaction(func(tx *gorm.DB) error {
@ -111,7 +120,12 @@ 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
}
_, err = scanner.NewRootAlbum(tx, rootPath, user)
if err != nil {
return err
}
@ -139,9 +153,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 +168,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 +189,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
}
@ -214,3 +224,124 @@ func (r *mutationResolver) DeleteUser(ctx context.Context, id int) (*models.User
return &user, nil
}
func (r *mutationResolver) UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error) {
rootPath = path.Clean(rootPath)
var user models.User
if err := r.Database.First(&user, id).Error; err != nil {
return nil, err
}
if !models.ValidRootPath(rootPath) {
return nil, errors.New("invalid root path")
}
upperPaths := make([]string, 1)
upperPath := rootPath
upperPaths[0] = upperPath
for {
substrIndex := strings.LastIndex(upperPath, "/")
if substrIndex == -1 {
break
}
if substrIndex == 0 {
upperPaths = append(upperPaths, "/")
break
}
upperPath = upperPath[0:substrIndex]
upperPaths = append(upperPaths, upperPath)
}
var upperAlbums []models.Album
if err := r.Database.Model(&user).Association("Albums").Find(&upperAlbums, "albums.path IN (?)", upperPaths); err != nil {
// if err := r.Database.Model(models.Album{}).Where("path IN (?)", upperPaths).Find(&upperAlbums).Error; err != nil {
return nil, err
}
if len(upperAlbums) > 0 {
return nil, errors.New(fmt.Sprintf("user already owns a path containing this path: %s", upperAlbums[0].Path))
}
newAlbum, err := scanner.NewRootAlbum(r.Database, rootPath, &user)
if err != nil {
return nil, err
}
return newAlbum, nil
}
func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error) {
var album models.Album
if err := r.Database.First(&album, albumID).Error; err != nil {
return nil, err
}
var deletedAlbumIDs []int = nil
err := r.Database.Transaction(func(tx *gorm.DB) error {
if err := tx.Raw("DELETE FROM user_albums WHERE user_id = ? AND album_id = ?", userID, albumID).Error; err != nil {
return err
}
children, err := album.GetChildren(tx)
if err != nil {
return err
}
childAlbumIDs := make([]int, len(children))
for i, child := range children {
childAlbumIDs[i] = child.ID
}
result := tx.Exec("DELETE FROM user_albums WHERE user_id = ? and album_id IN (?)", userID, childAlbumIDs)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("No relation deleted")
}
// Cleanup if no user owns the album anymore
var count int
if err := tx.Raw("SELECT COUNT(user_id) FROM user_albums WHERE album_id = ?", albumID).Scan(&count).Error; err != nil {
return err
}
if count == 0 {
deletedAlbumIDs = append(childAlbumIDs, albumID)
childAlbumIDs = nil
// Delete albums from database
if err := tx.Delete(&models.Album{}, "id IN (?)", deletedAlbumIDs).Error; err != nil {
deletedAlbumIDs = nil
return err
}
}
return nil
})
if err != nil {
return nil, err
}
if deletedAlbumIDs != nil {
// Delete albums from cache
for _, id := range deletedAlbumIDs {
cacheAlbumPath := path.Join(scanner.PhotoCache(), strconv.Itoa(id))
if err := os.RemoveAll(cacheAlbumPath); err != nil {
return nil, err
}
}
}
return &album, nil
}

View File

@ -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,18 +85,20 @@ 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
deleteUser(id: ID!): User @isAdmin
"Add a root path from where to look for media for the given user"
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
userRemoveRootAlbum(userId: ID!, albumId: ID!): Album @isAdmin
"""
Set how often, in seconds, the server should automatically scan for new media,
a value of 0 will disable periodic scans
@ -180,8 +175,11 @@ type User {
id: ID!
username: String!
#albums: [Album]
"Local filepath for the user's photos"
rootPath: String! @isAdmin
# rootPath: String! @isAdmin
"All albums owned by this user"
albums: [Album!]! @isAdmin
"Top level albums owned by this user"
rootAlbums: [Album!]! @isAdmin
admin: Boolean!
#shareTokens: [ShareToken]
}

View File

@ -19,7 +19,12 @@ func authenticateMedia(media *models.Media, db *gorm.DB, r *http.Request) (succe
return false, "internal server error", http.StatusInternalServerError, err
}
if album.OwnerID != user.ID {
ownsAlbum, err := user.OwnsAlbum(db, &album)
if err != nil {
return false, "internal server error", http.StatusInternalServerError, err
}
if !ownsAlbum {
return false, "invalid credentials", http.StatusForbidden, nil
}
} else {
@ -55,20 +60,21 @@ func authenticateMedia(media *models.Media, db *gorm.DB, r *http.Request) (succe
if shareToken.AlbumID != nil && media.AlbumID != *shareToken.AlbumID {
// Check child albums
result := db.Raw(`
var count int
err := db.Raw(`
WITH recursive child_albums AS (
SELECT * FROM album WHERE parent_album = ?
SELECT * FROM albums WHERE parent_album_id = ?
UNION ALL
SELECT child.* FROM album child JOIN child_albums parent ON parent.album_id = child.parent_album
SELECT child.* FROM albums child JOIN child_albums parent ON parent.id = child.parent_album_id
)
SELECT * FROM child_albums WHERE album_id = ?
`, *shareToken.AlbumID, media.AlbumID)
SELECT COUNT(id) FROM child_albums WHERE id = ?
`, *shareToken.AlbumID, media.AlbumID).Find(&count).Error
if err := result.Error; err != nil {
if err != nil {
return false, "internal server error", http.StatusInternalServerError, err
}
if result.RowsAffected == 0 {
if count == 0 {
return false, "unauthorized", http.StatusForbidden, nil
}
}

View File

@ -4,7 +4,6 @@ import (
"os"
"path"
"strconv"
"strings"
"github.com/photoview/photoview/api/graphql/models"
"github.com/pkg/errors"
@ -59,28 +58,32 @@ func deleteOldUserAlbums(db *gorm.DB, scannedAlbums []*models.Album, user *model
return nil
}
albumPaths := make([]interface{}, len(scannedAlbums))
scannedAlbumIDs := make([]interface{}, len(scannedAlbums))
for i, album := range scannedAlbums {
albumPaths[i] = album.Path
scannedAlbumIDs[i] = album.ID
}
// Delete old albums
album_args := make([]interface{}, 0)
album_args = append(album_args, user.ID)
album_args = append(album_args, albumPaths...)
var albums []models.Album
albums_questions := strings.Repeat("MD5(?),", len(albumPaths))[:len(albumPaths)*7-1]
if err := db.Where("owner_id = ? AND path_hash NOT IN ("+albums_questions+")", album_args...).Find(&albums).Error; err != nil {
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := db.
Where("id IN (?)", userAlbumIDs).
Where("id NOT IN (?)", scannedAlbumIDs)
if err := query.Find(&albums).Error; err != nil {
return []error{errors.Wrap(err, "get albums to be deleted from database")}
}
deleteErrors := make([]error, 0)
albumIDs := make([]int, 0)
for _, album := range albums {
albumIDs = append(albumIDs, album.ID)
deleteAlbumIDs := make([]int, len(albums))
for i, album := range albums {
deleteAlbumIDs[i] = album.ID
cachePath := path.Join(PhotoCache(), strconv.Itoa(int(album.ID)))
err := os.RemoveAll(cachePath)
if err != nil {
@ -88,7 +91,7 @@ func deleteOldUserAlbums(db *gorm.DB, scannedAlbums []*models.Album, user *model
}
}
if err := db.Where("id IN ?", albumIDs).Delete(models.Album{}).Error; err != nil {
if err := db.Where("id IN ?", deleteAlbumIDs).Delete(models.Album{}).Error; err != nil {
ScannerError("Could not delete old albums from database:\n%s\n", err)
deleteErrors = append(deleteErrors, errors.Wrap(err, "delete old albums from database"))
}

View File

@ -13,6 +13,40 @@ import (
"gorm.io/gorm"
)
func NewRootAlbum(db *gorm.DB, rootPath string, owner *models.User) (*models.Album, error) {
owners := []models.User{
*owner,
}
var matchedAlbums []models.Album
if err := db.Where("path_hash = MD5(?)", rootPath).Find(&matchedAlbums).Error; err != nil {
return nil, err
}
if len(matchedAlbums) > 0 {
album := matchedAlbums[0]
if err := db.Model(&owner).Association("Albums").Append(&album); err != nil {
return nil, errors.Wrap(err, "failed to add owner to already existing album")
}
return &album, nil
} else {
album := models.Album{
Title: path.Base(rootPath),
Path: rootPath,
Owners: owners,
}
if err := db.Create(&album).Error; err != nil {
return nil, err
}
return &album, nil
}
}
func scanAlbum(album *models.Album, cache *AlbumScannerCache, db *gorm.DB) {
album_notify_key := utils.GenerateToken()

View File

@ -17,71 +17,112 @@ 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)}
}
if err := user.FillAlbums(db); err != nil {
return nil, []error{err}
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
var userRootAlbums []*models.Album
if err := db.Where("id IN (?)", userAlbumIDs).Where("parent_album_id IS NULL").Find(&userRootAlbums).Error; err != nil {
return nil, []error{err}
}
scanErrors := make([]error, 0)
type scanInfo struct {
path string
parentID *int
path string
parent *models.Album
}
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,
parent: nil,
})
}
}
userAlbums := make([]*models.Album, 0)
albumErrors := make([]error, 0)
// newPhotos := make([]*models.Photo, 0)
for scanQueue.Front() != nil {
albumInfo := scanQueue.Front().Value.(scanInfo)
scanQueue.Remove(scanQueue.Front())
albumPath := albumInfo.path
albumParentID := albumInfo.parentID
albumParent := albumInfo.parent
// 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
}
// Will become new album or album from db
var album models.Album
var album *models.Album
transErr := db.Transaction(func(tx *gorm.DB) error {
log.Printf("Scanning directory: %s", albumPath)
// Make album if not exists
albumTitle := path.Base(albumPath)
err = tx.FirstOrCreate(&album, models.Album{
Title: albumTitle,
ParentAlbumID: albumParentID,
OwnerID: user.ID,
Path: albumPath,
}).Error
if err != nil {
return errors.Wrap(err, "insert album into database")
// check if album already exists
var albumResult []models.Album
result := tx.Where("path_hash = md5(?)", albumPath).Find(&albumResult)
if result.Error != nil {
return result.Error
}
userAlbums = append(userAlbums, &album)
// album does not exist, create new
if len(albumResult) == 0 {
albumTitle := path.Base(albumPath)
var albumParentID *int
parentOwners := make([]models.User, 0)
if albumParent != nil {
albumParentID = &albumParent.ID
if err := db.Model(&albumParent).Association("Owners").Find(&parentOwners); err != nil {
return err
}
}
album = &models.Album{
Title: albumTitle,
ParentAlbumID: albumParentID,
Path: albumPath,
}
if err := tx.Create(&album).Error; err != nil {
return errors.Wrap(err, "insert album into database")
}
if err := tx.Model(&album).Association("Owners").Append(parentOwners); err != nil {
return errors.Wrap(err, "add owners to album")
}
} else {
album = &albumResult[0]
}
userAlbums = append(userAlbums, album)
return nil
})
if transErr != nil {
albumErrors = append(albumErrors, errors.Wrap(transErr, "begin database transaction"))
scanErrors = append(scanErrors, errors.Wrap(transErr, "begin database transaction"))
continue
}
@ -96,17 +137,17 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner
if item.IsDir() && directoryContainsPhotos(subalbumPath, album_cache) {
scanQueue.PushBack(scanInfo{
path: subalbumPath,
parentID: &album.ID,
path: subalbumPath,
parent: album,
})
}
}
}
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 {

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
import Layout from '../../Layout'
import ScannerSection from './ScannerSection'
import UsersTable from './UsersTable'
import UsersTable from './Users/UsersTable'
export const SectionTitle = styled.h2`
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;

View File

@ -1,298 +0,0 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import {
Button,
Checkbox,
Form,
Icon,
Input,
Modal,
Table,
} from 'semantic-ui-react'
const updateUserMutation = gql`
mutation updateUser(
$id: ID!
$username: String
$rootPath: String
$admin: Boolean
) {
updateUser(
id: $id
username: $username
rootPath: $rootPath
admin: $admin
) {
id
username
rootPath
admin
}
}
`
const deleteUserMutation = gql`
mutation deleteUser($id: ID!) {
deleteUser(id: $id) {
id
username
}
}
`
const changeUserPasswordMutation = gql`
mutation changeUserPassword($userId: ID!, $password: String!) {
updateUser(id: $userId, password: $password) {
id
}
}
`
const scanUserMutation = gql`
mutation scanUser($userId: ID!) {
scanUser(userId: $userId) {
success
}
}
`
const ChangePasswordModal = ({ onClose, user, ...props }) => {
const [passwordInput, setPasswordInput] = useState('')
const [changePassword] = useMutation(changeUserPasswordMutation, {
onCompleted: () => {
onClose && onClose()
},
})
return (
<Modal {...props}>
<Modal.Header>Change password</Modal.Header>
<Modal.Content>
<p>
Change password for <b>{user.username}</b>
</p>
<Form>
<Form.Field>
<label>New password</label>
<Input
placeholder="password"
onChange={e => setPasswordInput(e.target.value)}
type="password"
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => onClose && onClose()}>Cancel</Button>
<Button
positive
onClick={() => {
changePassword({
variables: {
userId: user.id,
password: passwordInput,
},
})
}}
>
Change password
</Button>
</Modal.Actions>
</Modal>
)
}
ChangePasswordModal.propTypes = {
onClose: PropTypes.func,
user: PropTypes.object.isRequired,
}
const UserRow = ({ user, refetchUsers }) => {
const [state, setState] = useState({
...user,
editing: false,
})
const [showConfirmDelete, setConfirmDelete] = useState(false)
const [showChangePassword, setChangePassword] = useState(false)
function updateInput(event, key) {
setState({
...state,
[key]: event.target.value,
})
}
const [updateUser, { loading: updateUserLoading }] = useMutation(
updateUserMutation,
{
onCompleted: data => {
setState({
...data.updateUser,
editing: false,
})
refetchUsers()
},
}
)
const [deleteUser] = useMutation(deleteUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
if (state.editing) {
return (
<Table.Row>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.username}
value={state.username}
onChange={e => updateInput(e, 'username')}
/>
</Table.Cell>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.rootPath}
value={state.rootPath}
onChange={e => updateInput(e, 'rootPath')}
/>
</Table.Cell>
<Table.Cell>
<Checkbox
toggle
checked={state.admin}
onChange={(_, data) => {
setState({
...state,
admin: data.checked,
})
}}
/>
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
negative
onClick={() =>
setState({
...state.oldState,
})
}
>
Cancel
</Button>
<Button
loading={updateUserLoading}
disabled={updateUserLoading}
positive
onClick={() =>
updateUser({
variables: {
id: user.id,
username: state.username,
rootPath: state.rootPath,
admin: state.admin,
},
})
}
>
Save
</Button>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
return (
<Table.Row>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{user.rootPath}</Table.Cell>
<Table.Cell>
{user.admin ? <Icon name="checkmark" size="large" /> : null}
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
onClick={() => {
setState({ ...state, editing: true, oldState: state })
}}
>
<Icon name="edit" />
Edit
</Button>
<Button
disabled={scanUserCalled}
onClick={() => scanUser({ variables: { userId: user.id } })}
>
<Icon name="sync" />
Scan
</Button>
<Button onClick={() => setChangePassword(true)}>
<Icon name="key" />
Change password
</Button>
<ChangePasswordModal
user={user}
open={showChangePassword}
onClose={() => setChangePassword(false)}
/>
<Button
negative
onClick={() => {
setConfirmDelete(true)
}}
>
<Icon name="delete" />
Delete
</Button>
<Modal open={showConfirmDelete}>
<Modal.Header>Delete user</Modal.Header>
<Modal.Content>
<p>
{`Are you sure, you want to delete `}
<b>{user.username}</b>?
</p>
<p>{`This action cannot be undone`}</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setConfirmDelete(false)}>Cancel</Button>
<Button
negative
onClick={() => {
setConfirmDelete(false)
deleteUser({
variables: {
id: user.id,
},
})
}}
>
Delete {user.username}
</Button>
</Modal.Actions>
</Modal>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
UserRow.propTypes = {
user: PropTypes.object.isRequired,
refetchUsers: PropTypes.func.isRequired,
}
export default UserRow

View File

@ -0,0 +1,81 @@
import React from 'react'
import { Button, Checkbox, Input, Table } from 'semantic-ui-react'
import { EditRootPaths } from './EditUserRowRootPaths'
import { UserRowProps } from './UserRow'
const EditUserRow = ({
user,
state,
setState,
updateUser,
updateUserLoading,
}) => {
function updateInput(event, key) {
setState(state => ({
...state,
[key]: event.target.value,
}))
}
return (
<Table.Row>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.username}
value={state.username}
onChange={e => updateInput(e, 'username')}
/>
</Table.Cell>
<Table.Cell>
<EditRootPaths user={user} />
</Table.Cell>
<Table.Cell>
<Checkbox
toggle
checked={state.admin}
onChange={(_, data) => {
setState(state => ({
...state,
admin: data.checked,
}))
}}
/>
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
negative
onClick={() =>
setState(state => ({
...state.oldState,
}))
}
>
Cancel
</Button>
<Button
loading={updateUserLoading}
disabled={updateUserLoading}
positive
onClick={() =>
updateUser({
variables: {
id: user.id,
username: state.username,
admin: state.admin,
},
})
}
>
Save
</Button>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
EditUserRow.propTypes = UserRowProps
export default EditUserRow

View File

@ -0,0 +1,135 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import { Button, Icon, Input } from 'semantic-ui-react'
import styled from 'styled-components'
import { USERS_QUERY } from './UsersTable'
const userAddRootPathMutation = gql`
mutation userAddRootPath($id: ID!, $rootPath: String!) {
userAddRootPath(id: $id, rootPath: $rootPath) {
id
}
}
`
const userRemoveAlbumPathMutation = gql`
mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) {
userRemoveRootAlbum(userId: $userId, albumId: $albumId) {
id
}
}
`
const RootPathListItem = styled.li`
display: flex;
justify-content: space-between;
align-items: center;
`
const EditRootPath = ({ album, user }) => {
const [removeAlbumPath, { loading }] = useMutation(
userRemoveAlbumPathMutation,
{
refetchQueries: [
{
query: USERS_QUERY,
},
],
}
)
return (
<RootPathListItem>
<span>{album.filePath}</span>
<Button
negative
disabled={loading}
onClick={() =>
removeAlbumPath({
variables: {
userId: user.id,
albumId: album.id,
},
})
}
>
<Icon name="remove" />
Remove
</Button>
</RootPathListItem>
)
}
EditRootPath.propTypes = {
album: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
}
const NewRootPathInput = styled(Input)`
width: 100%;
margin-top: 24px;
`
const EditNewRootPath = ({ userID }) => {
const [value, setValue] = useState('')
const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, {
refetchQueries: [
{
query: USERS_QUERY,
},
],
})
return (
<li>
<NewRootPathInput
style={{ width: '100%' }}
value={value}
onChange={e => setValue(e.target.value)}
disabled={loading}
action={{
positive: true,
icon: 'add',
content: 'Add',
onClick: () => {
setValue('')
addRootPath({
variables: {
id: userID,
rootPath: value,
},
})
},
}}
/>
</li>
)
}
EditNewRootPath.propTypes = {
userID: PropTypes.string.isRequired,
}
const RootPathList = styled.ul`
margin: 0;
padding: 0;
list-style: none;
`
export const EditRootPaths = ({ user }) => {
const editRows = user.rootAlbums.map(album => (
<EditRootPath key={album.id} album={album} user={user} />
))
return (
<RootPathList>
{editRows}
<EditNewRootPath userID={user.id} />
</RootPathList>
)
}
EditRootPaths.propTypes = {
user: PropTypes.object.isRequired,
}

View File

@ -0,0 +1,66 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { gql, useMutation } from '@apollo/client'
import { Button, Form, Input, Modal } from 'semantic-ui-react'
const changeUserPasswordMutation = gql`
mutation changeUserPassword($userId: ID!, $password: String!) {
updateUser(id: $userId, password: $password) {
id
}
}
`
const ChangePasswordModal = ({ onClose, user, ...props }) => {
const [passwordInput, setPasswordInput] = useState('')
const [changePassword] = useMutation(changeUserPasswordMutation, {
onCompleted: () => {
onClose && onClose()
},
})
return (
<Modal {...props}>
<Modal.Header>Change password</Modal.Header>
<Modal.Content>
<p>
Change password for <b>{user.username}</b>
</p>
<Form>
<Form.Field>
<label>New password</label>
<Input
placeholder="password"
onChange={e => setPasswordInput(e.target.value)}
type="password"
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => onClose && onClose()}>Cancel</Button>
<Button
positive
onClick={() => {
changePassword({
variables: {
userId: user.id,
password: passwordInput,
},
})
}}
>
Change password
</Button>
</Modal.Actions>
</Modal>
)
}
ChangePasswordModal.propTypes = {
onClose: PropTypes.func,
user: PropTypes.object.isRequired,
}
export default ChangePasswordModal

View File

@ -0,0 +1,111 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import EditUserRow from './EditUserRow'
import ViewUserRow from './ViewUserRow'
const updateUserMutation = gql`
mutation updateUser($id: ID!, $username: String, $admin: Boolean) {
updateUser(id: $id, username: $username, admin: $admin) {
id
username
admin
}
}
`
const deleteUserMutation = gql`
mutation deleteUser($id: ID!) {
deleteUser(id: $id) {
id
username
}
}
`
const scanUserMutation = gql`
mutation scanUser($userId: ID!) {
scanUser(userId: $userId) {
success
}
}
`
const UserRow = ({ user, refetchUsers }) => {
const [state, setState] = useState({
...user,
editing: false,
newRootPath: '',
})
const [showConfirmDelete, setConfirmDelete] = useState(false)
const [showChangePassword, setChangePassword] = useState(false)
const [updateUser, { loading: updateUserLoading }] = useMutation(
updateUserMutation,
{
onCompleted: data => {
setState({
...data.updateUser,
editing: false,
})
refetchUsers()
},
}
)
const [deleteUser] = useMutation(deleteUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
const props = {
user,
state,
setState,
scanUser,
updateUser,
updateUserLoading,
deleteUser,
setChangePassword,
setConfirmDelete,
scanUserCalled,
showChangePassword,
showConfirmDelete,
}
if (state.editing) {
return <EditUserRow {...props} />
}
return <ViewUserRow {...props} />
}
UserRow.propTypes = {
user: PropTypes.object.isRequired,
refetchUsers: PropTypes.func.isRequired,
}
export const UserRowProps = {
user: PropTypes.object.isRequired,
state: PropTypes.object.isRequired,
setState: PropTypes.func.isRequired,
scanUser: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired,
updateUserLoading: PropTypes.bool.isRequired,
deleteUser: PropTypes.func.isRequired,
setChangePassword: PropTypes.func.isRequired,
setConfirmDelete: PropTypes.func.isRequired,
scanUserCalled: PropTypes.func.isRequired,
showChangePassword: PropTypes.func.isRequired,
showConfirmDelete: PropTypes.func.isRequired,
}
export default UserRow

View File

@ -4,15 +4,19 @@ import { Table, Loader, Button, Icon } from 'semantic-ui-react'
import { useQuery, gql } from '@apollo/client'
import UserRow from './UserRow'
import AddUserRow from './AddUserRow'
import { SectionTitle } from './SettingsPage'
import { SectionTitle } from '../SettingsPage'
const USERS_QUERY = gql`
export const USERS_QUERY = gql`
query settingsUsersQuery {
user {
id
username
rootPath
# rootPath
admin
rootAlbums {
id
filePath
}
}
}
`

View File

@ -0,0 +1,109 @@
import React from 'react'
import { Button, Icon, Table, Modal } from 'semantic-ui-react'
import styled from 'styled-components'
import ChangePasswordModal from './UserChangePassword'
import { UserRowProps } from './UserRow'
const PathList = styled.ul`
margin: 0;
padding: 0 0 0 12px;
list-style: none;
`
const ViewUserRow = ({
user,
// state,
setState,
scanUser,
deleteUser,
setChangePassword,
setConfirmDelete,
scanUserCalled,
showChangePassword,
showConfirmDelete,
}) => {
const paths = (
<PathList>
{user.rootAlbums.map(album => (
<li key={album.id}>{album.filePath}</li>
))}
</PathList>
)
return (
<Table.Row>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{paths}</Table.Cell>
<Table.Cell>
{user.admin ? <Icon name="checkmark" size="large" /> : null}
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
onClick={() => {
setState(state => ({ ...state, editing: true, oldState: state }))
}}
>
<Icon name="edit" />
Edit
</Button>
<Button
disabled={scanUserCalled}
onClick={() => scanUser({ variables: { userId: user.id } })}
>
<Icon name="sync" />
Scan
</Button>
<Button onClick={() => setChangePassword(true)}>
<Icon name="key" />
Change password
</Button>
<ChangePasswordModal
user={user}
open={showChangePassword}
onClose={() => setChangePassword(false)}
/>
<Button
negative
onClick={() => {
setConfirmDelete(true)
}}
>
<Icon name="delete" />
Delete
</Button>
<Modal open={showConfirmDelete}>
<Modal.Header>Delete user</Modal.Header>
<Modal.Content>
<p>
{`Are you sure, you want to delete `}
<b>{user.username}</b>?
</p>
<p>{`This action cannot be undone`}</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setConfirmDelete(false)}>Cancel</Button>
<Button
negative
onClick={() => {
setConfirmDelete(false)
deleteUser({
variables: {
id: user.id,
},
})
}}
>
Delete {user.username}
</Button>
</Modal.Actions>
</Modal>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
ViewUserRow.propTypes = UserRowProps
export default ViewUserRow

View File

@ -61,6 +61,8 @@ export const SHARE_TOKEN_QUERY = gql`
}
highRes {
url
width
height
}
videoWeb {
url

View File

@ -32,7 +32,7 @@ const AlbumSidebar = ({ albumId }) => {
}
AlbumSidebar.propTypes = {
albumId: PropTypes.number.isRequired,
albumId: PropTypes.string.isRequired,
}
export default AlbumSidebar