mirror of https://github.com/stashapp/stash.git
226 lines
5.4 KiB
Go
226 lines
5.4 KiB
Go
package gallery
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/stashapp/stash/pkg/file"
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/manager/paths"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/plugin"
|
|
"github.com/stashapp/stash/pkg/utils"
|
|
)
|
|
|
|
const mutexType = "gallery"
|
|
|
|
type Scanner struct {
|
|
file.Scanner
|
|
|
|
ImageExtensions []string
|
|
StripFileExtension bool
|
|
Ctx context.Context
|
|
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(existing file.FileBased, file file.SourceFile) (retGallery *models.Gallery, scanImages bool, err error) {
|
|
scanned, err := scanner.Scanner.ScanExisting(existing, file)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// we don't currently store sizes for gallery files
|
|
// clear the file size so that we don't incorrectly detect a
|
|
// change
|
|
scanned.New.Size = ""
|
|
|
|
retGallery = existing.(*models.Gallery)
|
|
|
|
path := scanned.New.Path
|
|
|
|
changed := false
|
|
|
|
if scanned.ContentsChanged() {
|
|
retGallery.SetFile(*scanned.New)
|
|
changed = true
|
|
} else if scanned.FileUpdated() {
|
|
logger.Infof("Updated gallery file %s", path)
|
|
|
|
retGallery.SetFile(*scanned.New)
|
|
changed = true
|
|
}
|
|
|
|
if changed {
|
|
scanImages = true
|
|
logger.Infof("%s has been updated: rescanning", path)
|
|
|
|
retGallery.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(context.TODO(), func(r models.Repository) error {
|
|
// free the mutex once transaction is complete
|
|
defer close(done)
|
|
|
|
// ensure no clashes of hashes
|
|
if scanned.New.Checksum != "" && scanned.Old.Checksum != scanned.New.Checksum {
|
|
dupe, _ := r.Gallery().FindByChecksum(retGallery.Checksum)
|
|
if dupe != nil {
|
|
return fmt.Errorf("MD5 for file %s is the same as that of %s", path, dupe.Path.String)
|
|
}
|
|
}
|
|
|
|
retGallery, err = r.Gallery().Update(*retGallery)
|
|
return err
|
|
}); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
scanner.PluginCache.ExecutePostHooks(scanner.Ctx, retGallery.ID, plugin.GalleryUpdatePost, nil, nil)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (scanner *Scanner) ScanNew(file file.SourceFile) (retGallery *models.Gallery, scanImages bool, err error) {
|
|
scanned, err := scanner.Scanner.ScanNew(file)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
path := file.Path()
|
|
checksum := scanned.Checksum
|
|
isNewGallery := false
|
|
isUpdatedGallery := false
|
|
var g *models.Gallery
|
|
|
|
// grab a mutex on the checksum
|
|
done := make(chan struct{})
|
|
scanner.MutexManager.Claim(mutexType, checksum, done)
|
|
defer close(done)
|
|
|
|
if err := scanner.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
|
qb := r.Gallery()
|
|
|
|
g, _ = qb.FindByChecksum(checksum)
|
|
if g != nil {
|
|
exists, _ := utils.FileExists(g.Path.String)
|
|
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, g.Path.String) {
|
|
exists = false
|
|
}
|
|
}
|
|
|
|
if exists {
|
|
logger.Infof("%s already exists. Duplicate of %s ", path, g.Path.String)
|
|
} else {
|
|
logger.Infof("%s already exists. Updating path...", path)
|
|
g.Path = sql.NullString{
|
|
String: path,
|
|
Valid: true,
|
|
}
|
|
g, err = qb.Update(*g)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isUpdatedGallery = true
|
|
}
|
|
} else if scanner.hasImages(path) { // don't create gallery if it has no images
|
|
currentTime := time.Now()
|
|
|
|
g = &models.Gallery{
|
|
Zip: true,
|
|
Title: sql.NullString{
|
|
String: utils.GetNameFromPath(path, scanner.StripFileExtension),
|
|
Valid: true,
|
|
},
|
|
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
|
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
|
}
|
|
|
|
g.SetFile(*scanned)
|
|
|
|
// only warn when creating the gallery
|
|
ok, err := utils.IsZipFileUncompressed(path)
|
|
if err == nil && !ok {
|
|
logger.Warnf("%s is using above store (0) level compression.", path)
|
|
}
|
|
|
|
logger.Infof("%s doesn't exist. Creating new item...", path)
|
|
g, err = qb.Create(*g)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
scanImages = true
|
|
isNewGallery = true
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if isNewGallery {
|
|
scanner.PluginCache.ExecutePostHooks(scanner.Ctx, g.ID, plugin.GalleryCreatePost, nil, nil)
|
|
} else if isUpdatedGallery {
|
|
scanner.PluginCache.ExecutePostHooks(scanner.Ctx, g.ID, plugin.GalleryUpdatePost, nil, nil)
|
|
}
|
|
|
|
scanImages = isNewGallery
|
|
retGallery = g
|
|
|
|
return
|
|
}
|
|
|
|
func (scanner *Scanner) isImage(pathname string) bool {
|
|
return utils.MatchExtension(pathname, scanner.ImageExtensions)
|
|
}
|
|
|
|
func (scanner *Scanner) hasImages(path string) bool {
|
|
readCloser, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
logger.Warnf("Error while walking gallery zip: %v", err)
|
|
return false
|
|
}
|
|
defer readCloser.Close()
|
|
|
|
for _, file := range readCloser.File {
|
|
if file.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(file.Name, "__MACOSX") {
|
|
continue
|
|
}
|
|
|
|
if !scanner.isImage(file.Name) {
|
|
continue
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|