From a61f1387b09e1a9ea36ac1150d8b7c9a64b7eedd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 14 Aug 2011 19:03:21 -0600 Subject: [PATCH] Fix #825706 (Private bug) --- src/calibre/devices/usbms/cli.py | 8 +-- src/calibre/devices/usbms/driver.py | 4 +- src/calibre/utils/filenames.py | 85 ++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/calibre/devices/usbms/cli.py b/src/calibre/devices/usbms/cli.py index 1554d6fce0..4ff9efef8b 100644 --- a/src/calibre/devices/usbms/cli.py +++ b/src/calibre/devices/usbms/cli.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' import os, shutil, time from calibre.devices.errors import PathError +from calibre.utils.filenames import case_preserving_open_file class File(object): @@ -46,10 +47,8 @@ class CLI(object): path = os.path.join(path, infile.name) if not replace_file and os.path.exists(path): raise PathError('File already exists: ' + path) - d = os.path.dirname(path) - if not os.path.exists(d): - os.makedirs(d) - with open(path, 'w+b') as dest: + dest, actual_path = case_preserving_open_file(path) + with dest: try: shutil.copyfileobj(infile, dest) except IOError: @@ -62,6 +61,7 @@ class CLI(object): #if not check_transfer(infile, dest): raise Exception('Transfer failed') if close: infile.close() + return actual_path def munge_path(self, path): if path.startswith('/') and not (path.startswith(self._main_prefix) or \ diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 89531ec057..e09876081b 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -258,10 +258,10 @@ class USBMS(CLI, Device): for i, infile in enumerate(files): mdata, fname = metadata.next(), names.next() filepath = self.normalize_path(self.create_upload_path(path, mdata, fname)) - paths.append(filepath) if not hasattr(infile, 'read'): infile = self.normalize_path(infile) - self.put_file(infile, filepath, replace_file=True) + filepath = self.put_file(infile, filepath, replace_file=True) + paths.append(filepath) try: self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py index 8c6daa5adf..61a0b8e398 100644 --- a/src/calibre/utils/filenames.py +++ b/src/calibre/utils/filenames.py @@ -3,11 +3,12 @@ Make strings safe for use as ASCII filenames, while trying to preserve as much meaning as possible. ''' -import os +import os, errno from math import ceil -from calibre import sanitize_file_name -from calibre.constants import preferred_encoding, iswindows +from calibre import sanitize_file_name, isbytestring, force_unicode +from calibre.constants import (preferred_encoding, iswindows, + filesystem_encoding) from calibre.utils.localization import get_udc def ascii_text(orig): @@ -114,3 +115,81 @@ def is_case_sensitive(path): os.remove(f1) return is_case_sensitive +def case_preserving_open_file(path, mode='wb', mkdir_mode=0777): + ''' + Open the file pointed to by path with the specified mode. If any + directories in path do not exist, they are created. Returns the + opened file object and the path to the opened file object. This path is + guaranteed to have the same case as the on disk path. For case insensitive + filesystems, the returned path may be different from the passed in path. + The returned path is always unicode and always an absolute path. + + If mode is None, then this function assumes that path points to a directory + and return the path to the directory as the file object. + + mkdir_mode specifies the mode with which any missing directories in path + are created. + ''' + if isbytestring(path): + path = path.decode(filesystem_encoding) + + path = os.path.abspath(path) + + sep = force_unicode(os.sep, 'ascii') + + if path.endswith(sep): + path = path[:-1] + if not path: + raise ValueError('Path must not point to root') + + components = path.split(sep) + if not components: + raise ValueError('Invalid path: %r'%path) + + cpath = sep + if iswindows: + # Always upper case the drive letter and add a trailing slash so that + # the first os.listdir works correctly + cpath = components[0].upper() + sep + + # Create all the directories in path, putting the on disk case version of + # the directory into cpath + dirs = components[1:] if mode is None else components[1:-1] + for comp in dirs: + cdir = os.path.join(cpath, comp) + try: + os.mkdir(cdir, mkdir_mode) + except OSError as e: + if e.errno != errno.EEXIST: + if not os.path.exists(cdir): + # Check for exists again, as we could have got a permission + # denied error + raise + # This component already exists, ensure the case is correct + cl = comp.lower() + candidates = [c for c in os.listdir(cpath) if c.lower() == cl] + if len(candidates) == 1: + cdir = os.path.join(cpath, candidates[0]) + # else: We are on a case sensitive file system so cdir must already + # be correct + cpath = cdir + + if mode is None: + ans = fpath = cpath + else: + fname = components[-1] + ans = open(os.path.join(cpath, fname), mode) + # Ensure file and all its metadata is written to disk so that subsequent + # listdir() has file name in it. I don't know if this is actually + # necessary, but given the diversity of platforms, best to be safe. + ans.flush() + os.fsync(ans.fileno()) + + cl = fname.lower() + candidates = [c for c in os.listdir(cpath) if c.lower() == cl] + if len(candidates) == 1: + fpath = os.path.join(cpath, candidates[0]) + else: + fpath = ans.name + return ans, fpath +