Commits

Adrian Sampson committed cb6760f

fix escaping of / in paths on Windows

  • Participants
  • Parent commits ec582b7

Comments (0)

Files changed (4)

 * 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.
 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
 -------
     """
     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
         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)
     
     (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)
         
             comp = comp[:MAX_FILENAME_LENGTH]
                 
         comps[i] = comp
-    return os.path.join(*comps)
+    return pathmod.join(*comps)
 
 
 # Library items (songs).
         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)
         
                 # 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
         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))   
 import sys
 import os
 import sqlite3
+import ntpath
+import posixpath
 sys.path.append('..')
 import beets.library
 
         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)
         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):