fix escaping of / in paths on Windows

This commit is contained in:
Adrian Sampson 2010-09-27 16:56:40 -07:00
parent d453f5911d
commit 7cf10d13e5
4 changed files with 46 additions and 25 deletions

1
NEWS
View file

@ -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.

View file

@ -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
-------

View file

@ -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))

View file

@ -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):