mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-10 19:37:08 +02:00
On Windows, filepath.Rel never returns when the base is a UNC share root (\\host\share) and the target is the same root with a trailing separator (golang/go#79784, fixed only in Go 1.28 by CL 786100). A user on Windows Server with MusicFolder='\\server\Music' (discussion #6232) sent CPU profiles showing 96% of CPU time in filepath.Rel, called from fromArtistFolder <- resolveArtistFolderPattern <- resolver.resolveArtist. For an album directly under the library root, loadArtistFolder's filepath.Dir returns '\\server\Music\', so the artwork worker spins forever and, because Worker.drain waits for the whole batch, the artist artwork pool stops. Add utils.RelPath, which normalizes a UNC volume root followed only by separators to the volume plus one separator before calling filepath.Rel. Both sides then compare equal and Rel returns '.' before reaching the loop. A bare '\\host\share' target under a deeper base now also gives '..' instead of an error. Drive-letter paths are left unchanged, and on non-Windows systems the helper behaves exactly like filepath.Rel. Use it at every production filepath.Rel call site: artist folder artwork, scanner library-relative paths, the watcher, playlist path resolution and PlaylistsPath matching, and the cache spread FS. Windows-only tests cover the UNC cases, with a timeout so a regression fails instead of hanging CI. Run under Wine, 6 of them fail with plain filepath.Rel (4 by timeout) and all pass with RelPath. The helper can be removed after moving to Go 1.28.
248 lines
7.2 KiB
Go
248 lines
7.2 KiB
Go
package utils_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/utils"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("TempFileName", func() {
|
|
It("creates a temporary file name with prefix and suffix", func() {
|
|
prefix := "test-"
|
|
suffix := ".tmp"
|
|
result := utils.TempFileName(prefix, suffix)
|
|
|
|
Expect(result).To(ContainSubstring(prefix))
|
|
Expect(result).To(HaveSuffix(suffix))
|
|
Expect(result).To(ContainSubstring(os.TempDir()))
|
|
})
|
|
|
|
It("creates unique file names on multiple calls", func() {
|
|
prefix := "unique-"
|
|
suffix := ".test"
|
|
|
|
result1 := utils.TempFileName(prefix, suffix)
|
|
result2 := utils.TempFileName(prefix, suffix)
|
|
|
|
Expect(result1).NotTo(Equal(result2))
|
|
})
|
|
|
|
It("handles empty prefix and suffix", func() {
|
|
result := utils.TempFileName("", "")
|
|
|
|
Expect(result).To(ContainSubstring(os.TempDir()))
|
|
Expect(len(result)).To(BeNumerically(">", len(os.TempDir())))
|
|
})
|
|
|
|
It("creates proper file path separators", func() {
|
|
prefix := "path-test-"
|
|
suffix := ".ext"
|
|
result := utils.TempFileName(prefix, suffix)
|
|
|
|
expectedDir := os.TempDir()
|
|
Expect(result).To(HavePrefix(expectedDir))
|
|
Expect(strings.Count(result, string(filepath.Separator))).To(BeNumerically(">=", strings.Count(expectedDir, string(filepath.Separator))))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("BaseName", func() {
|
|
It("extracts basename from a simple filename", func() {
|
|
result := utils.BaseName("test.mp3")
|
|
Expect(result).To(Equal("test"))
|
|
})
|
|
|
|
It("extracts basename from a file path", func() {
|
|
result := utils.BaseName("/path/to/file.txt")
|
|
Expect(result).To(Equal("file"))
|
|
})
|
|
|
|
It("handles files without extension", func() {
|
|
result := utils.BaseName("/path/to/filename")
|
|
Expect(result).To(Equal("filename"))
|
|
})
|
|
|
|
It("handles files with multiple dots", func() {
|
|
result := utils.BaseName("archive.tar.gz")
|
|
Expect(result).To(Equal("archive.tar"))
|
|
})
|
|
|
|
It("handles hidden files", func() {
|
|
// For hidden files without additional extension, path.Ext returns the entire name
|
|
// So basename becomes empty string after TrimSuffix
|
|
result := utils.BaseName(".hidden")
|
|
Expect(result).To(Equal(""))
|
|
})
|
|
|
|
It("handles hidden files with extension", func() {
|
|
result := utils.BaseName(".config.json")
|
|
Expect(result).To(Equal(".config"))
|
|
})
|
|
|
|
It("handles empty string", func() {
|
|
// The actual behavior returns empty string for empty input
|
|
result := utils.BaseName("")
|
|
Expect(result).To(Equal(""))
|
|
})
|
|
|
|
It("handles path ending with separator", func() {
|
|
result := utils.BaseName("/path/to/dir/")
|
|
Expect(result).To(Equal("dir"))
|
|
})
|
|
|
|
It("handles complex nested path", func() {
|
|
result := utils.BaseName("/very/long/path/to/my/favorite/song.mp3")
|
|
Expect(result).To(Equal("song"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("CleanFileName", func() {
|
|
It("lowercases and replaces spaces with underscores", func() {
|
|
Expect(utils.CleanFileName("My Cool Playlist")).To(Equal("my_cool_playlist"))
|
|
})
|
|
|
|
It("strips special characters", func() {
|
|
Expect(utils.CleanFileName("Rock & Roll! (2024)")).To(Equal("rock__roll_2024"))
|
|
})
|
|
|
|
It("handles unicode characters", func() {
|
|
Expect(utils.CleanFileName("Música Favorita")).To(Equal("msica_favorita"))
|
|
})
|
|
|
|
It("preserves hyphens", func() {
|
|
Expect(utils.CleanFileName("lo-fi beats")).To(Equal("lo-fi_beats"))
|
|
})
|
|
|
|
It("returns empty string for empty input", func() {
|
|
Expect(utils.CleanFileName("")).To(BeEmpty())
|
|
})
|
|
|
|
It("returns empty string for whitespace-only input", func() {
|
|
Expect(utils.CleanFileName(" ")).To(BeEmpty())
|
|
})
|
|
|
|
It("returns empty string when all characters are stripped", func() {
|
|
Expect(utils.CleanFileName("!!!@@@###")).To(BeEmpty())
|
|
})
|
|
|
|
It("truncates to 50 characters", func() {
|
|
long := strings.Repeat("abcdefghij", 10) // 100 chars
|
|
result := utils.CleanFileName(long)
|
|
Expect(len(result)).To(Equal(50))
|
|
})
|
|
|
|
It("trims trailing underscores and hyphens after truncation", func() {
|
|
// 49 a's + space + "b" = after clean: 49 a's + "_b" = 51 chars, truncated to 50 = 49 a's + "_"
|
|
name := strings.Repeat("a", 49) + " b"
|
|
result := utils.CleanFileName(name)
|
|
Expect(result).To(Equal(strings.Repeat("a", 49)))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("FileExists", func() {
|
|
var tempFile *os.File
|
|
var tempDir string
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
tempFile, err = os.CreateTemp("", "fileexists-test-*.txt")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
tempDir, err = os.MkdirTemp("", "fileexists-test-dir-*")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
if tempFile != nil {
|
|
os.Remove(tempFile.Name())
|
|
tempFile.Close()
|
|
}
|
|
if tempDir != "" {
|
|
os.RemoveAll(tempDir)
|
|
}
|
|
})
|
|
|
|
It("returns true for existing file", func() {
|
|
Expect(utils.FileExists(tempFile.Name())).To(BeTrue())
|
|
})
|
|
|
|
It("returns true for existing directory", func() {
|
|
Expect(utils.FileExists(tempDir)).To(BeTrue())
|
|
})
|
|
|
|
It("returns false for non-existing file", func() {
|
|
nonExistentPath := filepath.Join(tempDir, "does-not-exist.txt")
|
|
Expect(utils.FileExists(nonExistentPath)).To(BeFalse())
|
|
})
|
|
|
|
It("returns false for empty path", func() {
|
|
Expect(utils.FileExists("")).To(BeFalse())
|
|
})
|
|
|
|
It("handles nested non-existing path", func() {
|
|
nonExistentPath := "/this/path/definitely/does/not/exist/file.txt"
|
|
Expect(utils.FileExists(nonExistentPath)).To(BeFalse())
|
|
})
|
|
|
|
Context("when file is deleted after creation", func() {
|
|
It("returns false after file deletion", func() {
|
|
filePath := tempFile.Name()
|
|
Expect(utils.FileExists(filePath)).To(BeTrue())
|
|
|
|
// Close the file before removing it. On Windows, an open handle
|
|
// holds a file lock and os.Remove fails; closing first makes the
|
|
// test cross-platform.
|
|
Expect(tempFile.Close()).To(Succeed())
|
|
err := os.Remove(filePath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
tempFile = nil // Prevent cleanup attempt
|
|
|
|
Expect(utils.FileExists(filePath)).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Context("when directory is deleted after creation", func() {
|
|
It("returns false after directory deletion", func() {
|
|
dirPath := tempDir
|
|
Expect(utils.FileExists(dirPath)).To(BeTrue())
|
|
|
|
err := os.RemoveAll(dirPath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
tempDir = "" // Prevent cleanup attempt
|
|
|
|
Expect(utils.FileExists(dirPath)).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
It("handles permission denied scenarios gracefully", func() {
|
|
// This test might be platform specific, but we test the general case
|
|
result := utils.FileExists("/root/.ssh/id_rsa") // Likely to not exist or be inaccessible
|
|
Expect(result).To(Or(BeTrue(), BeFalse())) // Should not panic
|
|
})
|
|
})
|
|
|
|
var _ = Describe("RelPath", func() {
|
|
DescribeTable("returns the same result as filepath.Rel",
|
|
func(base, target string) {
|
|
base, target = filepath.FromSlash(base), filepath.FromSlash(target)
|
|
expected, expectedErr := filepath.Rel(base, target)
|
|
rel, err := utils.RelPath(base, target)
|
|
Expect(rel).To(Equal(expected))
|
|
if expectedErr == nil {
|
|
Expect(err).ToNot(HaveOccurred())
|
|
} else {
|
|
Expect(err).To(MatchError(expectedErr.Error()))
|
|
}
|
|
},
|
|
Entry("same path", "/music", "/music"),
|
|
Entry("same path with trailing separator", "/music", "/music/"),
|
|
Entry("child path", "/music", "/music/artist/album"),
|
|
Entry("sibling path", "/music/a", "/music/b"),
|
|
Entry("parent path", "/music/artist", "/music"),
|
|
Entry("relative paths", "music", "music/artist"),
|
|
Entry("absolute and relative paths", "/music", "music"),
|
|
)
|
|
})
|