Fix handling of files to delete during delete Gallery operation (#5213)

* Only remove file in zip from image if deleting from zip file
* Only remove file in folder from image if deleting from folder
This commit is contained in:
WithoutPants 2024-09-05 11:27:31 +10:00 committed by GitHub
parent 7a2e59fcef
commit ad17e7defe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 136 additions and 99 deletions

View File

@ -22,8 +22,8 @@ func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *i
imgsDestroyed = zipImgsDestroyed
// only delete folder based gallery images if we're deleting the folder
if deleteFile {
folderImgsDestroyed, err := s.destroyFolderImages(ctx, i, fileDeleter, deleteGenerated, deleteFile)
if deleteFile && i.FolderID != nil {
folderImgsDestroyed, err := s.ImageService.DestroyFolderImages(ctx, *i.FolderID, fileDeleter, deleteGenerated, deleteFile)
if err != nil {
return nil, err
}
@ -86,36 +86,3 @@ func (s *Service) destroyZipFileImages(ctx context.Context, i *models.Gallery, f
return imgsDestroyed, nil
}
func (s *Service) destroyFolderImages(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) {
if i.FolderID == nil {
return nil, nil
}
var imgsDestroyed []*models.Image
// find images in this folder
imgs, err := s.ImageFinder.FindByFolderID(ctx, *i.FolderID)
if err != nil {
return nil, err
}
for _, img := range imgs {
if err := img.LoadGalleryIDs(ctx, s.ImageFinder); err != nil {
return nil, err
}
// only destroy images that are not attached to other galleries
if len(img.GalleryIDs.List()) > 1 {
continue
}
if err := s.ImageService.Destroy(ctx, img, fileDeleter, deleteGenerated, deleteFile); err != nil {
return nil, err
}
imgsDestroyed = append(imgsDestroyed, img)
}
return imgsDestroyed, nil
}

View File

@ -18,6 +18,7 @@ type ImageFinder interface {
type ImageService interface {
Destroy(ctx context.Context, i *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error
DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error)
DestroyFolderImages(ctx context.Context, folderID models.FolderID, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error)
}
type Service struct {

View File

@ -2,6 +2,7 @@ package image
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/fsutil"
@ -43,8 +44,9 @@ func (s *Service) Destroy(ctx context.Context, i *models.Image, fileDeleter *Fil
// Returns a slice of images that were destroyed.
func (s *Service) DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *FileDeleter, deleteGenerated bool) ([]*models.Image, error) {
var imgsDestroyed []*models.Image
zipFileID := zipFile.Base().ID
imgs, err := s.Repository.FindByZipFileID(ctx, zipFile.Base().ID)
imgs, err := s.Repository.FindByZipFileID(ctx, zipFileID)
if err != nil {
return nil, err
}
@ -54,6 +56,23 @@ func (s *Service) DestroyZipImages(ctx context.Context, zipFile models.File, fil
return nil, err
}
// #5048 - if the image has multiple files, we just want to remove the file in the zip file,
// not delete the image entirely
if len(img.Files.List()) > 1 {
for _, f := range img.Files.List() {
if f.Base().ZipFileID == nil || *f.Base().ZipFileID != zipFileID {
continue
}
if err := s.Repository.RemoveFileID(ctx, img.ID, f.Base().ID); err != nil {
return nil, fmt.Errorf("failed to remove file from image: %w", err)
}
}
// don't delete the image
continue
}
const deleteFileInZip = false
if err := s.destroyImage(ctx, img, fileDeleter, deleteGenerated, deleteFileInZip); err != nil {
return nil, err
@ -65,6 +84,66 @@ func (s *Service) DestroyZipImages(ctx context.Context, zipFile models.File, fil
return imgsDestroyed, nil
}
// DestroyFolderImages destroys all images in a folder, optionally marking the files and generated files for deletion.
// It will not delete images that are attached to more than one gallery.
// Returns a slice of images that were destroyed.
func (s *Service) DestroyFolderImages(ctx context.Context, folderID models.FolderID, fileDeleter *FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) {
var imgsDestroyed []*models.Image
// find images in this folder
imgs, err := s.Repository.FindByFolderID(ctx, folderID)
if err != nil {
return nil, err
}
for _, img := range imgs {
if err := img.LoadFiles(ctx, s.Repository); err != nil {
return nil, err
}
// #5048 - if the image has multiple files, we just want to remove the file
// in the folder
if len(img.Files.List()) > 1 {
for _, f := range img.Files.List() {
if f.Base().ParentFolderID != folderID {
continue
}
if err := s.Repository.RemoveFileID(ctx, img.ID, f.Base().ID); err != nil {
return nil, fmt.Errorf("failed to remove file from image: %w", err)
}
// we still want to delete the file from the folder, if applicable
if deleteFile {
if err := file.Destroy(ctx, s.File, f, fileDeleter.Deleter, deleteFile); err != nil {
return nil, fmt.Errorf("failed to delete image file: %w", err)
}
}
}
// don't delete the image
continue
}
if err := img.LoadGalleryIDs(ctx, s.Repository); err != nil {
return nil, err
}
// only destroy images that are not attached to other galleries
if len(img.GalleryIDs.List()) > 1 {
continue
}
if err := s.Destroy(ctx, img, fileDeleter, deleteGenerated, deleteFile); err != nil {
return nil, err
}
imgsDestroyed = append(imgsDestroyed, img)
}
return imgsDestroyed, nil
}
// Destroy destroys an image, optionally marking the file and generated files for deletion.
func (s *Service) destroyImage(ctx context.Context, i *models.Image, fileDeleter *FileDeleter, deleteGenerated, deleteFile bool) error {
if deleteFile {

View File

@ -638,6 +638,20 @@ func (_m *ImageReaderWriter) QueryCount(ctx context.Context, imageFilter *models
return r0, r1
}
// RemoveFileID provides a mock function with given fields: ctx, id, fileID
func (_m *ImageReaderWriter) RemoveFileID(ctx context.Context, id int, fileID models.FileID) error {
ret := _m.Called(ctx, id, fileID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int, models.FileID) error); ok {
r0 = rf(ctx, id, fileID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ResetOCounter provides a mock function with given fields: ctx, id
func (_m *ImageReaderWriter) ResetOCounter(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)

View File

@ -190,27 +190,6 @@ func (_m *SceneReaderWriter) CountByFileID(ctx context.Context, fileID models.Fi
return r0, r1
}
// CountByGroupID provides a mock function with given fields: ctx, groupID
func (_m *SceneReaderWriter) CountByGroupID(ctx context.Context, groupID int) (int, error) {
ret := _m.Called(ctx, groupID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, groupID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, groupID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountByPerformerID provides a mock function with given fields: ctx, performerID
func (_m *SceneReaderWriter) CountByPerformerID(ctx context.Context, performerID int) (int, error) {
ret := _m.Called(ctx, performerID)
@ -232,48 +211,6 @@ func (_m *SceneReaderWriter) CountByPerformerID(ctx context.Context, performerID
return r0, r1
}
// CountByStudioID provides a mock function with given fields: ctx, studioID
func (_m *SceneReaderWriter) CountByStudioID(ctx context.Context, studioID int) (int, error) {
ret := _m.Called(ctx, studioID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, studioID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, studioID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountByTagID provides a mock function with given fields: ctx, tagID
func (_m *SceneReaderWriter) CountByTagID(ctx context.Context, tagID int) (int, error) {
ret := _m.Called(ctx, tagID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, tagID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, tagID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountMissingChecksum provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) CountMissingChecksum(ctx context.Context) (int, error) {
ret := _m.Called(ctx)

View File

@ -89,6 +89,7 @@ type ImageWriter interface {
ImageDestroyer
AddFileID(ctx context.Context, id int, fileID FileID) error
RemoveFileID(ctx context.Context, id int, fileID FileID) error
IncrementOCounter(ctx context.Context, id int) (int, error)
DecrementOCounter(ctx context.Context, id int) (int, error)
ResetOCounter(ctx context.Context, id int) (int, error)

View File

@ -997,6 +997,21 @@ func (qb *ImageStore) AddFileID(ctx context.Context, id int, fileID models.FileI
return imagesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
}
// RemoveFileID removes the file ID from the image.
// If the file ID is the primary file, then the next file in the list is set as the primary file.
func (qb *ImageStore) RemoveFileID(ctx context.Context, id int, fileID models.FileID) error {
fileIDs, err := imagesFilesTableMgr.get(ctx, id)
if err != nil {
return fmt.Errorf("getting file IDs for image %d: %w", id, err)
}
fileIDs = sliceutil.Filter(fileIDs, func(f models.FileID) bool {
return f != fileID
})
return imagesFilesTableMgr.replaceJoins(ctx, id, fileIDs)
}
func (qb *ImageStore) GetGalleryIDs(ctx context.Context, imageID int) ([]int, error) {
return imageRepository.galleries.getIDs(ctx, imageID)
}

View File

@ -759,6 +759,29 @@ type relatedFilesTable struct {
// FileID models.FileID `db:"file_id"`
// }
// get returns the file IDs related to the provided scene ID
// the primary file is returned first
func (t *relatedFilesTable) get(ctx context.Context, id int) ([]models.FileID, error) {
q := dialect.Select("file_id").From(t.table.table).Where(t.idColumn.Eq(id)).Order(t.table.table.Col("primary").Desc())
const single = false
var ret []models.FileID
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
var v models.FileID
if err := rows.Scan(&v); err != nil {
return err
}
ret = append(ret, v)
return nil
}); err != nil {
return nil, fmt.Errorf("getting related files from %s: %w", t.table.table.GetTable(), err)
}
return ret, nil
}
func (t *relatedFilesTable) insertJoin(ctx context.Context, id int, primary bool, fileID models.FileID) error {
q := dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), "primary", "file_id").Vals(
goqu.Vals{id, primary, fileID},