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