From a1a14286d65eee729890392ac8e1efd5298b30e4 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 6 Jan 2021 17:28:06 +0100 Subject: [PATCH] Properly clean up when a user <-> album relation is deleted --- api/graphql/models/album.go | 2 +- api/graphql/models/media.go | 28 +++++--- api/graphql/models/share_token.go | 6 +- api/graphql/resolvers/user.go | 71 ++++++++++++++----- .../Pages/SettingsPage/Users/EditUserRow.js | 1 - .../Users/EditUserRowRootPaths.js | 1 + ui/src/Pages/SettingsPage/Users/UserRow.js | 15 +--- 7 files changed, 82 insertions(+), 42 deletions(-) diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go index dc8c45f2..4f290573 100644 --- a/api/graphql/models/album.go +++ b/api/graphql/models/album.go @@ -11,7 +11,7 @@ type Album struct { Model Title string `gorm:"not null"` ParentAlbumID *int - ParentAlbum *Album + ParentAlbum *Album `gorm:"constraint:OnDelete:SET NULL;"` // OwnerID int `gorm:"not null"` // Owner User Owners []User `gorm:"many2many:user_albums"` diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index ecaf12e7..36d813bb 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -17,16 +17,16 @@ type Media struct { Path string `gorm:"not null"` PathHash string `gorm:"not null"` AlbumID int `gorm:"not null"` - Album Album + Album Album `gorm:"constraint:OnDelete:CASCADE;"` ExifID *int - Exif *MediaEXIF - MediaURL []MediaURL - DateShot time.Time `gorm:"not null"` - DateImported time.Time `gorm:"not null"` + 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/resolvers/user.go b/api/graphql/resolvers/user.go index 03d3fd79..67ae2008 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -3,7 +3,9 @@ package resolvers import ( "context" "fmt" + "os" "path" + "strconv" "strings" api "github.com/photoview/photoview/api/graphql" @@ -280,28 +282,65 @@ func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int, return nil, err } - if err := r.Database.Raw("DELETE FROM user_albums WHERE user_id = ? AND album_id = ?", userID, 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 + }) - children, err := album.GetChildren(r.Database) if err != nil { return nil, err } - childAlbumIDs := make([]int, len(children)) - for i, child := range children { - childAlbumIDs[i] = child.ID - } + if deletedAlbumIDs != nil { + // Delete albums from cache + for _, id := range deletedAlbumIDs { + cacheAlbumPath := path.Join(scanner.PhotoCache(), strconv.Itoa(id)) - // result := r.Database.Delete(models.Album{}, "id IN (?) OR id = ?", childAlbumIDs, album.ID) - result := r.Database.Exec("DELETE FROM user_albums WHERE user_id = ? and album_id IN (?)", userID, childAlbumIDs) - if result.Error != nil { - return nil, result.Error - } - - if result.RowsAffected == 0 { - return nil, errors.New("No relation deleted") + if err := os.RemoveAll(cacheAlbumPath); err != nil { + return nil, err + } + } } return &album, nil diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRow.js b/ui/src/Pages/SettingsPage/Users/EditUserRow.js index 7865f629..881eb3a4 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRow.js @@ -63,7 +63,6 @@ const EditUserRow = ({ variables: { id: user.id, username: state.username, - rootPath: state.rootPath, admin: state.admin, }, }) diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js index 9fa9705b..f5f0ff87 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js @@ -93,6 +93,7 @@ const EditNewRootPath = ({ userID }) => { icon: 'add', content: 'Add', onClick: () => { + setValue('') addRootPath({ variables: { id: userID, diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.js b/ui/src/Pages/SettingsPage/Users/UserRow.js index 427defe4..05728b9f 100644 --- a/ui/src/Pages/SettingsPage/Users/UserRow.js +++ b/ui/src/Pages/SettingsPage/Users/UserRow.js @@ -5,21 +5,10 @@ import EditUserRow from './EditUserRow' import ViewUserRow from './ViewUserRow' const updateUserMutation = gql` - mutation updateUser( - $id: ID! - $username: String - $rootPath: String - $admin: Boolean - ) { - updateUser( - id: $id - username: $username - rootPath: $rootPath - admin: $admin - ) { + mutation updateUser($id: ID!, $username: String, $admin: Boolean) { + updateUser(id: $id, username: $username, admin: $admin) { id username - rootPath admin } }