file_util.py revision 70a74eb2c46e18f77cf75106a7ee07767fa90aaf
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            raise DistutilsFileError("could not open '%s': %s" % (src, e.strerror))
34
35        if os.path.exists(dst):
36            try:
37                os.unlink(dst)
38            except os.error as e:
39                raise DistutilsFileError(
40                      "could not delete '%s': %s" % (dst, e.strerror))
41
42        try:
43            fdst = open(dst, 'wb')
44        except os.error as e:
45            raise DistutilsFileError(
46                  "could not create '%s': %s" % (dst, e.strerror))
47
48        while True:
49            try:
50                buf = fsrc.read(buffer_size)
51            except os.error as e:
52                raise DistutilsFileError(
53                      "could not read from '%s': %s" % (src, e.strerror))
54
55            if not buf:
56                break
57
58            try:
59                fdst.write(buf)
60            except os.error as e:
61                raise DistutilsFileError(
62                      "could not write to '%s': %s" % (dst, e.strerror))
63    finally:
64        if fdst:
65            fdst.close()
66        if fsrc:
67            fsrc.close()
68
69def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0,
70              link=None, verbose=1, dry_run=0):
71    """Copy a file 'src' to 'dst'.  If 'dst' is a directory, then 'src' is
72    copied there with the same name; otherwise, it must be a filename.  (If
73    the file exists, it will be ruthlessly clobbered.)  If 'preserve_mode'
74    is true (the default), the file's mode (type and permission bits, or
75    whatever is analogous on the current platform) is copied.  If
76    'preserve_times' is true (the default), the last-modified and
77    last-access times are copied as well.  If 'update' is true, 'src' will
78    only be copied if 'dst' does not exist, or if 'dst' does exist but is
79    older than 'src'.
80
81    'link' allows you to make hard links (os.link) or symbolic links
82    (os.symlink) instead of copying: set it to "hard" or "sym"; if it is
83    None (the default), files are copied.  Don't set 'link' on systems that
84    don't support it: 'copy_file()' doesn't check if hard or symbolic
85    linking is available.
86
87    Under Mac OS, uses the native file copy function in macostools; on
88    other systems, uses '_copy_file_contents()' to copy file contents.
89
90    Return a tuple (dest_name, copied): 'dest_name' is the actual name of
91    the output file, and 'copied' is true if the file was copied (or would
92    have been copied, if 'dry_run' true).
93    """
94    # XXX if the destination file already exists, we clobber it if
95    # copying, but blow up if linking.  Hmmm.  And I don't know what
96    # macostools.copyfile() does.  Should definitely be consistent, and
97    # should probably blow up if destination exists and we would be
98    # changing it (ie. it's not already a hard/soft link to src OR
99    # (not update) and (src newer than dst).
100
101    from distutils.dep_util import newer
102    from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE
103
104    if not os.path.isfile(src):
105        raise DistutilsFileError(
106              "can't copy '%s': doesn't exist or not a regular file" % src)
107
108    if os.path.isdir(dst):
109        dir = dst
110        dst = os.path.join(dst, os.path.basename(src))
111    else:
112        dir = os.path.dirname(dst)
113
114    if update and not newer(src, dst):
115        if verbose == 1:
116            log.debug("not copying %s (output up-to-date)", src)
117        return (dst, 0)
118
119    try:
120        action = _copy_action[link]
121    except KeyError:
122        raise ValueError("invalid value '%s' for 'link' argument" % link)
123
124    if verbose == 1:
125        if os.path.basename(dst) == os.path.basename(src):
126            log.info("%s %s -> %s", action, src, dir)
127        else:
128            log.info("%s %s -> %s", action, src, dst)
129
130    if dry_run:
131        return (dst, 1)
132
133    # On Mac OS, use the native file copy routine
134    if os.name == 'mac':
135        import macostools
136        try:
137            macostools.copy(src, dst, 0, preserve_times)
138        except os.error as exc:
139            raise DistutilsFileError(
140                  "could not copy '%s' to '%s': %s" % (src, dst, exc.args[-1]))
141
142    # If linking (hard or symbolic), use the appropriate system call
143    # (Unix only, of course, but that's the caller's responsibility)
144    elif link == 'hard':
145        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
146            os.link(src, dst)
147    elif link == 'sym':
148        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
149            os.symlink(src, dst)
150
151    # Otherwise (non-Mac, not linking), copy the file contents and
152    # (optionally) copy the times and mode.
153    else:
154        _copy_file_contents(src, dst)
155        if preserve_mode or preserve_times:
156            st = os.stat(src)
157
158            # According to David Ascher <da@ski.org>, utime() should be done
159            # before chmod() (at least under NT).
160            if preserve_times:
161                os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
162            if preserve_mode:
163                os.chmod(dst, S_IMODE(st[ST_MODE]))
164
165    return (dst, 1)
166
167
168# XXX I suspect this is Unix-specific -- need porting help!
169def move_file (src, dst,
170               verbose=1,
171               dry_run=0):
172
173    """Move a file 'src' to 'dst'.  If 'dst' is a directory, the file will
174    be moved into it with the same name; otherwise, 'src' is just renamed
175    to 'dst'.  Return the new 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 = False
205    try:
206        os.rename(src, dst)
207    except os.error as e:
208        (num, msg) = e
209        if num == errno.EXDEV:
210            copy_it = True
211        else:
212            raise DistutilsFileError(
213                  "couldn't move '%s' to '%s': %s" % (src, dst, msg))
214
215    if copy_it:
216        copy_file(src, dst, verbose=verbose)
217        try:
218            os.unlink(src)
219        except os.error as e:
220            (num, msg) = e
221            try:
222                os.unlink(dst)
223            except os.error:
224                pass
225            raise DistutilsFileError(
226                  "couldn't move '%s' to '%s' by copy/delete: "
227                  "delete '%s' failed: %s"
228                  % (src, dst, src, msg))
229    return dst
230
231
232def write_file (filename, contents):
233    """Create a file with the specified name and write 'contents' (a
234    sequence of strings without line terminators) to it.
235    """
236    f = open(filename, "w")
237    for line in contents:
238        f.write(line + "\n")
239    f.close()
240