file_util.py 8.07 KB
Newer Older
1 2
"""distutils.file_util

Greg Ward's avatar
Greg Ward committed
3 4
Utility functions for operating on single files.
"""
5

6
# This module should be kept compatible with Python 2.1.
7

8 9 10 11
__revision__ = "$Id$"

import os
from distutils.errors import DistutilsFileError
12
from distutils import log
13 14 15 16 17 18 19 20 21

# 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):
    """Copy the file 'src' to 'dst'; both must be filenames.  Any error
Greg Ward's avatar
Greg Ward committed
22 23 24 25 26
    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.
    """
27 28 29 30 31 32 33 34 35 36 37
    # 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 os.error, (errno, errstr):
            raise DistutilsFileError, \
                  "could not open '%s': %s" % (src, errstr)
Fred Drake's avatar
Fred Drake committed
38

39 40 41 42 43 44
        if os.path.exists(dst):
            try:
                os.unlink(dst)
            except os.error, (errno, errstr):
                raise DistutilsFileError, \
                      "could not delete '%s': %s" % (dst, errstr)
45

46 47 48 49 50
        try:
            fdst = open(dst, 'wb')
        except os.error, (errno, errstr):
            raise DistutilsFileError, \
                  "could not create '%s': %s" % (dst, errstr)
Fred Drake's avatar
Fred Drake committed
51

52 53
        while 1:
            try:
Greg Ward's avatar
Greg Ward committed
54
                buf = fsrc.read(buffer_size)
55 56 57
            except os.error, (errno, errstr):
                raise DistutilsFileError, \
                      "could not read from '%s': %s" % (src, errstr)
Fred Drake's avatar
Fred Drake committed
58

59 60 61 62 63 64 65 66
            if not buf:
                break

            try:
                fdst.write(buf)
            except os.error, (errno, errstr):
                raise DistutilsFileError, \
                      "could not write to '%s': %s" % (dst, errstr)
Fred Drake's avatar
Fred Drake committed
67

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
    finally:
        if fdst:
            fdst.close()
        if fsrc:
            fsrc.close()

# _copy_file_contents()

def copy_file (src, dst,
               preserve_mode=1,
               preserve_times=1,
               update=0,
               link=None,
               verbose=0,
               dry_run=0):

Greg Ward's avatar
Greg Ward committed
84 85 86 87 88 89 90 91
    """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
92
    older than 'src'.
Greg Ward's avatar
Greg Ward committed
93 94 95 96 97 98 99 100 101 102

    '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.

    Under Mac OS, uses the native file copy function in macostools; on
    other systems, uses '_copy_file_contents()' to copy file contents.

103 104 105
    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 (or would
    have been copied, if 'dry_run' true).
Greg Ward's avatar
Greg Ward committed
106
    """
107 108 109 110 111 112 113 114
    # 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.dep_util import newer
115
    from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE
116

Greg Ward's avatar
Greg Ward committed
117
    if not os.path.isfile(src):
118 119 120
        raise DistutilsFileError, \
              "can't copy '%s': doesn't exist or not a regular file" % src

Greg Ward's avatar
Greg Ward committed
121
    if os.path.isdir(dst):
122
        dir = dst
Greg Ward's avatar
Greg Ward committed
123
        dst = os.path.join(dst, os.path.basename(src))
124
    else:
Greg Ward's avatar
Greg Ward committed
125
        dir = os.path.dirname(dst)
126

Greg Ward's avatar
Greg Ward committed
127
    if update and not newer(src, dst):
128 129
        log.debug("not copying %s (output up-to-date)", src)
        return dst, 0
130 131 132 133 134 135

    try:
        action = _copy_action[link]
    except KeyError:
        raise ValueError, \
              "invalid value '%s' for 'link' argument" % link
136 137 138 139
    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)
Fred Drake's avatar
Fred Drake committed
140

141
    if dry_run:
142
        return (dst, 1)
143

144
    # On Mac OS, use the native file copy routine
145 146 147
    if os.name == 'mac':
        import macostools
        try:
Greg Ward's avatar
Greg Ward committed
148
            macostools.copy(src, dst, 0, preserve_times)
149
        except os.error, exc:
150 151
            raise DistutilsFileError, \
                  "could not copy '%s' to '%s': %s" % (src, dst, exc[-1])
Fred Drake's avatar
Fred Drake committed
152

153 154 155
    # If linking (hard or symbolic), use the appropriate system call
    # (Unix only, of course, but that's the caller's responsibility)
    elif link == 'hard':
Greg Ward's avatar
Greg Ward committed
156 157
        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
            os.link(src, dst)
158
    elif link == 'sym':
Greg Ward's avatar
Greg Ward committed
159 160
        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
            os.symlink(src, dst)
161 162 163 164

    # Otherwise (non-Mac, not linking), copy the file contents and
    # (optionally) copy the times and mode.
    else:
Greg Ward's avatar
Greg Ward committed
165
        _copy_file_contents(src, dst)
166
        if preserve_mode or preserve_times:
Greg Ward's avatar
Greg Ward committed
167
            st = os.stat(src)
168 169 170 171

            # According to David Ascher <da@ski.org>, utime() should be done
            # before chmod() (at least under NT).
            if preserve_times:
Greg Ward's avatar
Greg Ward committed
172
                os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
173
            if preserve_mode:
Greg Ward's avatar
Greg Ward committed
174
                os.chmod(dst, S_IMODE(st[ST_MODE]))
175

176
    return (dst, 1)
177 178 179 180 181 182 183 184 185

# copy_file ()


# XXX I suspect this is Unix-specific -- need porting help!
def move_file (src, dst,
               verbose=0,
               dry_run=0):

Greg Ward's avatar
Greg Ward committed
186 187 188
    """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.
189

Greg Ward's avatar
Greg Ward committed
190 191 192
    Handles cross-device moves on Unix using 'copy_file()'.  What about
    other systems???
    """
193
    from os.path import exists, isfile, isdir, basename, dirname
194
    import errno
Fred Drake's avatar
Fred Drake committed
195

196
    log.info("moving %s -> %s", src, dst)
197 198 199 200

    if dry_run:
        return dst

Greg Ward's avatar
Greg Ward committed
201
    if not isfile(src):
202 203 204
        raise DistutilsFileError, \
              "can't move '%s': not a regular file" % src

Greg Ward's avatar
Greg Ward committed
205 206 207
    if isdir(dst):
        dst = os.path.join(dst, basename(src))
    elif exists(dst):
208 209 210 211
        raise DistutilsFileError, \
              "can't move '%s': destination '%s' already exists" % \
              (src, dst)

Greg Ward's avatar
Greg Ward committed
212
    if not isdir(dirname(dst)):
213 214 215 216 217 218
        raise DistutilsFileError, \
              "can't move '%s': destination '%s' not a valid path" % \
              (src, dst)

    copy_it = 0
    try:
Greg Ward's avatar
Greg Ward committed
219
        os.rename(src, dst)
220 221 222 223 224 225 226 227
    except os.error, (num, msg):
        if num == errno.EXDEV:
            copy_it = 1
        else:
            raise DistutilsFileError, \
                  "couldn't move '%s' to '%s': %s" % (src, dst, msg)

    if copy_it:
Greg Ward's avatar
Greg Ward committed
228
        copy_file(src, dst)
229
        try:
Greg Ward's avatar
Greg Ward committed
230
            os.unlink(src)
231 232
        except os.error, (num, msg):
            try:
Greg Ward's avatar
Greg Ward committed
233
                os.unlink(dst)
234 235 236
            except os.error:
                pass
            raise DistutilsFileError, \
Fred Drake's avatar
Fred Drake committed
237
                  ("couldn't move '%s' to '%s' by copy/delete: " +
238 239 240 241 242 243 244 245 246 247
                   "delete '%s' failed: %s") % \
                  (src, dst, src, msg)

    return dst

# move_file ()


def write_file (filename, contents):
    """Create a file with the specified name and write 'contents' (a
Greg Ward's avatar
Greg Ward committed
248 249 250
    sequence of strings without line terminators) to it.
    """
    f = open(filename, "w")
251
    for line in contents:
Greg Ward's avatar
Greg Ward committed
252 253
        f.write(line + "\n")
    f.close()