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