1"""distutils.file_util 2 3Utility functions for operating on single files. 4""" 5 6import os 7from distutils.errors import DistutilsFileError 8from distutils import log 9 10# for generating verbose output in 'copy_file()' 11_copy_action = { None: 'copying', 12 'hard': 'hard linking', 13 'sym': 'symbolically linking' } 14 15 16def _copy_file_contents(src, dst, buffer_size=16*1024): 17 """Copy the file 'src' to 'dst'; both must be filenames. Any error 18 opening either file, reading from 'src', or writing to 'dst', raises 19 DistutilsFileError. Data is read/written in chunks of 'buffer_size' 20 bytes (default 16k). No attempt is made to handle anything apart from 21 regular files. 22 """ 23 # Stolen from shutil module in the standard library, but with 24 # custom error-handling added. 25 fsrc = None 26 fdst = None 27 try: 28 try: 29 fsrc = open(src, 'rb') 30 except OSError as e: 31 raise DistutilsFileError("could not open '%s': %s" % (src, e.strerror)) 32 33 if os.path.exists(dst): 34 try: 35 os.unlink(dst) 36 except OSError as e: 37 raise DistutilsFileError( 38 "could not delete '%s': %s" % (dst, e.strerror)) 39 40 try: 41 fdst = open(dst, 'wb') 42 except OSError as e: 43 raise DistutilsFileError( 44 "could not create '%s': %s" % (dst, e.strerror)) 45 46 while True: 47 try: 48 buf = fsrc.read(buffer_size) 49 except OSError as e: 50 raise DistutilsFileError( 51 "could not read from '%s': %s" % (src, e.strerror)) 52 53 if not buf: 54 break 55 56 try: 57 fdst.write(buf) 58 except OSError as e: 59 raise DistutilsFileError( 60 "could not write to '%s': %s" % (dst, e.strerror)) 61 finally: 62 if fdst: 63 fdst.close() 64 if fsrc: 65 fsrc.close() 66 67def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0, 68 link=None, verbose=1, dry_run=0): 69 """Copy a file 'src' to 'dst'. If 'dst' is a directory, then 'src' is 70 copied there with the same name; otherwise, it must be a filename. (If 71 the file exists, it will be ruthlessly clobbered.) If 'preserve_mode' 72 is true (the default), the file's mode (type and permission bits, or 73 whatever is analogous on the current platform) is copied. If 74 'preserve_times' is true (the default), the last-modified and 75 last-access times are copied as well. If 'update' is true, 'src' will 76 only be copied if 'dst' does not exist, or if 'dst' does exist but is 77 older than 'src'. 78 79 'link' allows you to make hard links (os.link) or symbolic links 80 (os.symlink) instead of copying: set it to "hard" or "sym"; if it is 81 None (the default), files are copied. Don't set 'link' on systems that 82 don't support it: 'copy_file()' doesn't check if hard or symbolic 83 linking is available. If hardlink fails, falls back to 84 _copy_file_contents(). 85 86 Under Mac OS, uses the native file copy function in macostools; on 87 other systems, uses '_copy_file_contents()' to copy file contents. 88 89 Return a tuple (dest_name, copied): 'dest_name' is the actual name of 90 the output file, and 'copied' is true if the file was copied (or would 91 have been copied, if 'dry_run' true). 92 """ 93 # XXX if the destination file already exists, we clobber it if 94 # copying, but blow up if linking. Hmmm. And I don't know what 95 # macostools.copyfile() does. Should definitely be consistent, and 96 # should probably blow up if destination exists and we would be 97 # changing it (ie. it's not already a hard/soft link to src OR 98 # (not update) and (src newer than dst). 99 100 from distutils.dep_util import newer 101 from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE 102 103 if not os.path.isfile(src): 104 raise DistutilsFileError( 105 "can't copy '%s': doesn't exist or not a regular file" % src) 106 107 if os.path.isdir(dst): 108 dir = dst 109 dst = os.path.join(dst, os.path.basename(src)) 110 else: 111 dir = os.path.dirname(dst) 112 113 if update and not newer(src, dst): 114 if verbose >= 1: 115 log.debug("not copying %s (output up-to-date)", src) 116 return (dst, 0) 117 118 try: 119 action = _copy_action[link] 120 except KeyError: 121 raise ValueError("invalid value '%s' for 'link' argument" % link) 122 123 if verbose >= 1: 124 if os.path.basename(dst) == os.path.basename(src): 125 log.info("%s %s -> %s", action, src, dir) 126 else: 127 log.info("%s %s -> %s", action, src, dst) 128 129 if dry_run: 130 return (dst, 1) 131 132 # If linking (hard or symbolic), use the appropriate system call 133 # (Unix only, of course, but that's the caller's responsibility) 134 elif link == 'hard': 135 if not (os.path.exists(dst) and os.path.samefile(src, dst)): 136 try: 137 os.link(src, dst) 138 return (dst, 1) 139 except OSError: 140 # If hard linking fails, fall back on copying file 141 # (some special filesystems don't support hard linking 142 # even under Unix, see issue #8876). 143 pass 144 elif link == 'sym': 145 if not (os.path.exists(dst) and os.path.samefile(src, dst)): 146 os.symlink(src, dst) 147 return (dst, 1) 148 149 # Otherwise (non-Mac, not linking), copy the file contents and 150 # (optionally) copy the times and mode. 151 _copy_file_contents(src, dst) 152 if preserve_mode or preserve_times: 153 st = os.stat(src) 154 155 # According to David Ascher <da@ski.org>, utime() should be done 156 # before chmod() (at least under NT). 157 if preserve_times: 158 os.utime(dst, (st[ST_ATIME], st[ST_MTIME])) 159 if preserve_mode: 160 os.chmod(dst, S_IMODE(st[ST_MODE])) 161 162 return (dst, 1) 163 164 165# XXX I suspect this is Unix-specific -- need porting help! 166def move_file (src, dst, 167 verbose=1, 168 dry_run=0): 169 170 """Move a file 'src' to 'dst'. If 'dst' is a directory, the file will 171 be moved into it with the same name; otherwise, 'src' is just renamed 172 to 'dst'. Return the new full name of the file. 173 174 Handles cross-device moves on Unix using 'copy_file()'. What about 175 other systems??? 176 """ 177 from os.path import exists, isfile, isdir, basename, dirname 178 import errno 179 180 if verbose >= 1: 181 log.info("moving %s -> %s", src, dst) 182 183 if dry_run: 184 return dst 185 186 if not isfile(src): 187 raise DistutilsFileError("can't move '%s': not a regular file" % src) 188 189 if isdir(dst): 190 dst = os.path.join(dst, basename(src)) 191 elif exists(dst): 192 raise DistutilsFileError( 193 "can't move '%s': destination '%s' already exists" % 194 (src, dst)) 195 196 if not isdir(dirname(dst)): 197 raise DistutilsFileError( 198 "can't move '%s': destination '%s' not a valid path" % 199 (src, dst)) 200 201 copy_it = False 202 try: 203 os.rename(src, dst) 204 except OSError as e: 205 (num, msg) = e.args 206 if num == errno.EXDEV: 207 copy_it = True 208 else: 209 raise DistutilsFileError( 210 "couldn't move '%s' to '%s': %s" % (src, dst, msg)) 211 212 if copy_it: 213 copy_file(src, dst, verbose=verbose) 214 try: 215 os.unlink(src) 216 except OSError as e: 217 (num, msg) = e.args 218 try: 219 os.unlink(dst) 220 except OSError: 221 pass 222 raise DistutilsFileError( 223 "couldn't move '%s' to '%s' by copy/delete: " 224 "delete '%s' failed: %s" 225 % (src, dst, src, msg)) 226 return dst 227 228 229def write_file (filename, contents): 230 """Create a file with the specified name and write 'contents' (a 231 sequence of strings without line terminators) to it. 232 """ 233 f = open(filename, "w") 234 try: 235 for line in contents: 236 f.write(line + "\n") 237 finally: 238 f.close() 239