Skip to content

Commit 3b604c7

Browse files
committed
Merge pull request #1586 from mried/RobustCaseSensitiveDetection
A robust way to check for a case sensitive file system
2 parents 9f1c113 + b5f1f99 commit 3b604c7

File tree

5 files changed

+91
-11
lines changed

5 files changed

+91
-11
lines changed

beets/library.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import time
2525
import re
2626
from unidecode import unidecode
27-
import platform
2827

2928
from beets import logging
3029
from beets.mediafile import MediaFile, MutagenError, UnreadableFileError
@@ -61,9 +60,11 @@ def __init__(self, field, pattern, fast=True, case_sensitive=None):
6160
"""
6261
super(PathQuery, self).__init__(field, pattern, fast)
6362

64-
# By default, the case sensitivity depends on the platform.
63+
# By default, the case sensitivity depends on the filesystem
64+
# the library is located on.
6565
if case_sensitive is None:
66-
case_sensitive = platform.system() != 'Windows'
66+
case_sensitive = beets.util.is_filesystem_case_sensitive(
67+
beets.config['directory'].get())
6768
self.case_sensitive = case_sensitive
6869

6970
# Use a normalized-case pattern for case-insensitive matches.

beets/util/__init__.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import (division, absolute_import, print_function,
1818
unicode_literals)
19+
import ctypes
1920

2021
import os
2122
import sys
@@ -760,3 +761,52 @@ def interactive_open(targets, command=None):
760761
command += targets
761762

762763
return os.execlp(*command)
764+
765+
766+
def is_filesystem_case_sensitive(path):
767+
"""Checks if the filesystem at the given path is case sensitive.
768+
If the path does not exist, a case sensitive file system is
769+
assumed if the system is not windows.
770+
771+
:param path: The path to check for case sensitivity.
772+
:return: True if the file system is case sensitive, False else.
773+
"""
774+
if os.path.exists(path):
775+
# Check if the path to the library exists in lower and upper case
776+
if os.path.exists(path.lower()) and \
777+
os.path.exists(path.upper()):
778+
# All the paths may exist on the file system. Check if they
779+
# refer to different files
780+
if platform.system() != 'Windows':
781+
# os.path.samefile is only available on Unix systems for
782+
# python < 3.0
783+
return not os.path.samefile(path.lower(),
784+
path.upper())
785+
786+
# On windows we use GetLongPathNameW to determine the real path
787+
# using the actual case.
788+
def get_long_path_name(short_path):
789+
if not isinstance(short_path, unicode):
790+
short_path = unicode(short_path)
791+
buf = ctypes.create_unicode_buffer(260)
792+
get_long_path_name_w = ctypes.windll.kernel32.GetLongPathNameW
793+
return_value = get_long_path_name_w(short_path, buf, 260)
794+
if return_value == 0 or return_value > 260:
795+
# An error occurred
796+
return short_path
797+
else:
798+
long_path = buf.value
799+
# GetLongPathNameW does not change the case of the drive
800+
# letter.
801+
if len(long_path) > 1 and long_path[1] == ':':
802+
long_path = long_path[0].upper() + long_path[1:]
803+
return long_path
804+
805+
lower = get_long_path_name(path.lower())
806+
upper = get_long_path_name(path.upper())
807+
808+
return lower != upper
809+
else:
810+
return True
811+
# By default, the case sensitivity depends on the platform.
812+
return platform.system() != 'Windows'

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Fixes:
4646
written to files. Thanks to :user:`jdetrey`. :bug:`1303` :bug:`1589`
4747
* :doc:`/plugins/replaygain`: Avoid a crash when the PyAudioTools backend
4848
encounters an error. :bug:`1592`
49+
* The check whether the file system is case sensitive or not could lead to
50+
wrong results. It is much more robust now.
4951
* Case-insensitive path queries might have returned nothing because of a
5052
wrong SQL query.
5153
* Fix a crash when a query contains a "+" or "-" alone in a component.

docs/reference/query.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ Note that this only matches items that are *already in your library*, so a path
202202
query won't necessarily find *all* the audio files in a directory---just the
203203
ones you've already added to your beets library.
204204

205-
Path queries are case-sensitive on most platforms but case-insensitive on
206-
Windows.
205+
Path queries are case-sensitive if the file system the library is located on
206+
is case-sensitive, case-insensitive otherwise.
207207

208208
.. _query-sort:
209209

test/test_query.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,17 @@ def setUp(self):
376376
self.i.store()
377377
self.lib.add_album([self.i])
378378

379-
self.patcher = patch('beets.library.os.path.exists')
380-
self.patcher.start().return_value = True
379+
self.patcher_exists = patch('beets.library.os.path.exists')
380+
self.patcher_exists.start().return_value = True
381+
382+
self.patcher_samefile = patch('beets.library.os.path.samefile')
383+
self.patcher_samefile.start().return_value = True
381384

382385
def tearDown(self):
383386
super(PathQueryTest, self).tearDown()
384-
self.patcher.stop()
387+
388+
self.patcher_samefile.stop()
389+
self.patcher_exists.stop()
385390

386391
def test_path_exact_match(self):
387392
q = 'path:/a/b/c.mp3'
@@ -503,14 +508,36 @@ def test_case_sensitivity(self):
503508
results = self.lib.items(makeq(case_sensitive=False))
504509
self.assert_items_matched(results, ['path item', 'caps path'])
505510

506-
# test platform-aware default sensitivity
511+
# Check for correct case sensitivity selection (this check
512+
# only works for non-windows os)
513+
with _common.system_mock('Darwin'):
514+
# exists = True and samefile = True => Case insensitive
515+
q = makeq()
516+
self.assertEqual(q.case_sensitive, False)
517+
518+
self.patcher_samefile.stop()
519+
self.patcher_samefile.start().return_value = False
520+
521+
# exists = True and samefile = False => Case sensitive
522+
q = makeq()
523+
self.assertEqual(q.case_sensitive, True)
524+
525+
self.patcher_samefile.stop()
526+
self.patcher_samefile.start().return_value = True
527+
528+
# test platform-aware default sensitivity when the library
529+
# path does not exist (exist = False)
530+
self.patcher_exists.stop()
531+
self.patcher_exists.start().return_value = False
507532
with _common.system_mock('Darwin'):
508533
q = makeq()
509534
self.assertEqual(q.case_sensitive, True)
510535

511536
with _common.system_mock('Windows'):
512537
q = makeq()
513538
self.assertEqual(q.case_sensitive, False)
539+
self.patcher_exists.stop()
540+
self.patcher_exists.start().return_value = True
514541

515542
@patch('beets.library.os')
516543
def test_path_sep_detection(self, mock_os):
@@ -526,7 +553,7 @@ def test_path_sep_detection(self, mock_os):
526553

527554
def test_path_detection(self):
528555
# cover existence test
529-
self.patcher.stop()
556+
self.patcher_exists.stop()
530557
is_path = beets.library.PathQuery.is_path_query
531558

532559
try:
@@ -546,7 +573,7 @@ def test_path_detection(self):
546573
finally:
547574
os.chdir(cur_dir)
548575
finally:
549-
self.patcher.start()
576+
self.patcher_exists.start()
550577

551578

552579
class IntQueryTest(unittest.TestCase, TestHelper):

0 commit comments

Comments
 (0)