##// END OF EJS Templates
Safely encode paths in compress_user...
Sebastiaan Mathot -
Show More
@@ -1,446 +1,447 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for path handling.
3 Utilities for path handling.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import sys
10 import sys
11 import errno
11 import errno
12 import shutil
12 import shutil
13 import random
13 import random
14 import tempfile
14 import tempfile
15 import glob
15 import glob
16 from warnings import warn
16 from warnings import warn
17 from hashlib import md5
17 from hashlib import md5
18
18
19 from IPython.utils.process import system
19 from IPython.utils.process import system
20 from IPython.utils import py3compat
20 from IPython.utils import py3compat
21 from IPython.utils.decorators import undoc
21 from IPython.utils.decorators import undoc
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Code
24 # Code
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 fs_encoding = sys.getfilesystemencoding()
27 fs_encoding = sys.getfilesystemencoding()
28
28
29 def _writable_dir(path):
29 def _writable_dir(path):
30 """Whether `path` is a directory, to which the user has write access."""
30 """Whether `path` is a directory, to which the user has write access."""
31 return os.path.isdir(path) and os.access(path, os.W_OK)
31 return os.path.isdir(path) and os.access(path, os.W_OK)
32
32
33 if sys.platform == 'win32':
33 if sys.platform == 'win32':
34 def _get_long_path_name(path):
34 def _get_long_path_name(path):
35 """Get a long path name (expand ~) on Windows using ctypes.
35 """Get a long path name (expand ~) on Windows using ctypes.
36
36
37 Examples
37 Examples
38 --------
38 --------
39
39
40 >>> get_long_path_name('c:\\docume~1')
40 >>> get_long_path_name('c:\\docume~1')
41 u'c:\\\\Documents and Settings'
41 u'c:\\\\Documents and Settings'
42
42
43 """
43 """
44 try:
44 try:
45 import ctypes
45 import ctypes
46 except ImportError:
46 except ImportError:
47 raise ImportError('you need to have ctypes installed for this to work')
47 raise ImportError('you need to have ctypes installed for this to work')
48 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
48 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
49 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
49 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
50 ctypes.c_uint ]
50 ctypes.c_uint ]
51
51
52 buf = ctypes.create_unicode_buffer(260)
52 buf = ctypes.create_unicode_buffer(260)
53 rv = _GetLongPathName(path, buf, 260)
53 rv = _GetLongPathName(path, buf, 260)
54 if rv == 0 or rv > 260:
54 if rv == 0 or rv > 260:
55 return path
55 return path
56 else:
56 else:
57 return buf.value
57 return buf.value
58 else:
58 else:
59 def _get_long_path_name(path):
59 def _get_long_path_name(path):
60 """Dummy no-op."""
60 """Dummy no-op."""
61 return path
61 return path
62
62
63
63
64
64
65 def get_long_path_name(path):
65 def get_long_path_name(path):
66 """Expand a path into its long form.
66 """Expand a path into its long form.
67
67
68 On Windows this expands any ~ in the paths. On other platforms, it is
68 On Windows this expands any ~ in the paths. On other platforms, it is
69 a null operation.
69 a null operation.
70 """
70 """
71 return _get_long_path_name(path)
71 return _get_long_path_name(path)
72
72
73
73
74 def unquote_filename(name, win32=(sys.platform=='win32')):
74 def unquote_filename(name, win32=(sys.platform=='win32')):
75 """ On Windows, remove leading and trailing quotes from filenames.
75 """ On Windows, remove leading and trailing quotes from filenames.
76 """
76 """
77 if win32:
77 if win32:
78 if name.startswith(("'", '"')) and name.endswith(("'", '"')):
78 if name.startswith(("'", '"')) and name.endswith(("'", '"')):
79 name = name[1:-1]
79 name = name[1:-1]
80 return name
80 return name
81
81
82 def compress_user(path):
82 def compress_user(path):
83 """Reverse of :func:`os.path.expanduser`
83 """Reverse of :func:`os.path.expanduser`
84 """
84 """
85 path = py3compat.unicode_to_str(path, sys.getfilesystemencoding())
85 home = os.path.expanduser('~')
86 home = os.path.expanduser('~')
86 if path.startswith(home):
87 if path.startswith(home):
87 path = "~" + path[len(home):]
88 path = "~" + path[len(home):]
88 return path
89 return path
89
90
90 def get_py_filename(name, force_win32=None):
91 def get_py_filename(name, force_win32=None):
91 """Return a valid python filename in the current directory.
92 """Return a valid python filename in the current directory.
92
93
93 If the given name is not a file, it adds '.py' and searches again.
94 If the given name is not a file, it adds '.py' and searches again.
94 Raises IOError with an informative message if the file isn't found.
95 Raises IOError with an informative message if the file isn't found.
95
96
96 On Windows, apply Windows semantics to the filename. In particular, remove
97 On Windows, apply Windows semantics to the filename. In particular, remove
97 any quoting that has been applied to it. This option can be forced for
98 any quoting that has been applied to it. This option can be forced for
98 testing purposes.
99 testing purposes.
99 """
100 """
100
101
101 name = os.path.expanduser(name)
102 name = os.path.expanduser(name)
102 if force_win32 is None:
103 if force_win32 is None:
103 win32 = (sys.platform == 'win32')
104 win32 = (sys.platform == 'win32')
104 else:
105 else:
105 win32 = force_win32
106 win32 = force_win32
106 name = unquote_filename(name, win32=win32)
107 name = unquote_filename(name, win32=win32)
107 if not os.path.isfile(name) and not name.endswith('.py'):
108 if not os.path.isfile(name) and not name.endswith('.py'):
108 name += '.py'
109 name += '.py'
109 if os.path.isfile(name):
110 if os.path.isfile(name):
110 return name
111 return name
111 else:
112 else:
112 raise IOError('File `%r` not found.' % name)
113 raise IOError('File `%r` not found.' % name)
113
114
114
115
115 def filefind(filename, path_dirs=None):
116 def filefind(filename, path_dirs=None):
116 """Find a file by looking through a sequence of paths.
117 """Find a file by looking through a sequence of paths.
117
118
118 This iterates through a sequence of paths looking for a file and returns
119 This iterates through a sequence of paths looking for a file and returns
119 the full, absolute path of the first occurence of the file. If no set of
120 the full, absolute path of the first occurence of the file. If no set of
120 path dirs is given, the filename is tested as is, after running through
121 path dirs is given, the filename is tested as is, after running through
121 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
122 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
122
123
123 filefind('myfile.txt')
124 filefind('myfile.txt')
124
125
125 will find the file in the current working dir, but::
126 will find the file in the current working dir, but::
126
127
127 filefind('~/myfile.txt')
128 filefind('~/myfile.txt')
128
129
129 Will find the file in the users home directory. This function does not
130 Will find the file in the users home directory. This function does not
130 automatically try any paths, such as the cwd or the user's home directory.
131 automatically try any paths, such as the cwd or the user's home directory.
131
132
132 Parameters
133 Parameters
133 ----------
134 ----------
134 filename : str
135 filename : str
135 The filename to look for.
136 The filename to look for.
136 path_dirs : str, None or sequence of str
137 path_dirs : str, None or sequence of str
137 The sequence of paths to look for the file in. If None, the filename
138 The sequence of paths to look for the file in. If None, the filename
138 need to be absolute or be in the cwd. If a string, the string is
139 need to be absolute or be in the cwd. If a string, the string is
139 put into a sequence and the searched. If a sequence, walk through
140 put into a sequence and the searched. If a sequence, walk through
140 each element and join with ``filename``, calling :func:`expandvars`
141 each element and join with ``filename``, calling :func:`expandvars`
141 and :func:`expanduser` before testing for existence.
142 and :func:`expanduser` before testing for existence.
142
143
143 Returns
144 Returns
144 -------
145 -------
145 Raises :exc:`IOError` or returns absolute path to file.
146 Raises :exc:`IOError` or returns absolute path to file.
146 """
147 """
147
148
148 # If paths are quoted, abspath gets confused, strip them...
149 # If paths are quoted, abspath gets confused, strip them...
149 filename = filename.strip('"').strip("'")
150 filename = filename.strip('"').strip("'")
150 # If the input is an absolute path, just check it exists
151 # If the input is an absolute path, just check it exists
151 if os.path.isabs(filename) and os.path.isfile(filename):
152 if os.path.isabs(filename) and os.path.isfile(filename):
152 return filename
153 return filename
153
154
154 if path_dirs is None:
155 if path_dirs is None:
155 path_dirs = ("",)
156 path_dirs = ("",)
156 elif isinstance(path_dirs, py3compat.string_types):
157 elif isinstance(path_dirs, py3compat.string_types):
157 path_dirs = (path_dirs,)
158 path_dirs = (path_dirs,)
158
159
159 for path in path_dirs:
160 for path in path_dirs:
160 if path == '.': path = py3compat.getcwd()
161 if path == '.': path = py3compat.getcwd()
161 testname = expand_path(os.path.join(path, filename))
162 testname = expand_path(os.path.join(path, filename))
162 if os.path.isfile(testname):
163 if os.path.isfile(testname):
163 return os.path.abspath(testname)
164 return os.path.abspath(testname)
164
165
165 raise IOError("File %r does not exist in any of the search paths: %r" %
166 raise IOError("File %r does not exist in any of the search paths: %r" %
166 (filename, path_dirs) )
167 (filename, path_dirs) )
167
168
168
169
169 class HomeDirError(Exception):
170 class HomeDirError(Exception):
170 pass
171 pass
171
172
172
173
173 def get_home_dir(require_writable=False):
174 def get_home_dir(require_writable=False):
174 """Return the 'home' directory, as a unicode string.
175 """Return the 'home' directory, as a unicode string.
175
176
176 Uses os.path.expanduser('~'), and checks for writability.
177 Uses os.path.expanduser('~'), and checks for writability.
177
178
178 See stdlib docs for how this is determined.
179 See stdlib docs for how this is determined.
179 $HOME is first priority on *ALL* platforms.
180 $HOME is first priority on *ALL* platforms.
180
181
181 Parameters
182 Parameters
182 ----------
183 ----------
183
184
184 require_writable : bool [default: False]
185 require_writable : bool [default: False]
185 if True:
186 if True:
186 guarantees the return value is a writable directory, otherwise
187 guarantees the return value is a writable directory, otherwise
187 raises HomeDirError
188 raises HomeDirError
188 if False:
189 if False:
189 The path is resolved, but it is not guaranteed to exist or be writable.
190 The path is resolved, but it is not guaranteed to exist or be writable.
190 """
191 """
191
192
192 homedir = os.path.expanduser('~')
193 homedir = os.path.expanduser('~')
193 # Next line will make things work even when /home/ is a symlink to
194 # Next line will make things work even when /home/ is a symlink to
194 # /usr/home as it is on FreeBSD, for example
195 # /usr/home as it is on FreeBSD, for example
195 homedir = os.path.realpath(homedir)
196 homedir = os.path.realpath(homedir)
196
197
197 if not _writable_dir(homedir) and os.name == 'nt':
198 if not _writable_dir(homedir) and os.name == 'nt':
198 # expanduser failed, use the registry to get the 'My Documents' folder.
199 # expanduser failed, use the registry to get the 'My Documents' folder.
199 try:
200 try:
200 try:
201 try:
201 import winreg as wreg # Py 3
202 import winreg as wreg # Py 3
202 except ImportError:
203 except ImportError:
203 import _winreg as wreg # Py 2
204 import _winreg as wreg # Py 2
204 key = wreg.OpenKey(
205 key = wreg.OpenKey(
205 wreg.HKEY_CURRENT_USER,
206 wreg.HKEY_CURRENT_USER,
206 "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
207 "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
207 )
208 )
208 homedir = wreg.QueryValueEx(key,'Personal')[0]
209 homedir = wreg.QueryValueEx(key,'Personal')[0]
209 key.Close()
210 key.Close()
210 except:
211 except:
211 pass
212 pass
212
213
213 if (not require_writable) or _writable_dir(homedir):
214 if (not require_writable) or _writable_dir(homedir):
214 return py3compat.cast_unicode(homedir, fs_encoding)
215 return py3compat.cast_unicode(homedir, fs_encoding)
215 else:
216 else:
216 raise HomeDirError('%s is not a writable dir, '
217 raise HomeDirError('%s is not a writable dir, '
217 'set $HOME environment variable to override' % homedir)
218 'set $HOME environment variable to override' % homedir)
218
219
219 def get_xdg_dir():
220 def get_xdg_dir():
220 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
221 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
221
222
222 This is only for non-OS X posix (Linux,Unix,etc.) systems.
223 This is only for non-OS X posix (Linux,Unix,etc.) systems.
223 """
224 """
224
225
225 env = os.environ
226 env = os.environ
226
227
227 if os.name == 'posix' and sys.platform != 'darwin':
228 if os.name == 'posix' and sys.platform != 'darwin':
228 # Linux, Unix, AIX, etc.
229 # Linux, Unix, AIX, etc.
229 # use ~/.config if empty OR not set
230 # use ~/.config if empty OR not set
230 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
231 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
231 if xdg and _writable_dir(xdg):
232 if xdg and _writable_dir(xdg):
232 return py3compat.cast_unicode(xdg, fs_encoding)
233 return py3compat.cast_unicode(xdg, fs_encoding)
233
234
234 return None
235 return None
235
236
236
237
237 def get_xdg_cache_dir():
238 def get_xdg_cache_dir():
238 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
239 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
239
240
240 This is only for non-OS X posix (Linux,Unix,etc.) systems.
241 This is only for non-OS X posix (Linux,Unix,etc.) systems.
241 """
242 """
242
243
243 env = os.environ
244 env = os.environ
244
245
245 if os.name == 'posix' and sys.platform != 'darwin':
246 if os.name == 'posix' and sys.platform != 'darwin':
246 # Linux, Unix, AIX, etc.
247 # Linux, Unix, AIX, etc.
247 # use ~/.cache if empty OR not set
248 # use ~/.cache if empty OR not set
248 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
249 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
249 if xdg and _writable_dir(xdg):
250 if xdg and _writable_dir(xdg):
250 return py3compat.cast_unicode(xdg, fs_encoding)
251 return py3compat.cast_unicode(xdg, fs_encoding)
251
252
252 return None
253 return None
253
254
254
255
255 @undoc
256 @undoc
256 def get_ipython_dir():
257 def get_ipython_dir():
257 warn("get_ipython_dir has moved to the IPython.paths module")
258 warn("get_ipython_dir has moved to the IPython.paths module")
258 from IPython.paths import get_ipython_dir
259 from IPython.paths import get_ipython_dir
259 return get_ipython_dir()
260 return get_ipython_dir()
260
261
261 @undoc
262 @undoc
262 def get_ipython_cache_dir():
263 def get_ipython_cache_dir():
263 warn("get_ipython_cache_dir has moved to the IPython.paths module")
264 warn("get_ipython_cache_dir has moved to the IPython.paths module")
264 from IPython.paths import get_ipython_cache_dir
265 from IPython.paths import get_ipython_cache_dir
265 return get_ipython_cache_dir()
266 return get_ipython_cache_dir()
266
267
267 @undoc
268 @undoc
268 def get_ipython_package_dir():
269 def get_ipython_package_dir():
269 warn("get_ipython_package_dir has moved to the IPython.paths module")
270 warn("get_ipython_package_dir has moved to the IPython.paths module")
270 from IPython.paths import get_ipython_package_dir
271 from IPython.paths import get_ipython_package_dir
271 return get_ipython_package_dir()
272 return get_ipython_package_dir()
272
273
273 @undoc
274 @undoc
274 def get_ipython_module_path(module_str):
275 def get_ipython_module_path(module_str):
275 warn("get_ipython_module_path has moved to the IPython.paths module")
276 warn("get_ipython_module_path has moved to the IPython.paths module")
276 from IPython.paths import get_ipython_module_path
277 from IPython.paths import get_ipython_module_path
277 return get_ipython_module_path(module_str)
278 return get_ipython_module_path(module_str)
278
279
279 @undoc
280 @undoc
280 def locate_profile(profile='default'):
281 def locate_profile(profile='default'):
281 warn("locate_profile has moved to the IPython.paths module")
282 warn("locate_profile has moved to the IPython.paths module")
282 from IPython.paths import locate_profile
283 from IPython.paths import locate_profile
283 return locate_profile(profile=profile)
284 return locate_profile(profile=profile)
284
285
285 def expand_path(s):
286 def expand_path(s):
286 """Expand $VARS and ~names in a string, like a shell
287 """Expand $VARS and ~names in a string, like a shell
287
288
288 :Examples:
289 :Examples:
289
290
290 In [2]: os.environ['FOO']='test'
291 In [2]: os.environ['FOO']='test'
291
292
292 In [3]: expand_path('variable FOO is $FOO')
293 In [3]: expand_path('variable FOO is $FOO')
293 Out[3]: 'variable FOO is test'
294 Out[3]: 'variable FOO is test'
294 """
295 """
295 # This is a pretty subtle hack. When expand user is given a UNC path
296 # This is a pretty subtle hack. When expand user is given a UNC path
296 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
297 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
297 # the $ to get (\\server\share\%username%). I think it considered $
298 # the $ to get (\\server\share\%username%). I think it considered $
298 # alone an empty var. But, we need the $ to remains there (it indicates
299 # alone an empty var. But, we need the $ to remains there (it indicates
299 # a hidden share).
300 # a hidden share).
300 if os.name=='nt':
301 if os.name=='nt':
301 s = s.replace('$\\', 'IPYTHON_TEMP')
302 s = s.replace('$\\', 'IPYTHON_TEMP')
302 s = os.path.expandvars(os.path.expanduser(s))
303 s = os.path.expandvars(os.path.expanduser(s))
303 if os.name=='nt':
304 if os.name=='nt':
304 s = s.replace('IPYTHON_TEMP', '$\\')
305 s = s.replace('IPYTHON_TEMP', '$\\')
305 return s
306 return s
306
307
307
308
308 def unescape_glob(string):
309 def unescape_glob(string):
309 """Unescape glob pattern in `string`."""
310 """Unescape glob pattern in `string`."""
310 def unescape(s):
311 def unescape(s):
311 for pattern in '*[]!?':
312 for pattern in '*[]!?':
312 s = s.replace(r'\{0}'.format(pattern), pattern)
313 s = s.replace(r'\{0}'.format(pattern), pattern)
313 return s
314 return s
314 return '\\'.join(map(unescape, string.split('\\\\')))
315 return '\\'.join(map(unescape, string.split('\\\\')))
315
316
316
317
317 def shellglob(args):
318 def shellglob(args):
318 """
319 """
319 Do glob expansion for each element in `args` and return a flattened list.
320 Do glob expansion for each element in `args` and return a flattened list.
320
321
321 Unmatched glob pattern will remain as-is in the returned list.
322 Unmatched glob pattern will remain as-is in the returned list.
322
323
323 """
324 """
324 expanded = []
325 expanded = []
325 # Do not unescape backslash in Windows as it is interpreted as
326 # Do not unescape backslash in Windows as it is interpreted as
326 # path separator:
327 # path separator:
327 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
328 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
328 for a in args:
329 for a in args:
329 expanded.extend(glob.glob(a) or [unescape(a)])
330 expanded.extend(glob.glob(a) or [unescape(a)])
330 return expanded
331 return expanded
331
332
332
333
333 def target_outdated(target,deps):
334 def target_outdated(target,deps):
334 """Determine whether a target is out of date.
335 """Determine whether a target is out of date.
335
336
336 target_outdated(target,deps) -> 1/0
337 target_outdated(target,deps) -> 1/0
337
338
338 deps: list of filenames which MUST exist.
339 deps: list of filenames which MUST exist.
339 target: single filename which may or may not exist.
340 target: single filename which may or may not exist.
340
341
341 If target doesn't exist or is older than any file listed in deps, return
342 If target doesn't exist or is older than any file listed in deps, return
342 true, otherwise return false.
343 true, otherwise return false.
343 """
344 """
344 try:
345 try:
345 target_time = os.path.getmtime(target)
346 target_time = os.path.getmtime(target)
346 except os.error:
347 except os.error:
347 return 1
348 return 1
348 for dep in deps:
349 for dep in deps:
349 dep_time = os.path.getmtime(dep)
350 dep_time = os.path.getmtime(dep)
350 if dep_time > target_time:
351 if dep_time > target_time:
351 #print "For target",target,"Dep failed:",dep # dbg
352 #print "For target",target,"Dep failed:",dep # dbg
352 #print "times (dep,tar):",dep_time,target_time # dbg
353 #print "times (dep,tar):",dep_time,target_time # dbg
353 return 1
354 return 1
354 return 0
355 return 0
355
356
356
357
357 def target_update(target,deps,cmd):
358 def target_update(target,deps,cmd):
358 """Update a target with a given command given a list of dependencies.
359 """Update a target with a given command given a list of dependencies.
359
360
360 target_update(target,deps,cmd) -> runs cmd if target is outdated.
361 target_update(target,deps,cmd) -> runs cmd if target is outdated.
361
362
362 This is just a wrapper around target_outdated() which calls the given
363 This is just a wrapper around target_outdated() which calls the given
363 command if target is outdated."""
364 command if target is outdated."""
364
365
365 if target_outdated(target,deps):
366 if target_outdated(target,deps):
366 system(cmd)
367 system(cmd)
367
368
368 @undoc
369 @undoc
369 def filehash(path):
370 def filehash(path):
370 """Make an MD5 hash of a file, ignoring any differences in line
371 """Make an MD5 hash of a file, ignoring any differences in line
371 ending characters."""
372 ending characters."""
372 warn("filehash() is deprecated")
373 warn("filehash() is deprecated")
373 with open(path, "rU") as f:
374 with open(path, "rU") as f:
374 return md5(py3compat.str_to_bytes(f.read())).hexdigest()
375 return md5(py3compat.str_to_bytes(f.read())).hexdigest()
375
376
376 ENOLINK = 1998
377 ENOLINK = 1998
377
378
378 def link(src, dst):
379 def link(src, dst):
379 """Hard links ``src`` to ``dst``, returning 0 or errno.
380 """Hard links ``src`` to ``dst``, returning 0 or errno.
380
381
381 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
382 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
382 supported by the operating system.
383 supported by the operating system.
383 """
384 """
384
385
385 if not hasattr(os, "link"):
386 if not hasattr(os, "link"):
386 return ENOLINK
387 return ENOLINK
387 link_errno = 0
388 link_errno = 0
388 try:
389 try:
389 os.link(src, dst)
390 os.link(src, dst)
390 except OSError as e:
391 except OSError as e:
391 link_errno = e.errno
392 link_errno = e.errno
392 return link_errno
393 return link_errno
393
394
394
395
395 def link_or_copy(src, dst):
396 def link_or_copy(src, dst):
396 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
397 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
397
398
398 Attempts to maintain the semantics of ``shutil.copy``.
399 Attempts to maintain the semantics of ``shutil.copy``.
399
400
400 Because ``os.link`` does not overwrite files, a unique temporary file
401 Because ``os.link`` does not overwrite files, a unique temporary file
401 will be used if the target already exists, then that file will be moved
402 will be used if the target already exists, then that file will be moved
402 into place.
403 into place.
403 """
404 """
404
405
405 if os.path.isdir(dst):
406 if os.path.isdir(dst):
406 dst = os.path.join(dst, os.path.basename(src))
407 dst = os.path.join(dst, os.path.basename(src))
407
408
408 link_errno = link(src, dst)
409 link_errno = link(src, dst)
409 if link_errno == errno.EEXIST:
410 if link_errno == errno.EEXIST:
410 if os.stat(src).st_ino == os.stat(dst).st_ino:
411 if os.stat(src).st_ino == os.stat(dst).st_ino:
411 # dst is already a hard link to the correct file, so we don't need
412 # dst is already a hard link to the correct file, so we don't need
412 # to do anything else. If we try to link and rename the file
413 # to do anything else. If we try to link and rename the file
413 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
414 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
414 return
415 return
415
416
416 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
417 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
417 try:
418 try:
418 link_or_copy(src, new_dst)
419 link_or_copy(src, new_dst)
419 except:
420 except:
420 try:
421 try:
421 os.remove(new_dst)
422 os.remove(new_dst)
422 except OSError:
423 except OSError:
423 pass
424 pass
424 raise
425 raise
425 os.rename(new_dst, dst)
426 os.rename(new_dst, dst)
426 elif link_errno != 0:
427 elif link_errno != 0:
427 # Either link isn't supported, or the filesystem doesn't support
428 # Either link isn't supported, or the filesystem doesn't support
428 # linking, or 'src' and 'dst' are on different filesystems.
429 # linking, or 'src' and 'dst' are on different filesystems.
429 shutil.copy(src, dst)
430 shutil.copy(src, dst)
430
431
431 def ensure_dir_exists(path, mode=0o755):
432 def ensure_dir_exists(path, mode=0o755):
432 """ensure that a directory exists
433 """ensure that a directory exists
433
434
434 If it doesn't exist, try to create it and protect against a race condition
435 If it doesn't exist, try to create it and protect against a race condition
435 if another process is doing the same.
436 if another process is doing the same.
436
437
437 The default permissions are 755, which differ from os.makedirs default of 777.
438 The default permissions are 755, which differ from os.makedirs default of 777.
438 """
439 """
439 if not os.path.exists(path):
440 if not os.path.exists(path):
440 try:
441 try:
441 os.makedirs(path, mode=mode)
442 os.makedirs(path, mode=mode)
442 except OSError as e:
443 except OSError as e:
443 if e.errno != errno.EEXIST:
444 if e.errno != errno.EEXIST:
444 raise
445 raise
445 elif not os.path.isdir(path):
446 elif not os.path.isdir(path):
446 raise IOError("%r exists but is not a directory" % path)
447 raise IOError("%r exists but is not a directory" % path)
General Comments 0
You need to be logged in to leave comments. Login now