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