stash/pkg/image/scan.go

192 lines
5.0 KiB
Go

package image
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/paths"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/utils"
)
const mutexType = "image"
type Scanner struct {
file.Scanner
StripFileExtension bool
CaseSensitiveFs bool
TxnManager models.TransactionManager
Paths *paths.Paths
PluginCache *plugin.Cache
MutexManager *utils.MutexManager
}
func FileScanner(hasher file.Hasher) file.Scanner {
return file.Scanner{
Hasher: hasher,
CalculateMD5: true,
}
}
func (scanner *Scanner) ScanExisting(ctx context.Context, existing file.FileBased, file file.SourceFile) (retImage *models.Image, err error) {
scanned, err := scanner.Scanner.ScanExisting(existing, file)
if err != nil {
return nil, err
}
i := existing.(*models.Image)
path := scanned.New.Path
oldChecksum := i.Checksum
changed := false
if scanned.ContentsChanged() {
logger.Infof("%s has been updated: rescanning", path)
// regenerate the file details as well
if err := SetFileDetails(i); err != nil {
return nil, err
}
changed = true
} else if scanned.FileUpdated() {
logger.Infof("Updated image file %s", path)
changed = true
}
if changed {
i.SetFile(*scanned.New)
i.UpdatedAt = models.SQLiteTimestamp{Timestamp: time.Now()}
// we are operating on a checksum now, so grab a mutex on the checksum
done := make(chan struct{})
scanner.MutexManager.Claim(mutexType, scanned.New.Checksum, done)
if err := scanner.TxnManager.WithTxn(ctx, func(r models.Repository) error {
// free the mutex once transaction is complete
defer close(done)
var err error
// ensure no clashes of hashes
if scanned.New.Checksum != "" && scanned.Old.Checksum != scanned.New.Checksum {
dupe, _ := r.Image().FindByChecksum(i.Checksum)
if dupe != nil {
return fmt.Errorf("MD5 for file %s is the same as that of %s", path, dupe.Path)
}
}
retImage, err = r.Image().UpdateFull(*i)
return err
}); err != nil {
return nil, err
}
// remove the old thumbnail if the checksum changed - we'll regenerate it
if oldChecksum != scanned.New.Checksum {
// remove cache dir of gallery
err = os.Remove(scanner.Paths.Generated.GetThumbnailPath(oldChecksum, models.DefaultGthumbWidth))
if err != nil {
logger.Errorf("Error deleting thumbnail image: %s", err)
}
}
scanner.PluginCache.ExecutePostHooks(ctx, retImage.ID, plugin.ImageUpdatePost, nil, nil)
}
return
}
func (scanner *Scanner) ScanNew(ctx context.Context, f file.SourceFile) (retImage *models.Image, err error) {
scanned, err := scanner.Scanner.ScanNew(f)
if err != nil {
return nil, err
}
path := f.Path()
checksum := scanned.Checksum
// grab a mutex on the checksum
done := make(chan struct{})
scanner.MutexManager.Claim(mutexType, checksum, done)
defer close(done)
// check for image by checksum
var existingImage *models.Image
if err := scanner.TxnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error {
var err error
existingImage, err = r.Image().FindByChecksum(checksum)
return err
}); err != nil {
return nil, err
}
pathDisplayName := file.ZipPathDisplayName(path)
if existingImage != nil {
exists := FileExists(existingImage.Path)
if !scanner.CaseSensitiveFs {
// #1426 - if file exists but is a case-insensitive match for the
// original filename, then treat it as a move
if exists && strings.EqualFold(path, existingImage.Path) {
exists = false
}
}
if exists {
logger.Infof("%s already exists. Duplicate of %s ", pathDisplayName, file.ZipPathDisplayName(existingImage.Path))
return nil, nil
} else {
logger.Infof("%s already exists. Updating path...", pathDisplayName)
imagePartial := models.ImagePartial{
ID: existingImage.ID,
Path: &path,
}
if err := scanner.TxnManager.WithTxn(ctx, func(r models.Repository) error {
retImage, err = r.Image().Update(imagePartial)
return err
}); err != nil {
return nil, err
}
scanner.PluginCache.ExecutePostHooks(ctx, existingImage.ID, plugin.ImageUpdatePost, nil, nil)
}
} else {
logger.Infof("%s doesn't exist. Creating new item...", pathDisplayName)
currentTime := time.Now()
newImage := models.Image{
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}
newImage.SetFile(*scanned)
newImage.Title.String = GetFilename(&newImage, scanner.StripFileExtension)
newImage.Title.Valid = true
if err := SetFileDetails(&newImage); err != nil {
logger.Error(err.Error())
return nil, err
}
if err := scanner.TxnManager.WithTxn(ctx, func(r models.Repository) error {
var err error
retImage, err = r.Image().Create(newImage)
return err
}); err != nil {
return nil, err
}
scanner.PluginCache.ExecutePostHooks(ctx, retImage.ID, plugin.ImageCreatePost, nil, nil)
}
return
}