Update timestamps in headers of modified files.
[portage.git] / pym / portage / util / movefile.py
1 # Copyright 2010-2011 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3
4 __all__ = ['movefile']
5
6 import errno
7 import os as _os
8 import shutil as _shutil
9 import stat
10
11 import portage
12 from portage import bsd_chflags, _encodings, _os_overrides, _selinux, \
13         _unicode_decode, _unicode_func_wrapper, _unicode_module_wrapper
14 from portage.const import MOVE_BINARY
15 from portage.localization import _
16 from portage.process import spawn
17 from portage.util import writemsg
18
19 def movefile(src, dest, newmtime=None, sstat=None, mysettings=None,
20                 hardlink_candidates=None, encoding=_encodings['fs']):
21         """moves a file from src to dest, preserving all permissions and attributes; mtime will
22         be preserved even when moving across filesystems.  Returns true on success and false on
23         failure.  Move is atomic."""
24         #print "movefile("+str(src)+","+str(dest)+","+str(newmtime)+","+str(sstat)+")"
25
26         if mysettings is None:
27                 mysettings = portage.settings
28
29         selinux_enabled = mysettings.selinux_enabled()
30         if selinux_enabled:
31                 selinux = _unicode_module_wrapper(_selinux, encoding=encoding)
32
33         lchown = _unicode_func_wrapper(portage.data.lchown, encoding=encoding)
34         os = _unicode_module_wrapper(_os,
35                 encoding=encoding, overrides=_os_overrides)
36         shutil = _unicode_module_wrapper(_shutil, encoding=encoding)
37
38         try:
39                 if not sstat:
40                         sstat=os.lstat(src)
41
42         except SystemExit as e:
43                 raise
44         except Exception as e:
45                 print(_("!!! Stating source file failed... movefile()"))
46                 print("!!!",e)
47                 return None
48
49         destexists=1
50         try:
51                 dstat=os.lstat(dest)
52         except (OSError, IOError):
53                 dstat=os.lstat(os.path.dirname(dest))
54                 destexists=0
55
56         if bsd_chflags:
57                 if destexists and dstat.st_flags != 0:
58                         bsd_chflags.lchflags(dest, 0)
59                 # Use normal stat/chflags for the parent since we want to
60                 # follow any symlinks to the real parent directory.
61                 pflags = os.stat(os.path.dirname(dest)).st_flags
62                 if pflags != 0:
63                         bsd_chflags.chflags(os.path.dirname(dest), 0)
64
65         if destexists:
66                 if stat.S_ISLNK(dstat[stat.ST_MODE]):
67                         try:
68                                 os.unlink(dest)
69                                 destexists=0
70                         except SystemExit as e:
71                                 raise
72                         except Exception as e:
73                                 pass
74
75         if stat.S_ISLNK(sstat[stat.ST_MODE]):
76                 try:
77                         target=os.readlink(src)
78                         if mysettings and mysettings["D"]:
79                                 if target.find(mysettings["D"])==0:
80                                         target=target[len(mysettings["D"]):]
81                         if destexists and not stat.S_ISDIR(dstat[stat.ST_MODE]):
82                                 os.unlink(dest)
83                         try:
84                                 if selinux_enabled:
85                                         selinux.symlink(target, dest, src)
86                                 else:
87                                         os.symlink(target, dest)
88                         except OSError as e:
89                                 # Some programs will create symlinks automatically, so we have
90                                 # to tolerate these links being recreated during the merge
91                                 # process. In any case, if the link is pointing at the right
92                                 # place, we're in good shape.
93                                 if e.errno not in (errno.ENOENT, errno.EEXIST) or \
94                                         target != os.readlink(dest):
95                                         raise
96                         lchown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
97                         # utime() only works on the target of a symlink, so it's not
98                         # possible to perserve mtime on symlinks.
99                         return os.lstat(dest)[stat.ST_MTIME]
100                 except SystemExit as e:
101                         raise
102                 except Exception as e:
103                         print(_("!!! failed to properly create symlink:"))
104                         print("!!!",dest,"->",target)
105                         print("!!!",e)
106                         return None
107
108         hardlinked = False
109         # Since identical files might be merged to multiple filesystems,
110         # so os.link() calls might fail for some paths, so try them all.
111         # For atomic replacement, first create the link as a temp file
112         # and them use os.rename() to replace the destination.
113         if hardlink_candidates:
114                 head, tail = os.path.split(dest)
115                 hardlink_tmp = os.path.join(head, ".%s._portage_merge_.%s" % \
116                         (tail, os.getpid()))
117                 try:
118                         os.unlink(hardlink_tmp)
119                 except OSError as e:
120                         if e.errno != errno.ENOENT:
121                                 writemsg(_("!!! Failed to remove hardlink temp file: %s\n") % \
122                                         (hardlink_tmp,), noiselevel=-1)
123                                 writemsg("!!! %s\n" % (e,), noiselevel=-1)
124                                 return None
125                         del e
126                 for hardlink_src in hardlink_candidates:
127                         try:
128                                 os.link(hardlink_src, hardlink_tmp)
129                         except OSError:
130                                 continue
131                         else:
132                                 try:
133                                         os.rename(hardlink_tmp, dest)
134                                 except OSError as e:
135                                         writemsg(_("!!! Failed to rename %s to %s\n") % \
136                                                 (hardlink_tmp, dest), noiselevel=-1)
137                                         writemsg("!!! %s\n" % (e,), noiselevel=-1)
138                                         return None
139                                 hardlinked = True
140                                 break
141
142         renamefailed=1
143         if hardlinked:
144                 renamefailed = False
145         if not hardlinked and (selinux_enabled or sstat.st_dev == dstat.st_dev):
146                 try:
147                         if selinux_enabled:
148                                 selinux.rename(src, dest)
149                         else:
150                                 os.rename(src,dest)
151                         renamefailed=0
152                 except OSError as e:
153                         if e.errno != errno.EXDEV:
154                                 # Some random error.
155                                 print(_("!!! Failed to move %(src)s to %(dest)s") % {"src": src, "dest": dest})
156                                 print("!!!",e)
157                                 return None
158                         # Invalid cross-device-link 'bind' mounted or actually Cross-Device
159         if renamefailed:
160                 didcopy=0
161                 if stat.S_ISREG(sstat[stat.ST_MODE]):
162                         try: # For safety copy then move it over.
163                                 if selinux_enabled:
164                                         selinux.copyfile(src, dest + "#new")
165                                         selinux.rename(dest + "#new", dest)
166                                 else:
167                                         shutil.copyfile(src,dest+"#new")
168                                         os.rename(dest+"#new",dest)
169                                 didcopy=1
170                         except SystemExit as e:
171                                 raise
172                         except Exception as e:
173                                 print(_('!!! copy %(src)s -> %(dest)s failed.') % {"src": src, "dest": dest})
174                                 print("!!!",e)
175                                 return None
176                 else:
177                         #we don't yet handle special, so we need to fall back to /bin/mv
178                         a = spawn([MOVE_BINARY, '-f', src, dest], env=os.environ)
179                         if a != os.EX_OK:
180                                 writemsg(_("!!! Failed to move special file:\n"), noiselevel=-1)
181                                 writemsg(_("!!! '%(src)s' to '%(dest)s'\n") % \
182                                         {"src": _unicode_decode(src, encoding=encoding),
183                                         "dest": _unicode_decode(dest, encoding=encoding)}, noiselevel=-1)
184                                 writemsg("!!! %s\n" % a, noiselevel=-1)
185                                 return None # failure
186                 try:
187                         if didcopy:
188                                 if stat.S_ISLNK(sstat[stat.ST_MODE]):
189                                         lchown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
190                                 else:
191                                         os.chown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
192                                 os.chmod(dest, stat.S_IMODE(sstat[stat.ST_MODE])) # Sticky is reset on chown
193                                 os.unlink(src)
194                 except SystemExit as e:
195                         raise
196                 except Exception as e:
197                         print(_("!!! Failed to chown/chmod/unlink in movefile()"))
198                         print("!!!",dest)
199                         print("!!!",e)
200                         return None
201
202         # Always use stat_obj[stat.ST_MTIME] for the integral timestamp which
203         # is returned, since the stat_obj.st_mtime float attribute rounds *up*
204         # if the nanosecond part of the timestamp is 999999881 ns or greater.
205         try:
206                 if hardlinked:
207                         newmtime = os.stat(dest)[stat.ST_MTIME]
208                 else:
209                         # Note: It is not possible to preserve nanosecond precision
210                         # (supported in POSIX.1-2008 via utimensat) with the IEEE 754
211                         # double precision float which only has a 53 bit significand.
212                         if newmtime is not None:
213                                 os.utime(dest, (newmtime, newmtime))
214                         else:
215                                 newmtime = sstat[stat.ST_MTIME]
216                                 if renamefailed:
217                                         # If rename succeeded then timestamps are automatically
218                                         # preserved with complete precision because the source
219                                         # and destination inode are the same. Otherwise, round
220                                         # down to the nearest whole second since python's float
221                                         # st_mtime cannot be used to preserve the st_mtim.tv_nsec
222                                         # field with complete precision. Note that we have to use
223                                         # stat_obj[stat.ST_MTIME] here because the float
224                                         # stat_obj.st_mtime rounds *up* sometimes.
225                                         os.utime(dest, (newmtime, newmtime))
226         except OSError:
227                 # The utime can fail here with EPERM even though the move succeeded.
228                 # Instead of failing, use stat to return the mtime if possible.
229                 try:
230                         newmtime = os.stat(dest)[stat.ST_MTIME]
231                 except OSError as e:
232                         writemsg(_("!!! Failed to stat in movefile()\n"), noiselevel=-1)
233                         writemsg("!!! %s\n" % dest, noiselevel=-1)
234                         writemsg("!!! %s\n" % str(e), noiselevel=-1)
235                         return None
236
237         if bsd_chflags:
238                 # Restore the flags we saved before moving
239                 if pflags:
240                         bsd_chflags.chflags(os.path.dirname(dest), pflags)
241
242         return newmtime