mirror of https://github.com/stashapp/stash.git
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:
parent
7a2e59fcef
commit
ad17e7defe
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
Loading…
Reference in New Issue