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