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. If hardlink fails, falls back to
84    _copy_file_contents().
85
86    Under Mac OS, uses the native file copy function in macostools; on
87    other systems, uses '_copy_file_contents()' to copy file contents.
88
89    Return a tuple (dest_name, copied): 'dest_name' is the actual name of
90    the output file, and 'copied' is true if the file was copied (or would
91    have been copied, if 'dry_run' true).
92    """
93    # XXX if the destination file already exists, we clobber it if
94    # copying, but blow up if linking.  Hmmm.  And I don't know what
95    # macostools.copyfile() does.  Should definitely be consistent, and
96    # should probably blow up if destination exists and we would be
97    # changing it (ie. it's not already a hard/soft link to src OR
98    # (not update) and (src newer than dst).
99
100    from distutils.dep_util import newer
101    from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE
102
103    if not os.path.isfile(src):
104        raise DistutilsFileError(
105              "can't copy '%s': doesn't exist or not a regular file" % src)
106
107    if os.path.isdir(dst):
108        dir = dst
109        dst = os.path.join(dst, os.path.basename(src))
110    else:
111        dir = os.path.dirname(dst)
112
113    if update and not newer(src, dst):
114        if verbose >= 1:
115            log.debug("not copying %s (output up-to-date)", src)
116        return (dst, 0)
117
118    try:
119        action = _copy_action[link]
120    except KeyError:
121        raise ValueError("invalid value '%s' for 'link' argument" % link)
122
123    if verbose >= 1:
124        if os.path.basename(dst) == os.path.basename(src):
125            log.info("%s %s -> %s", action, src, dir)
126        else:
127            log.info("%s %s -> %s", action, src, dst)
128
129    if dry_run:
130        return (dst, 1)
131
132    # If linking (hard or symbolic), use the appropriate system call
133    # (Unix only, of course, but that's the caller's responsibility)
134    elif link == 'hard':
135        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
136            try:
137                os.link(src, dst)
138                return (dst, 1)
139            except OSError:
140                # If hard linking fails, fall back on copying file
141                # (some special filesystems don't support hard linking
142                #  even under Unix, see issue #8876).
143                pass
144    elif link == 'sym':
145        if not (os.path.exists(dst) and os.path.samefile(src, dst)):
146            os.symlink(src, dst)
147            return (dst, 1)
148
149    # Otherwise (non-Mac, not linking), copy the file contents and
150    # (optionally) copy the times and mode.
151    _copy_file_contents(src, dst)
152    if preserve_mode or preserve_times:
153        st = os.stat(src)
154
155        # According to David Ascher <da@ski.org>, utime() should be done
156        # before chmod() (at least under NT).
157        if preserve_times:
158            os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
159        if preserve_mode:
160            os.chmod(dst, S_IMODE(st[ST_MODE]))
161
162    return (dst, 1)
163
164
165# XXX I suspect this is Unix-specific -- need porting help!
166def move_file (src, dst,
167               verbose=1,
168               dry_run=0):
169
170    """Move a file 'src' to 'dst'.  If 'dst' is a directory, the file will
171    be moved into it with the same name; otherwise, 'src' is just renamed
172    to 'dst'.  Return the new full name of the file.
173
174    Handles cross-device moves on Unix using 'copy_file()'.  What about
175    other systems???
176    """
177    from os.path import exists, isfile, isdir, basename, dirname
178    import errno
179
180    if verbose >= 1:
181        log.info("moving %s -> %s", src, dst)
182
183    if dry_run:
184        return dst
185
186    if not isfile(src):
187        raise DistutilsFileError("can't move '%s': not a regular file" % src)
188
189    if isdir(dst):
190        dst = os.path.join(dst, basename(src))
191    elif exists(dst):
192        raise DistutilsFileError(
193              "can't move '%s': destination '%s' already exists" %
194              (src, dst))
195
196    if not isdir(dirname(dst)):
197        raise DistutilsFileError(
198              "can't move '%s': destination '%s' not a valid path" %
199              (src, dst))
200
201    copy_it = False
202    try:
203        os.rename(src, dst)
204    except OSError as e:
205        (num, msg) = e.args
206        if num == errno.EXDEV:
207            copy_it = True
208        else:
209            raise DistutilsFileError(
210                  "couldn't move '%s' to '%s': %s" % (src, dst, msg))
211
212    if copy_it:
213        copy_file(src, dst, verbose=verbose)
214        try:
215            os.unlink(src)
216        except OSError as e:
217            (num, msg) = e.args
218            try:
219                os.unlink(dst)
220            except OSError:
221                pass
222            raise DistutilsFileError(
223                  "couldn't move '%s' to '%s' by copy/delete: "
224                  "delete '%s' failed: %s"
225                  % (src, dst, src, msg))
226    return dst
227
228
229def write_file (filename, contents):
230    """Create a file with the specified name and write 'contents' (a
231    sequence of strings without line terminators) to it.
232    """
233    f = open(filename, "w")
234    try:
235        for line in contents:
236            f.write(line + "\n")
237    finally:
238        f.close()
239