file_util.py revision 6685883c02b29b66813343e6380133cd49df4300
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. 84 85 Under Mac OS, uses the native file copy function in macostools; on 86 other systems, uses '_copy_file_contents()' to copy file contents. 87 88 Return a tuple (dest_name, copied): 'dest_name' is the actual name of 89 the output file, and 'copied' is true if the file was copied (or would 90 have been copied, if 'dry_run' true). 91 """ 92 # XXX if the destination file already exists, we clobber it if 93 # copying, but blow up if linking. Hmmm. And I don't know what 94 # macostools.copyfile() does. Should definitely be consistent, and 95 # should probably blow up if destination exists and we would be 96 # changing it (ie. it's not already a hard/soft link to src OR 97 # (not update) and (src newer than dst). 98 99 from distutils.dep_util import newer 100 from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE 101 102 if not os.path.isfile(src): 103 raise DistutilsFileError( 104 "can't copy '%s': doesn't exist or not a regular file" % src) 105 106 if os.path.isdir(dst): 107 dir = dst 108 dst = os.path.join(dst, os.path.basename(src)) 109 else: 110 dir = os.path.dirname(dst) 111 112 if update and not newer(src, dst): 113 if verbose >= 1: 114 log.debug("not copying %s (output up-to-date)", src) 115 return (dst, 0) 116 117 try: 118 action = _copy_action[link] 119 except KeyError: 120 raise ValueError("invalid value '%s' for 'link' argument" % link) 121 122 if verbose >= 1: 123 if os.path.basename(dst) == os.path.basename(src): 124 log.info("%s %s -> %s", action, src, dir) 125 else: 126 log.info("%s %s -> %s", action, src, dst) 127 128 if dry_run: 129 return (dst, 1) 130 131 # If linking (hard or symbolic), use the appropriate system call 132 # (Unix only, of course, but that's the caller's responsibility) 133 elif link == 'hard': 134 if not (os.path.exists(dst) and os.path.samefile(src, dst)): 135 os.link(src, dst) 136 elif link == 'sym': 137 if not (os.path.exists(dst) and os.path.samefile(src, dst)): 138 os.symlink(src, dst) 139 140 # Otherwise (non-Mac, not linking), copy the file contents and 141 # (optionally) copy the times and mode. 142 else: 143 _copy_file_contents(src, dst) 144 if preserve_mode or preserve_times: 145 st = os.stat(src) 146 147 # According to David Ascher <da@ski.org>, utime() should be done 148 # before chmod() (at least under NT). 149 if preserve_times: 150 os.utime(dst, (st[ST_ATIME], st[ST_MTIME])) 151 if preserve_mode: 152 os.chmod(dst, S_IMODE(st[ST_MODE])) 153 154 return (dst, 1) 155 156 157# XXX I suspect this is Unix-specific -- need porting help! 158def move_file (src, dst, 159 verbose=1, 160 dry_run=0): 161 162 """Move a file 'src' to 'dst'. If 'dst' is a directory, the file will 163 be moved into it with the same name; otherwise, 'src' is just renamed 164 to 'dst'. Return the new full name of the file. 165 166 Handles cross-device moves on Unix using 'copy_file()'. What about 167 other systems??? 168 """ 169 from os.path import exists, isfile, isdir, basename, dirname 170 import errno 171 172 if verbose >= 1: 173 log.info("moving %s -> %s", src, dst) 174 175 if dry_run: 176 return dst 177 178 if not isfile(src): 179 raise DistutilsFileError("can't move '%s': not a regular file" % src) 180 181 if isdir(dst): 182 dst = os.path.join(dst, basename(src)) 183 elif exists(dst): 184 raise DistutilsFileError( 185 "can't move '%s': destination '%s' already exists" % 186 (src, dst)) 187 188 if not isdir(dirname(dst)): 189 raise DistutilsFileError( 190 "can't move '%s': destination '%s' not a valid path" % 191 (src, dst)) 192 193 copy_it = False 194 try: 195 os.rename(src, dst) 196 except OSError as e: 197 (num, msg) = e.args 198 if num == errno.EXDEV: 199 copy_it = True 200 else: 201 raise DistutilsFileError( 202 "couldn't move '%s' to '%s': %s" % (src, dst, msg)) 203 204 if copy_it: 205 copy_file(src, dst, verbose=verbose) 206 try: 207 os.unlink(src) 208 except OSError as e: 209 (num, msg) = e.args 210 try: 211 os.unlink(dst) 212 except OSError: 213 pass 214 raise DistutilsFileError( 215 "couldn't move '%s' to '%s' by copy/delete: " 216 "delete '%s' failed: %s" 217 % (src, dst, src, msg)) 218 return dst 219 220 221def write_file (filename, contents): 222 """Create a file with the specified name and write 'contents' (a 223 sequence of strings without line terminators) to it. 224 """ 225 f = open(filename, "w") 226 try: 227 for line in contents: 228 f.write(line + "\n") 229 finally: 230 f.close() 231