From 7cf10d13e531885f1095732de4309f17d34d2d28 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 27 Sep 2010 16:56:40 -0700 Subject: [PATCH] fix escaping of / in paths on Windows --- NEWS | 1 + README.rst | 5 +++-- beets/library.py | 47 ++++++++++++++++++++++++++++------------------- test/test_db.py | 18 ++++++++++++++---- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/NEWS b/NEWS index 3569bc832..7a04d6b16 100644 --- a/NEWS +++ b/NEWS @@ -31,6 +31,7 @@ * Fixed bug that logged the wrong paths when using "import -l". * Fixed autotagging for the creatively-named band !!!. * Fixed normalization of relative paths. +* Fixed escaping of / characters in paths on Windows. * Efficiency tweak should reduce the number of MusicBrainz queries per autotagged album. * A new "-v" command line switch enables debugging output. diff --git a/README.rst b/README.rst index 84544bc84..97ab40d66 100644 --- a/README.rst +++ b/README.rst @@ -27,14 +27,15 @@ play music in your beets library using a staggering variety of interfaces. Read More --------- -Learn more about beets at `its Web site`_. +Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Twitter for +news and updates. Check out the `Getting Started`_ guide to learn about installing and using beets. .. _its Web site: http://beets.radbox.org/ .. _Getting Started: http://code.google.com/p/beets/wiki/GettingStarted - +.. _@b33ts: http://twitter.com/b33ts/ Authors ------- diff --git a/beets/library.py b/beets/library.py index 421d34894..67f41604a 100644 --- a/beets/library.py +++ b/beets/library.py @@ -113,16 +113,17 @@ def _normpath(path): """ return os.path.normpath(os.path.abspath(os.path.expanduser(path))) -def _ancestry(path): +def _ancestry(path, pathmod=None): """Return a list consisting of path's parent directory, its grandparent, and so on. For instance: >>> _ancestry('/a/b/c') ['/', '/a', '/a/b'] """ + pathmod = pathmod or os.path out = [] last_path = None while path: - path = os.path.dirname(path) + path = pathmod.dirname(path) if path == last_path: break @@ -140,21 +141,22 @@ def _mkdirall(path): if not os.path.isdir(ancestor): os.mkdir(ancestor) -def _components(path): +def _components(path, pathmod=None): """Return a list of the path components in path. For instance: >>> _components('/a/b/c') ['a', 'b', 'c'] """ + pathmod = pathmod or os.path comps = [] - ances = _ancestry(path) + ances = _ancestry(path, pathmod) for anc in ances: - comp = os.path.basename(anc) + comp = pathmod.basename(anc) if comp: comps.append(comp) else: # root comps.append(anc) - last = os.path.basename(path) + last = pathmod.basename(path) if last: comps.append(last) @@ -182,17 +184,21 @@ CHAR_REPLACE = [ (re.compile(r':'), '-'), ] CHAR_REPLACE_WINDOWS = re.compile('["\*<>\|]|^\.|\.$'), '_' -def _sanitize_path(path, plat=None): - """Takes a path and makes sure that it is legal for the specified - platform (as returned by platform.system()). Returns a new path. +def _sanitize_path(path, pathmod=None): + """Takes a path and makes sure that it is legal. Returns a new path. + Only works with fragments; won't work reliably on Windows when a + path begins with a drive letter. Path separators (including altsep!) + should already be cleaned from the path components. """ - plat = plat or platform.system() - comps = _components(path) + pathmod = pathmod or os.path + windows = pathmod.__name__ == 'ntpath' + + comps = _components(path, pathmod) for i, comp in enumerate(comps): # Replace special characters. for regex, repl in CHAR_REPLACE: comp = regex.sub(repl, comp) - if plat == 'Windows': + if windows: regex, repl = CHAR_REPLACE_WINDOWS comp = regex.sub(repl, comp) @@ -201,7 +207,7 @@ def _sanitize_path(path, plat=None): comp = comp[:MAX_FILENAME_LENGTH] comps[i] = comp - return os.path.join(*comps) + return pathmod.join(*comps) # Library items (songs). @@ -802,12 +808,13 @@ class Library(BaseLibrary): self.conn.executescript(setup_sql) self.conn.commit() - def destination(self, item): + def destination(self, item, pathmod=None): """Returns the path in the library directory designated for item item (i.e., where the file ought to be). """ + pathmod = pathmod or os.path subpath_tmpl = Template(self.path_format) - + # Get the item's Album if it has one. album = self.get_album(item) @@ -823,10 +830,12 @@ class Library(BaseLibrary): # From Item. value = getattr(item, key) - # Sanitize the value for inclusion in a path: - # replace / and leading . with _ + # Sanitize the value for inclusion in a path: replace + # separators with _, etc. if isinstance(value, basestring): - value = value.replace(os.sep, '_') + for sep in (pathmod.sep, pathmod.altsep): + if sep: + value = value.replace(sep, '_') elif key in ('track', 'tracktotal', 'disc', 'disctotal'): # pad with zeros value = '%02i' % value @@ -846,7 +855,7 @@ class Library(BaseLibrary): subpath = _sanitize_path(subpath) # Preserve extension. - _, extension = os.path.splitext(item.path) + _, extension = pathmod.splitext(item.path) subpath += extension return _normpath(os.path.join(self.directory, subpath)) diff --git a/test/test_db.py b/test/test_db.py index 3f9ee64f6..76d503046 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -19,6 +19,8 @@ import unittest import sys import os import sqlite3 +import ntpath +import posixpath sys.path.append('..') import beets.library @@ -227,16 +229,24 @@ class DestinationTest(unittest.TestCase): dest = self.lib.destination(self.i) self.assertEqual(dest[-5:], '.extn') + def test_distination_windows_removes_both_separators(self): + self.i.title = 'one \\ two / three.mp3' + p = self.lib.destination(self.i, ntpath) + self.assertFalse('one \\ two' in p) + self.assertFalse('one / two' in p) + self.assertFalse('two \\ three' in p) + self.assertFalse('two / three' in p) + def test_sanitize_unix_replaces_leading_dot(self): - p = beets.library._sanitize_path('one/.two/three', 'Darwin') + p = beets.library._sanitize_path('one/.two/three', posixpath) self.assertFalse('.' in p) def test_sanitize_windows_replaces_trailing_dot(self): - p = beets.library._sanitize_path('one/two./three', 'Windows') + p = beets.library._sanitize_path('one/two./three', ntpath) self.assertFalse('.' in p) def test_sanitize_windows_replaces_illegal_chars(self): - p = beets.library._sanitize_path(':*?"<>|', 'Windows') + p = beets.library._sanitize_path(':*?"<>|', ntpath) self.assertFalse(':' in p) self.assertFalse('*' in p) self.assertFalse('?' in p) @@ -246,7 +256,7 @@ class DestinationTest(unittest.TestCase): self.assertFalse('|' in p) def test_sanitize_replaces_colon_with_dash(self): - p = beets.library._sanitize_path(u':', 'Darwin') + p = beets.library._sanitize_path(u':', posixpath) self.assertEqual(p, u'-') def test_path_with_format(self):