From a1a14286d65eee729890392ac8e1efd5298b30e4 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
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 dc8c45f..4f29057 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 ecaf12e..36d813b 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 7f374c6..abb4ec1 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 03d3fd7..67ae200 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 7865f62..881eb3a 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 9fa9705..f5f0ff8 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 427defe..05728b9 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
     }
   }
