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