mirror of
https://github.com/beetbox/beets.git
synced 2025-12-28 03:22:39 +01:00
fix escaping of / in paths on Windows
This commit is contained in:
parent
d453f5911d
commit
7cf10d13e5
4 changed files with 46 additions and 25 deletions
1
NEWS
1
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-------
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue