mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
File storage rewrite (#2676)
* Restructure data layer part 2 (#2599) * Refactor and separate image model * Refactor image query builder * Handle relationships in image query builder * Remove relationship management methods * Refactor gallery model/query builder * Add scenes to gallery model * Convert scene model * Refactor scene models * Remove unused methods * Add unit tests for gallery * Add image tests * Add scene tests * Convert unnecessary scene value pointers to values * Convert unnecessary pointer values to values * Refactor scene partial * Add scene partial tests * Refactor ImagePartial * Add image partial tests * Refactor gallery partial update * Add partial gallery update tests * Use zero/null package for null values * Add files and scan system * Add sqlite implementation for files/folders * Add unit tests for files/folders * Image refactors * Update image data layer * Refactor gallery model and creation * Refactor scene model * Refactor scenes * Don't set title from filename * Allow galleries to freely add/remove images * Add multiple scene file support to graphql and UI * Add multiple file support for images in graphql/UI * Add multiple file for galleries in graphql/UI * Remove use of some deprecated fields * Remove scene path usage * Remove gallery path usage * Remove path from image * Move funscript to video file * Refactor caption detection * Migrate existing data * Add post commit/rollback hook system * Lint. Comment out import/export tests * Add WithDatabase read only wrapper * Prepend tasks to list * Add 32 pre-migration * Add warnings in release and migration notes
This commit is contained in:
@@ -3,61 +3,45 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
|
||||
type cleaner interface {
|
||||
Clean(ctx context.Context, options file.CleanOptions, progress *job.Progress)
|
||||
}
|
||||
|
||||
type cleanJob struct {
|
||||
txnManager models.Repository
|
||||
input CleanMetadataInput
|
||||
scanSubs *subscriptionManager
|
||||
cleaner cleaner
|
||||
txnManager Repository
|
||||
input CleanMetadataInput
|
||||
sceneService SceneService
|
||||
imageService ImageService
|
||||
scanSubs *subscriptionManager
|
||||
}
|
||||
|
||||
func (j *cleanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
logger.Infof("Starting cleaning of tracked files")
|
||||
start := time.Now()
|
||||
if j.input.DryRun {
|
||||
logger.Infof("Running in Dry Mode")
|
||||
}
|
||||
|
||||
r := j.txnManager
|
||||
|
||||
if err := j.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
||||
total, err := j.getCount(ctx, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting count: %w", err)
|
||||
}
|
||||
|
||||
progress.SetTotal(total)
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := j.processScenes(ctx, progress, r.Scene); err != nil {
|
||||
return fmt.Errorf("error cleaning scenes: %w", err)
|
||||
}
|
||||
if err := j.processImages(ctx, progress, r.Image); err != nil {
|
||||
return fmt.Errorf("error cleaning images: %w", err)
|
||||
}
|
||||
if err := j.processGalleries(ctx, progress, r.Gallery, r.Image); err != nil {
|
||||
return fmt.Errorf("error cleaning galleries: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
j.cleaner.Clean(ctx, file.CleanOptions{
|
||||
Paths: j.input.Paths,
|
||||
DryRun: j.input.DryRun,
|
||||
PathFilter: newCleanFilter(instance.Config),
|
||||
}, progress)
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
@@ -65,303 +49,91 @@ func (j *cleanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
}
|
||||
|
||||
j.scanSubs.notify()
|
||||
logger.Info("Finished Cleaning")
|
||||
elapsed := time.Since(start)
|
||||
logger.Info(fmt.Sprintf("Finished Cleaning (%s)", elapsed))
|
||||
}
|
||||
|
||||
func (j *cleanJob) getCount(ctx context.Context, r models.Repository) (int, error) {
|
||||
sceneFilter := scene.PathsFilter(j.input.Paths)
|
||||
sceneResult, err := r.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
Count: true,
|
||||
type cleanFilter struct {
|
||||
scanFilter
|
||||
}
|
||||
|
||||
func newCleanFilter(c *config.Instance) *cleanFilter {
|
||||
return &cleanFilter{
|
||||
scanFilter: scanFilter{
|
||||
stashPaths: c.GetStashPaths(),
|
||||
generatedPath: c.GetGeneratedPath(),
|
||||
vidExt: c.GetVideoExtensions(),
|
||||
imgExt: c.GetImageExtensions(),
|
||||
zipExt: c.GetGalleryExtensions(),
|
||||
videoExcludeRegex: generateRegexps(c.GetExcludes()),
|
||||
imageExcludeRegex: generateRegexps(c.GetImageExcludes()),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
imageCount, err := r.Image.QueryCount(ctx, image.PathsFilter(j.input.Paths), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
galleryCount, err := r.Gallery.QueryCount(ctx, gallery.PathsFilter(j.input.Paths), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return sceneResult.Count + imageCount + galleryCount, nil
|
||||
}
|
||||
|
||||
func (j *cleanJob) processScenes(ctx context.Context, progress *job.Progress, qb scene.Queryer) error {
|
||||
batchSize := 1000
|
||||
func (f *cleanFilter) Accept(ctx context.Context, path string, info fs.FileInfo) bool {
|
||||
// #1102 - clean anything in generated path
|
||||
generatedPath := f.generatedPath
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
sceneFilter := scene.PathsFilter(j.input.Paths)
|
||||
sort := "path"
|
||||
findFilter.Sort = &sort
|
||||
var stash *config.StashConfig
|
||||
fileOrFolder := "File"
|
||||
|
||||
var toDelete []int
|
||||
|
||||
more := true
|
||||
for more {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
scenes, err := scene.Query(ctx, qb, sceneFilter, findFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error querying for scenes: %w", err)
|
||||
}
|
||||
|
||||
for _, scene := range scenes {
|
||||
progress.ExecuteTask(fmt.Sprintf("Assessing scene %s for clean", scene.Path), func() {
|
||||
if j.shouldCleanScene(scene) {
|
||||
toDelete = append(toDelete, scene.ID)
|
||||
} else {
|
||||
// increment progress, no further processing
|
||||
progress.Increment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if len(scenes) != batchSize {
|
||||
more = false
|
||||
} else {
|
||||
*findFilter.Page++
|
||||
}
|
||||
if info.IsDir() {
|
||||
fileOrFolder = "Folder"
|
||||
stash = getStashFromDirPath(f.stashPaths, path)
|
||||
} else {
|
||||
stash = getStashFromPath(f.stashPaths, path)
|
||||
}
|
||||
|
||||
if j.input.DryRun && len(toDelete) > 0 {
|
||||
// add progress for scenes that would've been deleted
|
||||
progress.AddProcessed(len(toDelete))
|
||||
}
|
||||
|
||||
fileNamingAlgorithm := instance.Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
if !j.input.DryRun && len(toDelete) > 0 {
|
||||
progress.ExecuteTask(fmt.Sprintf("Cleaning %d scenes", len(toDelete)), func() {
|
||||
for _, sceneID := range toDelete {
|
||||
if job.IsCancelled(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
j.deleteScene(ctx, fileNamingAlgorithm, sceneID)
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *cleanJob) processGalleries(ctx context.Context, progress *job.Progress, qb gallery.Queryer, iqb models.ImageReader) error {
|
||||
batchSize := 1000
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
galleryFilter := gallery.PathsFilter(j.input.Paths)
|
||||
sort := "path"
|
||||
findFilter.Sort = &sort
|
||||
|
||||
var toDelete []int
|
||||
|
||||
more := true
|
||||
for more {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
galleries, _, err := qb.Query(ctx, galleryFilter, findFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error querying for galleries: %w", err)
|
||||
}
|
||||
|
||||
for _, gallery := range galleries {
|
||||
progress.ExecuteTask(fmt.Sprintf("Assessing gallery %s for clean", gallery.GetTitle()), func() {
|
||||
if j.shouldCleanGallery(ctx, gallery, iqb) {
|
||||
toDelete = append(toDelete, gallery.ID)
|
||||
} else {
|
||||
// increment progress, no further processing
|
||||
progress.Increment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if len(galleries) != batchSize {
|
||||
more = false
|
||||
} else {
|
||||
*findFilter.Page++
|
||||
}
|
||||
}
|
||||
|
||||
if j.input.DryRun && len(toDelete) > 0 {
|
||||
// add progress for galleries that would've been deleted
|
||||
progress.AddProcessed(len(toDelete))
|
||||
}
|
||||
|
||||
if !j.input.DryRun && len(toDelete) > 0 {
|
||||
progress.ExecuteTask(fmt.Sprintf("Cleaning %d galleries", len(toDelete)), func() {
|
||||
for _, galleryID := range toDelete {
|
||||
if job.IsCancelled(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
j.deleteGallery(ctx, galleryID)
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *cleanJob) processImages(ctx context.Context, progress *job.Progress, qb image.Queryer) error {
|
||||
batchSize := 1000
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
imageFilter := image.PathsFilter(j.input.Paths)
|
||||
|
||||
// performance consideration: order by path since default ordering by
|
||||
// title is slow
|
||||
sortBy := "path"
|
||||
findFilter.Sort = &sortBy
|
||||
|
||||
var toDelete []int
|
||||
|
||||
more := true
|
||||
for more {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
images, err := image.Query(ctx, qb, imageFilter, findFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error querying for images: %w", err)
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
progress.ExecuteTask(fmt.Sprintf("Assessing image %s for clean", image.Path), func() {
|
||||
if j.shouldCleanImage(image) {
|
||||
toDelete = append(toDelete, image.ID)
|
||||
} else {
|
||||
// increment progress, no further processing
|
||||
progress.Increment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if len(images) != batchSize {
|
||||
more = false
|
||||
} else {
|
||||
*findFilter.Page++
|
||||
}
|
||||
}
|
||||
|
||||
if j.input.DryRun && len(toDelete) > 0 {
|
||||
// add progress for images that would've been deleted
|
||||
progress.AddProcessed(len(toDelete))
|
||||
}
|
||||
|
||||
if !j.input.DryRun && len(toDelete) > 0 {
|
||||
progress.ExecuteTask(fmt.Sprintf("Cleaning %d images", len(toDelete)), func() {
|
||||
for _, imageID := range toDelete {
|
||||
if job.IsCancelled(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
j.deleteImage(ctx, imageID)
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *cleanJob) shouldClean(path string) bool {
|
||||
// use image.FileExists for zip file checking
|
||||
fileExists := image.FileExists(path)
|
||||
|
||||
// #1102 - clean anything in generated path
|
||||
generatedPath := config.GetInstance().GetGeneratedPath()
|
||||
if !fileExists || getStashFromPath(path) == nil || fsutil.IsPathInDir(generatedPath, path) {
|
||||
logger.Infof("File not found. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *cleanJob) shouldCleanScene(s *models.Scene) bool {
|
||||
if j.shouldClean(s.Path) {
|
||||
return true
|
||||
}
|
||||
|
||||
stash := getStashFromPath(s.Path)
|
||||
if stash.ExcludeVideo {
|
||||
logger.Infof("File in stash library that excludes video. Marking to clean: \"%s\"", s.Path)
|
||||
return true
|
||||
}
|
||||
|
||||
config := config.GetInstance()
|
||||
if !fsutil.MatchExtension(s.Path, config.GetVideoExtensions()) {
|
||||
logger.Infof("File extension does not match video extensions. Marking to clean: \"%s\"", s.Path)
|
||||
return true
|
||||
}
|
||||
|
||||
if matchFile(s.Path, config.GetExcludes()) {
|
||||
logger.Infof("File matched regex. Marking to clean: \"%s\"", s.Path)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *cleanJob) shouldCleanGallery(ctx context.Context, g *models.Gallery, qb models.ImageReader) bool {
|
||||
// never clean manually created galleries
|
||||
if !g.Path.Valid {
|
||||
if stash == nil {
|
||||
logger.Infof("%s not in any stash library directories. Marking to clean: \"%s\"", fileOrFolder, path)
|
||||
return false
|
||||
}
|
||||
|
||||
path := g.Path.String
|
||||
if j.shouldClean(path) {
|
||||
if fsutil.IsPathInDir(generatedPath, path) {
|
||||
logger.Infof("%s is in generated path. Marking to clean: \"%s\"", fileOrFolder, path)
|
||||
return false
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return !f.shouldCleanFolder(path, stash)
|
||||
}
|
||||
|
||||
return !f.shouldCleanFile(path, info, stash)
|
||||
}
|
||||
|
||||
func (f *cleanFilter) shouldCleanFolder(path string, s *config.StashConfig) bool {
|
||||
// only delete folders where it is excluded from everything
|
||||
pathExcludeTest := path + string(filepath.Separator)
|
||||
if (s.ExcludeVideo || matchFileRegex(pathExcludeTest, f.videoExcludeRegex)) && (s.ExcludeImage || matchFileRegex(pathExcludeTest, f.imageExcludeRegex)) {
|
||||
logger.Infof("Folder is excluded from both video and image. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
stash := getStashFromPath(path)
|
||||
if stash.ExcludeImage {
|
||||
logger.Infof("File in stash library that excludes images. Marking to clean: \"%s\"", path)
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *cleanFilter) shouldCleanFile(path string, info fs.FileInfo, stash *config.StashConfig) bool {
|
||||
switch {
|
||||
case info.IsDir() || fsutil.MatchExtension(path, f.zipExt):
|
||||
return f.shouldCleanGallery(path, stash)
|
||||
case fsutil.MatchExtension(path, f.vidExt):
|
||||
return f.shouldCleanVideoFile(path, stash)
|
||||
case fsutil.MatchExtension(path, f.imgExt):
|
||||
return f.shouldCleanImage(path, stash)
|
||||
default:
|
||||
logger.Infof("File extension does not match any media extensions. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (f *cleanFilter) shouldCleanVideoFile(path string, stash *config.StashConfig) bool {
|
||||
if stash.ExcludeVideo {
|
||||
logger.Infof("File in stash library that excludes video. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
config := config.GetInstance()
|
||||
if g.Zip {
|
||||
if !fsutil.MatchExtension(path, config.GetGalleryExtensions()) {
|
||||
logger.Infof("File extension does not match gallery extensions. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
if countImagesInZip(path) == 0 {
|
||||
logger.Infof("Gallery has 0 images. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// folder-based - delete if it has no images
|
||||
count, err := qb.CountByGalleryID(ctx, g.ID)
|
||||
if err != nil {
|
||||
logger.Warnf("Error trying to count gallery images for %q: %v", path, err)
|
||||
return false
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if matchFile(path, config.GetImageExcludes()) {
|
||||
if matchFileRegex(path, f.videoExcludeRegex) {
|
||||
logger.Infof("File matched regex. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
@@ -369,141 +141,186 @@ func (j *cleanJob) shouldCleanGallery(ctx context.Context, g *models.Gallery, qb
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *cleanJob) shouldCleanImage(s *models.Image) bool {
|
||||
if j.shouldClean(s.Path) {
|
||||
return true
|
||||
}
|
||||
|
||||
stash := getStashFromPath(s.Path)
|
||||
func (f *cleanFilter) shouldCleanGallery(path string, stash *config.StashConfig) bool {
|
||||
if stash.ExcludeImage {
|
||||
logger.Infof("File in stash library that excludes images. Marking to clean: \"%s\"", s.Path)
|
||||
logger.Infof("File in stash library that excludes images. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
config := config.GetInstance()
|
||||
if !fsutil.MatchExtension(s.Path, config.GetImageExtensions()) {
|
||||
logger.Infof("File extension does not match image extensions. Marking to clean: \"%s\"", s.Path)
|
||||
return true
|
||||
}
|
||||
|
||||
if matchFile(s.Path, config.GetImageExcludes()) {
|
||||
logger.Infof("File matched regex. Marking to clean: \"%s\"", s.Path)
|
||||
if matchFileRegex(path, f.imageExcludeRegex) {
|
||||
logger.Infof("File matched regex. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *cleanJob) deleteScene(ctx context.Context, fileNamingAlgorithm models.HashAlgorithm, sceneID int) {
|
||||
fileNamingAlgo := GetInstance().Config.GetVideoFileNamingAlgorithm()
|
||||
func (f *cleanFilter) shouldCleanImage(path string, stash *config.StashConfig) bool {
|
||||
if stash.ExcludeImage {
|
||||
logger.Infof("File in stash library that excludes images. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
if matchFileRegex(path, f.imageExcludeRegex) {
|
||||
logger.Infof("File matched regex. Marking to clean: \"%s\"", path)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type cleanHandler struct {
|
||||
PluginCache *plugin.Cache
|
||||
}
|
||||
|
||||
func (h *cleanHandler) HandleFile(ctx context.Context, fileDeleter *file.Deleter, fileID file.ID) error {
|
||||
if err := h.deleteRelatedScenes(ctx, fileDeleter, fileID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.deleteRelatedGalleries(ctx, fileID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.deleteRelatedImages(ctx, fileDeleter, fileID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *cleanHandler) HandleFolder(ctx context.Context, fileDeleter *file.Deleter, folderID file.FolderID) error {
|
||||
return h.deleteRelatedFolderGalleries(ctx, folderID)
|
||||
}
|
||||
|
||||
func (h *cleanHandler) deleteRelatedScenes(ctx context.Context, fileDeleter *file.Deleter, fileID file.ID) error {
|
||||
mgr := GetInstance()
|
||||
sceneQB := mgr.Database.Scene
|
||||
scenes, err := sceneQB.FindByFileID(ctx, fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileNamingAlgo := mgr.Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
sceneFileDeleter := &scene.FileDeleter{
|
||||
Deleter: fileDeleter,
|
||||
FileNamingAlgo: fileNamingAlgo,
|
||||
Paths: GetInstance().Paths,
|
||||
Paths: mgr.Paths,
|
||||
}
|
||||
var s *models.Scene
|
||||
if err := j.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
||||
repo := j.txnManager
|
||||
qb := repo.Scene
|
||||
|
||||
var err error
|
||||
s, err = qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
for _, scene := range scenes {
|
||||
// only delete if the scene has no other files
|
||||
if len(scene.Files) <= 1 {
|
||||
logger.Infof("Deleting scene %q since it has no other related files", scene.GetTitle())
|
||||
if err := mgr.SceneService.Destroy(ctx, scene, sceneFileDeleter, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checksum := scene.Checksum()
|
||||
oshash := scene.OSHash()
|
||||
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, mgr.Database, scene.ID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
Checksum: checksum,
|
||||
OSHash: oshash,
|
||||
Path: scene.Path(),
|
||||
}, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *cleanHandler) deleteRelatedGalleries(ctx context.Context, fileID file.ID) error {
|
||||
mgr := GetInstance()
|
||||
qb := mgr.Database.Gallery
|
||||
galleries, err := qb.FindByFileID(ctx, fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, g := range galleries {
|
||||
// only delete if the gallery has no other files
|
||||
if len(g.Files) <= 1 {
|
||||
logger.Infof("Deleting gallery %q since it has no other related files", g.GetTitle())
|
||||
if err := qb.Destroy(ctx, g.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, mgr.Database, g.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
Checksum: g.Checksum(),
|
||||
Path: g.Path(),
|
||||
}, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *cleanHandler) deleteRelatedFolderGalleries(ctx context.Context, folderID file.FolderID) error {
|
||||
mgr := GetInstance()
|
||||
qb := mgr.Database.Gallery
|
||||
galleries, err := qb.FindByFolderID(ctx, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, g := range galleries {
|
||||
logger.Infof("Deleting folder-based gallery %q since the folder no longer exists", g.GetTitle())
|
||||
if err := qb.Destroy(ctx, g.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scene.Destroy(ctx, s, repo.Scene, repo.SceneMarker, fileDeleter, true, false)
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
|
||||
logger.Errorf("Error deleting scene from database: %s", err.Error())
|
||||
return
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, mgr.Database, g.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
Checksum: g.Checksum(),
|
||||
Path: g.Path(),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
|
||||
GetInstance().PluginCache.ExecutePostHooks(ctx, sceneID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
Checksum: s.Checksum.String,
|
||||
OSHash: s.OSHash.String,
|
||||
Path: s.Path,
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *cleanJob) deleteGallery(ctx context.Context, galleryID int) {
|
||||
var g *models.Gallery
|
||||
|
||||
if err := j.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
||||
qb := j.txnManager.Gallery
|
||||
|
||||
var err error
|
||||
g, err = qb.Find(ctx, galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return qb.Destroy(ctx, galleryID)
|
||||
}); err != nil {
|
||||
logger.Errorf("Error deleting gallery from database: %s", err.Error())
|
||||
return
|
||||
func (h *cleanHandler) deleteRelatedImages(ctx context.Context, fileDeleter *file.Deleter, fileID file.ID) error {
|
||||
mgr := GetInstance()
|
||||
imageQB := mgr.Database.Image
|
||||
images, err := imageQB.FindByFileID(ctx, fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
GetInstance().PluginCache.ExecutePostHooks(ctx, galleryID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
Checksum: g.Checksum,
|
||||
Path: g.Path.String,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (j *cleanJob) deleteImage(ctx context.Context, imageID int) {
|
||||
fileDeleter := &image.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
imageFileDeleter := &image.FileDeleter{
|
||||
Deleter: fileDeleter,
|
||||
Paths: GetInstance().Paths,
|
||||
}
|
||||
|
||||
var i *models.Image
|
||||
if err := j.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
||||
qb := j.txnManager.Image
|
||||
for _, i := range images {
|
||||
if len(i.Files) <= 1 {
|
||||
logger.Infof("Deleting image %q since it has no other related files", i.GetTitle())
|
||||
if err := mgr.ImageService.Destroy(ctx, i, imageFileDeleter, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
i, err = qb.Find(ctx, imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, mgr.Database, i.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
Checksum: i.Checksum(),
|
||||
Path: i.Path(),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
if i == nil {
|
||||
return fmt.Errorf("image not found: %d", imageID)
|
||||
}
|
||||
|
||||
return image.Destroy(ctx, i, qb, fileDeleter, true, false)
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
|
||||
logger.Errorf("Error deleting image from database: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
GetInstance().PluginCache.ExecutePostHooks(ctx, imageID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
Checksum: i.Checksum,
|
||||
Path: i.Path,
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStashFromPath(pathToCheck string) *config.StashConfig {
|
||||
for _, s := range config.GetInstance().GetStashPaths() {
|
||||
if fsutil.IsPathInDir(s.Path, filepath.Dir(pathToCheck)) {
|
||||
return s
|
||||
func getStashFromPath(stashes []*config.StashConfig, pathToCheck string) *config.StashConfig {
|
||||
for _, f := range stashes {
|
||||
if fsutil.IsPathInDir(f.Path, filepath.Dir(pathToCheck)) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStashFromDirPath(pathToCheck string) *config.StashConfig {
|
||||
for _, s := range config.GetInstance().GetStashPaths() {
|
||||
if fsutil.IsPathInDir(s.Path, pathToCheck) {
|
||||
return s
|
||||
func getStashFromDirPath(stashes []*config.StashConfig, pathToCheck string) *config.StashConfig {
|
||||
for _, f := range stashes {
|
||||
if fsutil.IsPathInDir(f.Path, pathToCheck) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user