mirror of
https://github.com/beetbox/beets.git
synced 2025-12-28 19:42:42 +01:00
Merge pull request #1586 from mried/RobustCaseSensitiveDetection
A robust way to check for a case sensitive file system
This commit is contained in:
commit
3b604c7ff9
5 changed files with 91 additions and 11 deletions
|
|
@ -24,7 +24,6 @@ import unicodedata
|
|||
import time
|
||||
import re
|
||||
from unidecode import unidecode
|
||||
import platform
|
||||
|
||||
from beets import logging
|
||||
from beets.mediafile import MediaFile, MutagenError, UnreadableFileError
|
||||
|
|
@ -61,9 +60,11 @@ class PathQuery(dbcore.FieldQuery):
|
|||
"""
|
||||
super(PathQuery, self).__init__(field, pattern, fast)
|
||||
|
||||
# By default, the case sensitivity depends on the platform.
|
||||
# By default, the case sensitivity depends on the filesystem
|
||||
# the library is located on.
|
||||
if case_sensitive is None:
|
||||
case_sensitive = platform.system() != 'Windows'
|
||||
case_sensitive = beets.util.is_filesystem_case_sensitive(
|
||||
beets.config['directory'].get())
|
||||
self.case_sensitive = case_sensitive
|
||||
|
||||
# Use a normalized-case pattern for case-insensitive matches.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
from __future__ import (division, absolute_import, print_function,
|
||||
unicode_literals)
|
||||
import ctypes
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -760,3 +761,52 @@ def interactive_open(targets, command=None):
|
|||
command += targets
|
||||
|
||||
return os.execlp(*command)
|
||||
|
||||
|
||||
def is_filesystem_case_sensitive(path):
|
||||
"""Checks if the filesystem at the given path is case sensitive.
|
||||
If the path does not exist, a case sensitive file system is
|
||||
assumed if the system is not windows.
|
||||
|
||||
:param path: The path to check for case sensitivity.
|
||||
:return: True if the file system is case sensitive, False else.
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
# Check if the path to the library exists in lower and upper case
|
||||
if os.path.exists(path.lower()) and \
|
||||
os.path.exists(path.upper()):
|
||||
# All the paths may exist on the file system. Check if they
|
||||
# refer to different files
|
||||
if platform.system() != 'Windows':
|
||||
# os.path.samefile is only available on Unix systems for
|
||||
# python < 3.0
|
||||
return not os.path.samefile(path.lower(),
|
||||
path.upper())
|
||||
|
||||
# On windows we use GetLongPathNameW to determine the real path
|
||||
# using the actual case.
|
||||
def get_long_path_name(short_path):
|
||||
if not isinstance(short_path, unicode):
|
||||
short_path = unicode(short_path)
|
||||
buf = ctypes.create_unicode_buffer(260)
|
||||
get_long_path_name_w = ctypes.windll.kernel32.GetLongPathNameW
|
||||
return_value = get_long_path_name_w(short_path, buf, 260)
|
||||
if return_value == 0 or return_value > 260:
|
||||
# An error occurred
|
||||
return short_path
|
||||
else:
|
||||
long_path = buf.value
|
||||
# GetLongPathNameW does not change the case of the drive
|
||||
# letter.
|
||||
if len(long_path) > 1 and long_path[1] == ':':
|
||||
long_path = long_path[0].upper() + long_path[1:]
|
||||
return long_path
|
||||
|
||||
lower = get_long_path_name(path.lower())
|
||||
upper = get_long_path_name(path.upper())
|
||||
|
||||
return lower != upper
|
||||
else:
|
||||
return True
|
||||
# By default, the case sensitivity depends on the platform.
|
||||
return platform.system() != 'Windows'
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ Fixes:
|
|||
written to files. Thanks to :user:`jdetrey`. :bug:`1303` :bug:`1589`
|
||||
* :doc:`/plugins/replaygain`: Avoid a crash when the PyAudioTools backend
|
||||
encounters an error. :bug:`1592`
|
||||
* The check whether the file system is case sensitive or not could lead to
|
||||
wrong results. It is much more robust now.
|
||||
* Case-insensitive path queries might have returned nothing because of a
|
||||
wrong SQL query.
|
||||
* Fix a crash when a query contains a "+" or "-" alone in a component.
|
||||
|
|
|
|||
|
|
@ -202,8 +202,8 @@ Note that this only matches items that are *already in your library*, so a path
|
|||
query won't necessarily find *all* the audio files in a directory---just the
|
||||
ones you've already added to your beets library.
|
||||
|
||||
Path queries are case-sensitive on most platforms but case-insensitive on
|
||||
Windows.
|
||||
Path queries are case-sensitive if the file system the library is located on
|
||||
is case-sensitive, case-insensitive otherwise.
|
||||
|
||||
.. _query-sort:
|
||||
|
||||
|
|
|
|||
|
|
@ -376,12 +376,17 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
|
|||
self.i.store()
|
||||
self.lib.add_album([self.i])
|
||||
|
||||
self.patcher = patch('beets.library.os.path.exists')
|
||||
self.patcher.start().return_value = True
|
||||
self.patcher_exists = patch('beets.library.os.path.exists')
|
||||
self.patcher_exists.start().return_value = True
|
||||
|
||||
self.patcher_samefile = patch('beets.library.os.path.samefile')
|
||||
self.patcher_samefile.start().return_value = True
|
||||
|
||||
def tearDown(self):
|
||||
super(PathQueryTest, self).tearDown()
|
||||
self.patcher.stop()
|
||||
|
||||
self.patcher_samefile.stop()
|
||||
self.patcher_exists.stop()
|
||||
|
||||
def test_path_exact_match(self):
|
||||
q = 'path:/a/b/c.mp3'
|
||||
|
|
@ -503,7 +508,27 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
|
|||
results = self.lib.items(makeq(case_sensitive=False))
|
||||
self.assert_items_matched(results, ['path item', 'caps path'])
|
||||
|
||||
# test platform-aware default sensitivity
|
||||
# Check for correct case sensitivity selection (this check
|
||||
# only works for non-windows os)
|
||||
with _common.system_mock('Darwin'):
|
||||
# exists = True and samefile = True => Case insensitive
|
||||
q = makeq()
|
||||
self.assertEqual(q.case_sensitive, False)
|
||||
|
||||
self.patcher_samefile.stop()
|
||||
self.patcher_samefile.start().return_value = False
|
||||
|
||||
# exists = True and samefile = False => Case sensitive
|
||||
q = makeq()
|
||||
self.assertEqual(q.case_sensitive, True)
|
||||
|
||||
self.patcher_samefile.stop()
|
||||
self.patcher_samefile.start().return_value = True
|
||||
|
||||
# test platform-aware default sensitivity when the library
|
||||
# path does not exist (exist = False)
|
||||
self.patcher_exists.stop()
|
||||
self.patcher_exists.start().return_value = False
|
||||
with _common.system_mock('Darwin'):
|
||||
q = makeq()
|
||||
self.assertEqual(q.case_sensitive, True)
|
||||
|
|
@ -511,6 +536,8 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
|
|||
with _common.system_mock('Windows'):
|
||||
q = makeq()
|
||||
self.assertEqual(q.case_sensitive, False)
|
||||
self.patcher_exists.stop()
|
||||
self.patcher_exists.start().return_value = True
|
||||
|
||||
@patch('beets.library.os')
|
||||
def test_path_sep_detection(self, mock_os):
|
||||
|
|
@ -526,7 +553,7 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
|
|||
|
||||
def test_path_detection(self):
|
||||
# cover existence test
|
||||
self.patcher.stop()
|
||||
self.patcher_exists.stop()
|
||||
is_path = beets.library.PathQuery.is_path_query
|
||||
|
||||
try:
|
||||
|
|
@ -546,7 +573,7 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin):
|
|||
finally:
|
||||
os.chdir(cur_dir)
|
||||
finally:
|
||||
self.patcher.start()
|
||||
self.patcher_exists.start()
|
||||
|
||||
|
||||
class IntQueryTest(unittest.TestCase, TestHelper):
|
||||
|
|
|
|||
Loading…
Reference in a new issue