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