| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- """distutils.file_util
- Utility functions for operating on single files.
- """
- import os
- from ._log import log
- from .errors import DistutilsFileError
- # for generating verbose output in 'copy_file()'
- _copy_action = {None: 'copying', 'hard': 'hard linking', 'sym': 'symbolically linking'}
- def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C901
- """Copy the file 'src' to 'dst'; both must be filenames. Any error
- opening either file, reading from 'src', or writing to 'dst', raises
- DistutilsFileError. Data is read/written in chunks of 'buffer_size'
- bytes (default 16k). No attempt is made to handle anything apart from
- regular files.
- """
- # Stolen from shutil module in the standard library, but with
- # custom error-handling added.
- fsrc = None
- fdst = None
- try:
- try:
- fsrc = open(src, 'rb')
- except OSError as e:
- raise DistutilsFileError(f"could not open '{src}': {e.strerror}")
- if os.path.exists(dst):
- try:
- os.unlink(dst)
- except OSError as e:
- raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}")
- try:
- fdst = open(dst, 'wb')
- except OSError as e:
- raise DistutilsFileError(f"could not create '{dst}': {e.strerror}")
- while True:
- try:
- buf = fsrc.read(buffer_size)
- except OSError as e:
- raise DistutilsFileError(f"could not read from '{src}': {e.strerror}")
- if not buf:
- break
- try:
- fdst.write(buf)
- except OSError as e:
- raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}")
- finally:
- if fdst:
- fdst.close()
- if fsrc:
- fsrc.close()
- def copy_file( # noqa: C901
- src,
- dst,
- preserve_mode=True,
- preserve_times=True,
- update=False,
- link=None,
- verbose=True,
- ):
- """Copy a file 'src' to 'dst'. If 'dst' is a directory, then 'src' is
- copied there with the same name; otherwise, it must be a filename. (If
- the file exists, it will be ruthlessly clobbered.) If 'preserve_mode'
- is true (the default), the file's mode (type and permission bits, or
- whatever is analogous on the current platform) is copied. If
- 'preserve_times' is true (the default), the last-modified and
- last-access times are copied as well. If 'update' is true, 'src' will
- only be copied if 'dst' does not exist, or if 'dst' does exist but is
- older than 'src'.
- 'link' allows you to make hard links (os.link) or symbolic links
- (os.symlink) instead of copying: set it to "hard" or "sym"; if it is
- None (the default), files are copied. Don't set 'link' on systems that
- don't support it: 'copy_file()' doesn't check if hard or symbolic
- linking is available. If hardlink fails, falls back to
- _copy_file_contents().
- Under Mac OS, uses the native file copy function in macostools; on
- other systems, uses '_copy_file_contents()' to copy file contents.
- Return a tuple (dest_name, copied): 'dest_name' is the actual name of
- the output file, and 'copied' is true if the file was copied.
- """
- # XXX if the destination file already exists, we clobber it if
- # copying, but blow up if linking. Hmmm. And I don't know what
- # macostools.copyfile() does. Should definitely be consistent, and
- # should probably blow up if destination exists and we would be
- # changing it (ie. it's not already a hard/soft link to src OR
- # (not update) and (src newer than dst).
- from distutils._modified import newer
- from stat import S_IMODE, ST_ATIME, ST_MODE, ST_MTIME
- if not os.path.isfile(src):
- raise DistutilsFileError(
- f"can't copy '{src}': doesn't exist or not a regular file"
- )
- if os.path.isdir(dst):
- dir = dst
- dst = os.path.join(dst, os.path.basename(src))
- else:
- dir = os.path.dirname(dst)
- if update and not newer(src, dst):
- if verbose >= 1:
- log.debug("not copying %s (output up-to-date)", src)
- return (dst, False)
- try:
- action = _copy_action[link]
- except KeyError:
- raise ValueError(f"invalid value '{link}' for 'link' argument")
- if verbose >= 1:
- if os.path.basename(dst) == os.path.basename(src):
- log.info("%s %s -> %s", action, src, dir)
- else:
- log.info("%s %s -> %s", action, src, dst)
- # If linking (hard or symbolic), use the appropriate system call
- # (Unix only, of course, but that's the caller's responsibility)
- if link == 'hard':
- if not (os.path.exists(dst) and os.path.samefile(src, dst)):
- try:
- os.link(src, dst)
- except OSError:
- # If hard linking fails, fall back on copying file
- # (some special filesystems don't support hard linking
- # even under Unix, see issue #8876).
- pass
- else:
- return (dst, True)
- elif link == 'sym':
- if not (os.path.exists(dst) and os.path.samefile(src, dst)):
- os.symlink(src, dst)
- return (dst, True)
- # Otherwise (non-Mac, not linking), copy the file contents and
- # (optionally) copy the times and mode.
- _copy_file_contents(src, dst)
- if preserve_mode or preserve_times:
- st = os.stat(src)
- # According to David Ascher <da@ski.org>, utime() should be done
- # before chmod() (at least under NT).
- if preserve_times:
- os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
- if preserve_mode:
- os.chmod(dst, S_IMODE(st[ST_MODE]))
- return (dst, True)
- # XXX I suspect this is Unix-specific -- need porting help!
- def move_file(src, dst, verbose=True): # noqa: C901
- """Move a file 'src' to 'dst'. If 'dst' is a directory, the file will
- be moved into it with the same name; otherwise, 'src' is just renamed
- to 'dst'. Return the new full name of the file.
- Handles cross-device moves on Unix using 'copy_file()'. What about
- other systems???
- """
- import errno
- from os.path import basename, dirname, exists, isdir, isfile
- if verbose >= 1:
- log.info("moving %s -> %s", src, dst)
- if not isfile(src):
- raise DistutilsFileError(f"can't move '{src}': not a regular file")
- if isdir(dst):
- dst = os.path.join(dst, basename(src))
- elif exists(dst):
- raise DistutilsFileError(
- f"can't move '{src}': destination '{dst}' already exists"
- )
- if not isdir(dirname(dst)):
- raise DistutilsFileError(
- f"can't move '{src}': destination '{dst}' not a valid path"
- )
- copy_it = False
- try:
- os.rename(src, dst)
- except OSError as e:
- (num, msg) = e.args
- if num == errno.EXDEV:
- copy_it = True
- else:
- raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}")
- if copy_it:
- copy_file(src, dst, verbose=verbose)
- try:
- os.unlink(src)
- except OSError as e:
- (num, msg) = e.args
- try:
- os.unlink(dst)
- except OSError:
- pass
- raise DistutilsFileError(
- f"couldn't move '{src}' to '{dst}' by copy/delete: "
- f"delete '{src}' failed: {msg}"
- )
- return dst
- def write_file(filename, contents):
- """Create a file with the specified name and write 'contents' (a
- sequence of strings without line terminators) to it.
- """
- with open(filename, 'w', encoding='utf-8') as f:
- f.writelines(line + '\n' for line in contents)
|