codekingpro/portable-devtools
114k
1"""distutils.file_util2 3Utility functions for operating on single files.4"""5 6import os7 8from ._log import log9from .errors import DistutilsFileError10 11# for generating verbose output in 'copy_file()'12_copy_action = {None: 'copying', 'hard': 'hard linking', 'sym': 'symbolically linking'}13 14 15def _copy_file_contents(src, dst, buffer_size=16 * 1024): # noqa: C90116 """Copy the file 'src' to 'dst'; both must be filenames. Any error17 opening either file, reading from 'src', or writing to 'dst', raises18 DistutilsFileError. Data is read/written in chunks of 'buffer_size'19 bytes (default 16k). No attempt is made to handle anything apart from20 regular files.21 """22 # Stolen from shutil module in the standard library, but with23 # custom error-handling added.24 fsrc = None25 fdst = None26 try:27 try:28 fsrc = open(src, 'rb')29 except OSError as e:30 raise DistutilsFileError(f"could not open '{src}': {e.strerror}")31 32 if os.path.exists(dst):33 try:34 os.unlink(dst)35 except OSError as e:36 raise DistutilsFileError(f"could not delete '{dst}': {e.strerror}")37 38 try:39 fdst = open(dst, 'wb')40 except OSError as e:41 raise DistutilsFileError(f"could not create '{dst}': {e.strerror}")42 43 while True:44 try:45 buf = fsrc.read(buffer_size)46 except OSError as e:47 raise DistutilsFileError(f"could not read from '{src}': {e.strerror}")48 49 if not buf:50 break51 52 try:53 fdst.write(buf)54 except OSError as e:55 raise DistutilsFileError(f"could not write to '{dst}': {e.strerror}")56 finally:57 if fdst:58 fdst.close()59 if fsrc:60 fsrc.close()61 62 63def copy_file( # noqa: C90164 src,65 dst,66 preserve_mode=True,67 preserve_times=True,68 update=False,69 link=None,70 verbose=True,71):72 """Copy a file 'src' to 'dst'. If 'dst' is a directory, then 'src' is73 copied there with the same name; otherwise, it must be a filename. (If74 the file exists, it will be ruthlessly clobbered.) If 'preserve_mode'75 is true (the default), the file's mode (type and permission bits, or76 whatever is analogous on the current platform) is copied. If77 'preserve_times' is true (the default), the last-modified and78 last-access times are copied as well. If 'update' is true, 'src' will79 only be copied if 'dst' does not exist, or if 'dst' does exist but is80 older than 'src'.81 82 'link' allows you to make hard links (os.link) or symbolic links83 (os.symlink) instead of copying: set it to "hard" or "sym"; if it is84 None (the default), files are copied. Don't set 'link' on systems that85 don't support it: 'copy_file()' doesn't check if hard or symbolic86 linking is available. If hardlink fails, falls back to87 _copy_file_contents().88 89 Under Mac OS, uses the native file copy function in macostools; on90 other systems, uses '_copy_file_contents()' to copy file contents.91 92 Return a tuple (dest_name, copied): 'dest_name' is the actual name of93 the output file, and 'copied' is true if the file was copied.94 """95 # XXX if the destination file already exists, we clobber it if96 # copying, but blow up if linking. Hmmm. And I don't know what97 # macostools.copyfile() does. Should definitely be consistent, and98 # should probably blow up if destination exists and we would be99 # changing it (ie. it's not already a hard/soft link to src OR100 # (not update) and (src newer than dst).101 102 from distutils._modified import newer103 from stat import S_IMODE, ST_ATIME, ST_MODE, ST_MTIME104 105 if not os.path.isfile(src):106 raise DistutilsFileError(107 f"can't copy '{src}': doesn't exist or not a regular file"108 )109 110 if os.path.isdir(dst):111 dir = dst112 dst = os.path.join(dst, os.path.basename(src))113 else:114 dir = os.path.dirname(dst)115 116 if update and not newer(src, dst):117 if verbose >= 1:118 log.debug("not copying %s (output up-to-date)", src)119 return (dst, False)120 121 try:122 action = _copy_action[link]123 except KeyError:124 raise ValueError(f"invalid value '{link}' for 'link' argument")125 126 if verbose >= 1:127 if os.path.basename(dst) == os.path.basename(src):128 log.info("%s %s -> %s", action, src, dir)129 else:130 log.info("%s %s -> %s", action, src, dst)131 132 # If linking (hard or symbolic), use the appropriate system call133 # (Unix only, of course, but that's the caller's responsibility)134 if link == 'hard':135 if not (os.path.exists(dst) and os.path.samefile(src, dst)):136 try:137 os.link(src, dst)138 except OSError:139 # If hard linking fails, fall back on copying file140 # (some special filesystems don't support hard linking141 # even under Unix, see issue #8876).142 pass143 else:144 return (dst, True)145 elif link == 'sym':146 if not (os.path.exists(dst) and os.path.samefile(src, dst)):147 os.symlink(src, dst)148 return (dst, True)149 150 # Otherwise (non-Mac, not linking), copy the file contents and151 # (optionally) copy the times and mode.152 _copy_file_contents(src, dst)153 if preserve_mode or preserve_times:154 st = os.stat(src)155 156 # According to David Ascher <da@ski.org>, utime() should be done157 # before chmod() (at least under NT).158 if preserve_times:159 os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))160 if preserve_mode:161 os.chmod(dst, S_IMODE(st[ST_MODE]))162 163 return (dst, True)164 165 166# XXX I suspect this is Unix-specific -- need porting help!167def move_file(src, dst, verbose=True): # noqa: C901168 """Move a file 'src' to 'dst'. If 'dst' is a directory, the file will169 be moved into it with the same name; otherwise, 'src' is just renamed170 to 'dst'. Return the new full name of the file.171 172 Handles cross-device moves on Unix using 'copy_file()'. What about173 other systems???174 """175 import errno176 from os.path import basename, dirname, exists, isdir, isfile177 178 if verbose >= 1:179 log.info("moving %s -> %s", src, dst)180 181 if not isfile(src):182 raise DistutilsFileError(f"can't move '{src}': not a regular file")183 184 if isdir(dst):185 dst = os.path.join(dst, basename(src))186 elif exists(dst):187 raise DistutilsFileError(188 f"can't move '{src}': destination '{dst}' already exists"189 )190 191 if not isdir(dirname(dst)):192 raise DistutilsFileError(193 f"can't move '{src}': destination '{dst}' not a valid path"194 )195 196 copy_it = False197 try:198 os.rename(src, dst)199 except OSError as e:200 (num, msg) = e.args201 if num == errno.EXDEV:202 copy_it = True203 else:204 raise DistutilsFileError(f"couldn't move '{src}' to '{dst}': {msg}")205 206 if copy_it:207 copy_file(src, dst, verbose=verbose)208 try:209 os.unlink(src)210 except OSError as e:211 (num, msg) = e.args212 try:213 os.unlink(dst)214 except OSError:215 pass216 raise DistutilsFileError(217 f"couldn't move '{src}' to '{dst}' by copy/delete: "218 f"delete '{src}' failed: {msg}"219 )220 return dst221 222 223def write_file(filename, contents):224 """Create a file with the specified name and write 'contents' (a225 sequence of strings without line terminators) to it.226 """227 with open(filename, 'w', encoding='utf-8') as f:228 f.writelines(line + '\n' for line in contents)229 