Skip to content

Commit

Permalink
Merge pull request #1586 from mried/RobustCaseSensitiveDetection
Browse files Browse the repository at this point in the history
A robust way to check for a case sensitive file system
  • Loading branch information
sampsyo committed Oct 7, 2015
2 parents 9f1c113 + b5f1f99 commit 3b604c7
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 11 deletions.
7 changes: 4 additions & 3 deletions beets/library.py
Expand Up @@ -24,7 +24,6 @@
import time
import re
from unidecode import unidecode
import platform

from beets import logging
from beets.mediafile import MediaFile, MutagenError, UnreadableFileError
Expand Down Expand Up @@ -61,9 +60,11 @@ def __init__(self, field, pattern, fast=True, case_sensitive=None):
"""
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.
Expand Down
50 changes: 50 additions & 0 deletions beets/util/__init__.py
Expand Up @@ -16,6 +16,7 @@

from __future__ import (division, absolute_import, print_function,
unicode_literals)
import ctypes

import os
import sys
Expand Down Expand Up @@ -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'
2 changes: 2 additions & 0 deletions docs/changelog.rst
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/query.rst
Expand Up @@ -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:

Expand Down
39 changes: 33 additions & 6 deletions test/test_query.py
Expand Up @@ -376,12 +376,17 @@ def setUp(self):
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'
Expand Down Expand Up @@ -503,14 +508,36 @@ def test_case_sensitivity(self):
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)

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):
Expand All @@ -526,7 +553,7 @@ def test_path_sep_detection(self, mock_os):

def test_path_detection(self):
# cover existence test
self.patcher.stop()
self.patcher_exists.stop()
is_path = beets.library.PathQuery.is_path_query

try:
Expand All @@ -546,7 +573,7 @@ def test_path_detection(self):
finally:
os.chdir(cur_dir)
finally:
self.patcher.start()
self.patcher_exists.start()


class IntQueryTest(unittest.TestCase, TestHelper):
Expand Down

0 comments on commit 3b604c7

Please sign in to comment.