Skip to content

Commit

Permalink
Handle case sensitive file moves (#1427)
Browse files Browse the repository at this point in the history
  • Loading branch information
WithoutPants authored Jun 11, 2021
1 parent f1786ad commit dde361f
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 0 deletions.
31 changes: 31 additions & 0 deletions pkg/manager/task_scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
var galleries []string

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 {
if job.IsCancelled(ctx) {
return stoppingErr
Expand All @@ -96,6 +101,7 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
GenerateSprite: utils.IsTrue(input.ScanGenerateSprites),
GeneratePhash: utils.IsTrue(input.ScanGeneratePhashes),
progress: progress,
CaseSensitiveFs: csFs,
}

go func() {
Expand Down Expand Up @@ -207,6 +213,7 @@ type ScanTask struct {
GenerateImagePreview bool
zipGallery *models.Gallery
progress *job.Progress
CaseSensitiveFs bool
}

func (t *ScanTask) Start(wg *sizedwaitgroup.SizedWaitGroup) {
Expand Down Expand Up @@ -397,6 +404,14 @@ func (t *ScanTask) scanGallery() {
g, _ = qb.FindByChecksum(checksum)
if g != nil {
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 {
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, g.Path.String)
} else {
Expand Down Expand Up @@ -749,6 +764,14 @@ func (t *ScanTask) scanScene() *models.Scene {

if s != nil {
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 {
logger.Infof("%s already exists. Duplicate of %s", t.FilePath, s.Path)
} else {
Expand Down Expand Up @@ -1034,6 +1057,14 @@ func (t *ScanTask) scanImage() {

if i != nil {
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 {
logger.Infof("%s already exists. Duplicate of %s ", image.PathDisplayName(t.FilePath), image.PathDisplayName(i.Path))
} else {
Expand Down
40 changes: 40 additions & 0 deletions pkg/utils/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,43 @@ func GetFunscriptPath(path string) string {
fn := strings.TrimSuffix(path, ext)
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)
}
30 changes: 30 additions & 0 deletions pkg/utils/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math/rand"
"strings"
"time"
"unicode"
)

var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
Expand All @@ -18,8 +19,37 @@ func RandomSequence(n int) string {
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{}

// 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 {
args := make([]string, len(m)*2)
i := 0
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/src/components/Changelog/versions/v080.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))

### 🐛 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 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))
Expand Down

0 comments on commit dde361f

Please sign in to comment.