mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 08:54:10 +01:00
Handle case sensitive file moves (#1427)
This commit is contained in:
parent
f1786ad871
commit
dde361f9f3
4 changed files with 102 additions and 0 deletions
|
|
@ -72,6 +72,11 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||||
var galleries []string
|
var galleries []string
|
||||||
|
|
||||||
for _, sp := range paths {
|
for _, sp := range paths {
|
||||||
|
csFs, er := utils.IsFsPathCaseSensitive(sp.Path)
|
||||||
|
if er != nil {
|
||||||
|
logger.Warnf("Cannot determine fs case sensitivity: %s", er.Error())
|
||||||
|
}
|
||||||
|
|
||||||
err = walkFilesToScan(sp, func(path string, info os.FileInfo, err error) error {
|
err = walkFilesToScan(sp, func(path string, info os.FileInfo, err error) error {
|
||||||
if job.IsCancelled(ctx) {
|
if job.IsCancelled(ctx) {
|
||||||
return stoppingErr
|
return stoppingErr
|
||||||
|
|
@ -96,6 +101,7 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||||
GenerateSprite: utils.IsTrue(input.ScanGenerateSprites),
|
GenerateSprite: utils.IsTrue(input.ScanGenerateSprites),
|
||||||
GeneratePhash: utils.IsTrue(input.ScanGeneratePhashes),
|
GeneratePhash: utils.IsTrue(input.ScanGeneratePhashes),
|
||||||
progress: progress,
|
progress: progress,
|
||||||
|
CaseSensitiveFs: csFs,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -207,6 +213,7 @@ type ScanTask struct {
|
||||||
GenerateImagePreview bool
|
GenerateImagePreview bool
|
||||||
zipGallery *models.Gallery
|
zipGallery *models.Gallery
|
||||||
progress *job.Progress
|
progress *job.Progress
|
||||||
|
CaseSensitiveFs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ScanTask) Start(wg *sizedwaitgroup.SizedWaitGroup) {
|
func (t *ScanTask) Start(wg *sizedwaitgroup.SizedWaitGroup) {
|
||||||
|
|
@ -397,6 +404,14 @@ func (t *ScanTask) scanGallery() {
|
||||||
g, _ = qb.FindByChecksum(checksum)
|
g, _ = qb.FindByChecksum(checksum)
|
||||||
if g != nil {
|
if g != nil {
|
||||||
exists, _ := utils.FileExists(g.Path.String)
|
exists, _ := utils.FileExists(g.Path.String)
|
||||||
|
if !t.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(t.FilePath, g.Path.String) {
|
||||||
|
exists = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, g.Path.String)
|
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, g.Path.String)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -749,6 +764,14 @@ func (t *ScanTask) scanScene() *models.Scene {
|
||||||
|
|
||||||
if s != nil {
|
if s != nil {
|
||||||
exists, _ := utils.FileExists(s.Path)
|
exists, _ := utils.FileExists(s.Path)
|
||||||
|
if !t.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(t.FilePath, s.Path) {
|
||||||
|
exists = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
logger.Infof("%s already exists. Duplicate of %s", t.FilePath, s.Path)
|
logger.Infof("%s already exists. Duplicate of %s", t.FilePath, s.Path)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1034,6 +1057,14 @@ func (t *ScanTask) scanImage() {
|
||||||
|
|
||||||
if i != nil {
|
if i != nil {
|
||||||
exists := image.FileExists(i.Path)
|
exists := image.FileExists(i.Path)
|
||||||
|
if !t.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(t.FilePath, i.Path) {
|
||||||
|
exists = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
logger.Infof("%s already exists. Duplicate of %s ", image.PathDisplayName(t.FilePath), image.PathDisplayName(i.Path))
|
logger.Infof("%s already exists. Duplicate of %s ", image.PathDisplayName(t.FilePath), image.PathDisplayName(i.Path))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -306,3 +306,43 @@ func GetFunscriptPath(path string) string {
|
||||||
fn := strings.TrimSuffix(path, ext)
|
fn := strings.TrimSuffix(path, ext)
|
||||||
return fn + ".funscript"
|
return fn + ".funscript"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsFsPathCaseSensitive checks the fs of the given path to see if it is case sensitive
|
||||||
|
// if the case sensitivity can not be determined false and an error != nil are returned
|
||||||
|
func IsFsPathCaseSensitive(path string) (bool, error) {
|
||||||
|
// The case sensitivity of the fs of "path" is determined by case flipping
|
||||||
|
// the first letter rune from the base string of the path
|
||||||
|
// If the resulting flipped path exists then the fs should not be case sensitive
|
||||||
|
// ( we check the file mod time to avoid matching an existing path )
|
||||||
|
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil { // path cannot be stat'd
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
base := filepath.Base(path)
|
||||||
|
fBase, err := FlipCaseSingle(base)
|
||||||
|
if err != nil { // cannot be case flipped
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
i := strings.LastIndex(path, base)
|
||||||
|
if i < 0 { // shouldn't happen
|
||||||
|
return false, fmt.Errorf("could not case flip path %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
flipped := []byte(path)
|
||||||
|
for _, c := range []byte(fBase) { // replace base of path with the flipped one ( we need to flip the base or last dir part )
|
||||||
|
flipped[i] = c
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
fiCase, err := os.Stat(string(flipped))
|
||||||
|
if err != nil { // cannot stat the case flipped path
|
||||||
|
return true, nil // fs of path should be case sensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
if fiCase.ModTime() == fi.ModTime() { // file path exists and is the same
|
||||||
|
return false, nil // fs of path is not case sensitive
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("can not determine case sensitivity of path %s", path)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
||||||
|
|
@ -18,8 +19,37 @@ func RandomSequence(n int) string {
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlipCaseSingle flips the case ( lower<->upper ) of a single char from the string s
|
||||||
|
// If the string cannot be flipped, the original string value and an error are returned
|
||||||
|
func FlipCaseSingle(s string) (string, error) {
|
||||||
|
rr := []rune(s)
|
||||||
|
for i, r := range rr {
|
||||||
|
if unicode.IsLetter(r) { // look for a letter to flip
|
||||||
|
if unicode.IsUpper(r) {
|
||||||
|
rr[i] = unicode.ToLower(r)
|
||||||
|
return string(rr), nil
|
||||||
|
}
|
||||||
|
rr[i] = unicode.ToUpper(r)
|
||||||
|
return string(rr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return s, fmt.Errorf("could not case flip string %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
type StrFormatMap map[string]interface{}
|
type StrFormatMap map[string]interface{}
|
||||||
|
|
||||||
|
// StrFormat formats the provided format string, replacing placeholders
|
||||||
|
// in the form of "{fieldName}" with the values in the provided
|
||||||
|
// StrFormatMap.
|
||||||
|
//
|
||||||
|
// For example,
|
||||||
|
// StrFormat("{foo} bar {baz}", StrFormatMap{
|
||||||
|
// "foo": "bar",
|
||||||
|
// "baz": "abc",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// would return: "bar bar abc"
|
||||||
func StrFormat(format string, m StrFormatMap) string {
|
func StrFormat(format string, m StrFormatMap) string {
|
||||||
args := make([]string, len(m)*2)
|
args := make([]string, len(m)*2)
|
||||||
i := 0
|
i := 0
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))
|
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))
|
||||||
|
|
||||||
### 🐛 Bug fixes
|
### 🐛 Bug fixes
|
||||||
|
* Fix file move detection when case of filename is changed on case-insensitive file systems. ([#1426](https://github.com/stashapp/stash/issues/1426))
|
||||||
* Fix auto-tagger not tagging scenes with no whitespace in name. ([#1488](https://github.com/stashapp/stash/pull/1488))
|
* Fix auto-tagger not tagging scenes with no whitespace in name. ([#1488](https://github.com/stashapp/stash/pull/1488))
|
||||||
* Fix click/drag to select scenes. ([#1476](https://github.com/stashapp/stash/pull/1476))
|
* Fix click/drag to select scenes. ([#1476](https://github.com/stashapp/stash/pull/1476))
|
||||||
* Fix clearing Performer and Movie ratings not working. ([#1429](https://github.com/stashapp/stash/pull/1429))
|
* Fix clearing Performer and Movie ratings not working. ([#1429](https://github.com/stashapp/stash/pull/1429))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue