173 lines
5.3 KiB
Python
173 lines
5.3 KiB
Python
|
# encoding: utf-8
|
||
|
"""
|
||
|
Utilities for path handling.
|
||
|
"""
|
||
|
|
||
|
# Copyright (c) IPython Development Team.
|
||
|
# Distributed under the terms of the Modified BSD License.
|
||
|
|
||
|
import os
|
||
|
import sys
|
||
|
import errno
|
||
|
import shutil
|
||
|
import random
|
||
|
|
||
|
from . import py3compat
|
||
|
|
||
|
|
||
|
fs_encoding = sys.getfilesystemencoding()
|
||
|
|
||
|
|
||
|
def filefind(filename, path_dirs=None):
|
||
|
"""Find a file by looking through a sequence of paths.
|
||
|
|
||
|
This iterates through a sequence of paths looking for a file and returns
|
||
|
the full, absolute path of the first occurence of the file. If no set of
|
||
|
path dirs is given, the filename is tested as is, after running through
|
||
|
:func:`expandvars` and :func:`expanduser`. Thus a simple call::
|
||
|
|
||
|
filefind('myfile.txt')
|
||
|
|
||
|
will find the file in the current working dir, but::
|
||
|
|
||
|
filefind('~/myfile.txt')
|
||
|
|
||
|
Will find the file in the users home directory. This function does not
|
||
|
automatically try any paths, such as the cwd or the user's home directory.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
filename : str
|
||
|
The filename to look for.
|
||
|
path_dirs : str, None or sequence of str
|
||
|
The sequence of paths to look for the file in. If None, the filename
|
||
|
need to be absolute or be in the cwd. If a string, the string is
|
||
|
put into a sequence and the searched. If a sequence, walk through
|
||
|
each element and join with ``filename``, calling :func:`expandvars`
|
||
|
and :func:`expanduser` before testing for existence.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Raises :exc:`IOError` or returns absolute path to file.
|
||
|
"""
|
||
|
|
||
|
# If paths are quoted, abspath gets confused, strip them...
|
||
|
filename = filename.strip('"').strip("'")
|
||
|
# If the input is an absolute path, just check it exists
|
||
|
if os.path.isabs(filename) and os.path.isfile(filename):
|
||
|
return filename
|
||
|
|
||
|
if path_dirs is None:
|
||
|
path_dirs = ("",)
|
||
|
elif isinstance(path_dirs, py3compat.string_types):
|
||
|
path_dirs = (path_dirs,)
|
||
|
|
||
|
for path in path_dirs:
|
||
|
if path == '.': path = py3compat.getcwd()
|
||
|
testname = expand_path(os.path.join(path, filename))
|
||
|
if os.path.isfile(testname):
|
||
|
return os.path.abspath(testname)
|
||
|
|
||
|
raise IOError("File %r does not exist in any of the search paths: %r" %
|
||
|
(filename, path_dirs) )
|
||
|
|
||
|
|
||
|
def expand_path(s):
|
||
|
"""Expand $VARS and ~names in a string, like a shell
|
||
|
|
||
|
:Examples:
|
||
|
|
||
|
In [2]: os.environ['FOO']='test'
|
||
|
|
||
|
In [3]: expand_path('variable FOO is $FOO')
|
||
|
Out[3]: 'variable FOO is test'
|
||
|
"""
|
||
|
# This is a pretty subtle hack. When expand user is given a UNC path
|
||
|
# on Windows (\\server\share$\%username%), os.path.expandvars, removes
|
||
|
# the $ to get (\\server\share\%username%). I think it considered $
|
||
|
# alone an empty var. But, we need the $ to remains there (it indicates
|
||
|
# a hidden share).
|
||
|
if os.name=='nt':
|
||
|
s = s.replace('$\\', 'IPYTHON_TEMP')
|
||
|
s = os.path.expandvars(os.path.expanduser(s))
|
||
|
if os.name=='nt':
|
||
|
s = s.replace('IPYTHON_TEMP', '$\\')
|
||
|
return s
|
||
|
|
||
|
|
||
|
try:
|
||
|
ENOLINK = errno.ENOLINK
|
||
|
except AttributeError:
|
||
|
ENOLINK = 1998
|
||
|
|
||
|
def link(src, dst):
|
||
|
"""Hard links ``src`` to ``dst``, returning 0 or errno.
|
||
|
|
||
|
Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
|
||
|
supported by the operating system.
|
||
|
"""
|
||
|
|
||
|
if not hasattr(os, "link"):
|
||
|
return ENOLINK
|
||
|
link_errno = 0
|
||
|
try:
|
||
|
os.link(src, dst)
|
||
|
except OSError as e:
|
||
|
link_errno = e.errno
|
||
|
return link_errno
|
||
|
|
||
|
|
||
|
def link_or_copy(src, dst):
|
||
|
"""Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
|
||
|
|
||
|
Attempts to maintain the semantics of ``shutil.copy``.
|
||
|
|
||
|
Because ``os.link`` does not overwrite files, a unique temporary file
|
||
|
will be used if the target already exists, then that file will be moved
|
||
|
into place.
|
||
|
"""
|
||
|
|
||
|
if os.path.isdir(dst):
|
||
|
dst = os.path.join(dst, os.path.basename(src))
|
||
|
|
||
|
link_errno = link(src, dst)
|
||
|
if link_errno == errno.EEXIST:
|
||
|
if os.stat(src).st_ino == os.stat(dst).st_ino:
|
||
|
# dst is already a hard link to the correct file, so we don't need
|
||
|
# to do anything else. If we try to link and rename the file
|
||
|
# anyway, we get duplicate files - see http://bugs.python.org/issue21876
|
||
|
return
|
||
|
|
||
|
new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
|
||
|
try:
|
||
|
link_or_copy(src, new_dst)
|
||
|
except:
|
||
|
try:
|
||
|
os.remove(new_dst)
|
||
|
except OSError:
|
||
|
pass
|
||
|
raise
|
||
|
os.rename(new_dst, dst)
|
||
|
elif link_errno != 0:
|
||
|
# Either link isn't supported, or the filesystem doesn't support
|
||
|
# linking, or 'src' and 'dst' are on different filesystems.
|
||
|
shutil.copy(src, dst)
|
||
|
|
||
|
|
||
|
def ensure_dir_exists(path, mode=0o755):
|
||
|
"""ensure that a directory exists
|
||
|
|
||
|
If it doesn't exist, try to create it and protect against a race condition
|
||
|
if another process is doing the same.
|
||
|
|
||
|
The default permissions are 755, which differ from os.makedirs default of 777.
|
||
|
"""
|
||
|
if not os.path.exists(path):
|
||
|
try:
|
||
|
os.makedirs(path, mode=mode)
|
||
|
except OSError as e:
|
||
|
if e.errno != errno.EEXIST:
|
||
|
raise
|
||
|
elif not os.path.isdir(path):
|
||
|
raise IOError("%r exists but is not a directory" % path)
|