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/gqlgen.yml b/api/gqlgen.yml
index 0f0c5e83..89eff49b 100644
--- a/api/gqlgen.yml
+++ b/api/gqlgen.yml
@@ -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:
diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 071d41cc..9d7744ae 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -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.marshalNUser2ᚖgithubᚗ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))
diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go
index 62b6b8a6..4f290573 100644
--- a/api/graphql/models/album.go
+++ b/api/graphql/models/album.go
@@ -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
+}
diff --git a/api/graphql/models/base.go b/api/graphql/models/base.go
index b44841df..2c56b385 100644
--- a/api/graphql/models/base.go
+++ b/api/graphql/models/base.go
@@ -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"`
}
diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go
index 2fe9eb28..36d813bb 100644
--- a/api/graphql/models/media.go
+++ b/api/graphql/models/media.go
@@ -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"`
diff --git a/api/graphql/models/share_token.go b/api/graphql/models/share_token.go
index 7f374c67..abb4ec11 100644
--- a/api/graphql/models/share_token.go
+++ b/api/graphql/models/share_token.go
@@ -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 {
diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go
index 7b2f1432..2d4505d8 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 {
@@ -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
+}
diff --git a/api/graphql/resolvers/album.go b/api/graphql/resolvers/album.go
index b8313fb0..c19998e9 100644
--- a/api/graphql/resolvers/album.go
+++ b/api/graphql/resolvers/album.go
@@ -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
diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go
index d7d00024..21482d27 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) {
@@ -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
diff --git a/api/graphql/resolvers/scanner.go b/api/graphql/resolvers/scanner.go
index 734eb477..179f1be3 100644
--- a/api/graphql/resolvers/scanner.go
+++ b/api/graphql/resolvers/scanner.go
@@ -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")
}
diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go
index f9d59af3..16586532 100644
--- a/api/graphql/resolvers/share_token.go
+++ b/api/graphql/resolvers/share_token.go
@@ -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
diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go
index 50c0e986..67ae2008 100644
--- a/api/graphql/resolvers/user.go
+++ b/api/graphql/resolvers/user.go
@@ -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
+}
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 3c8e95cf..c606cd64 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,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]
}
diff --git a/api/routes/authenticate_media.go b/api/routes/authenticate_media.go
index 8e42f413..80d1ed3d 100644
--- a/api/routes/authenticate_media.go
+++ b/api/routes/authenticate_media.go
@@ -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
}
}
diff --git a/api/scanner/cleanup_media.go b/api/scanner/cleanup_media.go
index ddf06492..e89b2d13 100644
--- a/api/scanner/cleanup_media.go
+++ b/api/scanner/cleanup_media.go
@@ -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"))
}
diff --git a/api/scanner/scanner_album.go b/api/scanner/scanner_album.go
index 38757da4..1a328573 100644
--- a/api/scanner/scanner_album.go
+++ b/api/scanner/scanner_album.go
@@ -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()
diff --git a/api/scanner/scanner_user.go b/api/scanner/scanner_user.go
index 1b1a322e..eb4ba153 100644
--- a/api/scanner/scanner_user.go
+++ b/api/scanner/scanner_user.go
@@ -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 {
diff --git a/ui/src/Pages/SettingsPage/SettingsPage.js b/ui/src/Pages/SettingsPage/SettingsPage.js
index abd73e05..2c815f2e 100644
--- a/ui/src/Pages/SettingsPage/SettingsPage.js
+++ b/ui/src/Pages/SettingsPage/SettingsPage.js
@@ -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;
diff --git a/ui/src/Pages/SettingsPage/UserRow.js b/ui/src/Pages/SettingsPage/UserRow.js
deleted file mode 100644
index 0c807e1d..00000000
--- a/ui/src/Pages/SettingsPage/UserRow.js
+++ /dev/null
@@ -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 (
-
- Change password for {user.username}
-
- {`Are you sure, you want to delete `} - {user.username}? -
-{`This action cannot be undone`}
-+ Change password for {user.username} +
++ {`Are you sure, you want to delete `} + {user.username}? +
+{`This action cannot be undone`}
+