diff --git a/api/graphql/generated.go b/api/graphql/generated.go index b03b397f..65985d37 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -215,6 +215,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) @@ -2366,14 +2368,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) @@ -2385,9 +2387,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) { @@ -7534,10 +7536,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 { diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go index 62b6b8a6..0e6149ac 100644 --- a/api/graphql/models/album.go +++ b/api/graphql/models/album.go @@ -12,10 +12,11 @@ type Album struct { 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"` + // 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 { diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go index 94b1a1c4..2d4505d8 100644 --- a/api/graphql/models/user.go +++ b/api/graphql/models/user.go @@ -153,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..ea8b5a70 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 = (?)", r.Database.Model(&models.Album{}).Select("id").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 a6b82313..f1e3d667 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -18,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) 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/user.go b/api/graphql/resolvers/user.go index f6d594d5..50dc24bc 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -5,6 +5,7 @@ import ( "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" @@ -84,6 +85,11 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri return err } + _, err = scanner.NewRootAlbum(tx, rootPath, user) + if err != nil { + return err + } + token, err = user.GenerateAccessToken(tx) if err != nil { return err diff --git a/api/routes/authenticate_media.go b/api/routes/authenticate_media.go index 8e42f413..e29d8cc0 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 { 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..11decfe3 100644 --- a/api/scanner/scanner_album.go +++ b/api/scanner/scanner_album.go @@ -13,6 +13,25 @@ import ( "gorm.io/gorm" ) +func NewRootAlbum(db *gorm.DB, rootPath string, owner *models.User) (*models.Album, error) { + + owners := []models.User{ + *owner, + } + + 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 ca1b157d..eb4ba153 100644 --- a/api/scanner/scanner_user.go +++ b/api/scanner/scanner_user.go @@ -17,16 +17,25 @@ import ( func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScannerCache) ([]*models.Album, []error) { + 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.Model(&user).Association("Albums").Find(&userRootAlbums); err != nil { - return nil, []error{errors.Wrapf(err, "get albums of user (%s)", user.Username)} + 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() @@ -41,8 +50,8 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner } } else { scanQueue.PushBack(scanInfo{ - path: album.Path, - parentID: nil, + path: album.Path, + parent: nil, }) } } @@ -54,7 +63,7 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner scanQueue.Remove(scanQueue.Front()) albumPath := albumInfo.path - albumParentID := albumInfo.parentID + albumParent := albumInfo.parent // Read path dirContent, err := ioutil.ReadDir(albumPath) @@ -64,26 +73,50 @@ func findAlbumsForUser(db *gorm.DB, user *models.User, album_cache *AlbumScanner } // 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 }) @@ -104,8 +137,8 @@ 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, }) } } diff --git a/ui/src/Pages/SettingsPage/UsersTable.js b/ui/src/Pages/SettingsPage/UsersTable.js index ea88ecea..1dc187e4 100644 --- a/ui/src/Pages/SettingsPage/UsersTable.js +++ b/ui/src/Pages/SettingsPage/UsersTable.js @@ -11,7 +11,7 @@ const USERS_QUERY = gql` user { id username - rootPath + # rootPath admin } }