Xorn
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Pages
fileutils.py
Go to the documentation of this file.
1 # Copyright (C) 1998-2010 Ales Hvezda
2 # Copyright (C) 1998-2010 gEDA Contributors (see ChangeLog for details)
3 # Copyright (C) 2013-2016 Roland Lutz
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 
19 ## \namespace xorn.fileutils
20 ## Writing files in a safe way.
21 
22 import errno, os, stat, tempfile
23 from gettext import gettext as _
24 
25 ## How many symlinks to follow before returning \c ELOOP.
26 #
27 # A more conventional value for this is \c 20 or even \c 8, but gEDA
28 # uses \c 256, so we're using that, too.
29 
30 MAXSYMLINKS = 256
31 
32 ## Follow a series of symbolic links and return the destination path.
33 #
34 # This function calls \c readlink(2) recursively until some place that
35 # doesn't contain a symlink is found---either some other kind of file,
36 # or a non-existing file if the last symlink was broken. There may
37 # still be symlinks in the directory components of the returned path.
38 
39 def follow_symlinks(filename):
40  orig_filename = filename
41  link_count = 0
42 
43  while link_count < MAXSYMLINKS:
44  try:
45  linkname = os.readlink(filename)
46  except OSError as e:
47  if e.errno != errno.EINVAL and e.errno != errno.ENOENT:
48  raise
49 
50  # Found something that isn't a symlink.
51  return filename
52 
53  link_count += 1
54 
55  # If the linkname is not an absolute path name, append it to
56  # the directory name of the followed filename.
57 
58  # os.path.join throws the first component away if the second
59  # component is an absolute path.
60 
61  filename = os.path.join(os.path.dirname(filename), linkname)
62 
63  # Note that os.path.normpath and os.path.abspath are buggy in
64  # the face of symlinks, for example they will happily collapse
65  # /etc/foo/../bar into /etc/bar, even though /etc/foo might be
66  # a link to /usr/lib/foo.
67  #
68  # The only safe way to collapse ".." elements is to resolve
69  # symlinks (but we aren't going to do this here).
70 
71  raise OSError(errno.ELOOP, os.strerror(errno.ELOOP), orig_filename)
72 
73 ## Return the process's file mode creation mask.
74 
75 def umask():
76  # POSIX.1 requires that the umask is a process-wide attribute.
77  # I don't see a way to read the umask without a potential race
78  # condition. To avoid at least the resulting security risk, I'll
79  # set the umask temporarily to the most restrictive value.
80  saved = os.umask(0777)
81  os.umask(saved)
82  return saved
83 
84 ## Write some data to a file in a reasonably safe way.
85 #
86 # Calls \a write_func to write some data to the file named \a
87 # filename. The data is first written to a temporary file which is
88 # then renamed to the final name. If \a write_func raises an
89 # exception, the temporary file is removed, and the original file is
90 # left untouched.
91 #
92 # If \a backup is \c True (the default), an existing regular file
93 # called \a filename will be backed up after the data has successfully
94 # been written, replacing an existing backup file. Otherwise, it will
95 # be overwritten. An exception to this is if the user doesn't have
96 # write permission to the existing file and \a overwrite is \c False.
97 # (This solves gEDA bug #698565.) If \a filename exists but is not a
98 # regular file, an exception is raised.
99 #
100 # If \a filename is a symbolic link, the (ultimately) linked file is
101 # replaced, not the link. The backup is created in the same directory
102 # as the linked file.
103 #
104 # Hard links to \a filename will break. Also, since the file is
105 # recreated, existing access control lists, metadata etc. may be lost.
106 # The function tries to preserve owner, group and attributes but
107 # doen't treat failure to do so as an error.
108 #
109 # \param [in] filename The name of a file to write to.
110 # \param [in] write_func A function that takes a file-like object as
111 # an argument and writes the data to it.
112 # \param [in] overwrite Overwrite the file even if the user doen't
113 # have write permission.
114 # \param [in] backup Back up the original file. Set this to \c
115 # False if the file was already saved in this
116 # invocation of the program.
117 # \param [in] fsync Sync the temporary file to ensure the data is
118 # on disk when it is renamed over the
119 # destination. This may cause significant lag;
120 # but otherwise, if the system crashes, both
121 # the new and the old file may be lost on some
122 # filesystems (i.e. those that don't guarantee
123 # the data is written to the disk before the
124 # metadata).
125 #
126 # \return \c None.
127 #
128 # \throw IOError, OSError if a filesystem error occurred
129 
130 def write(filename, write_func, overwrite = False,
131  backup = True,
132  fsync = True):
133  # Get the real filename
134  filename = follow_symlinks(filename)
135 
136  try:
137  st = os.stat(filename)
138  except OSError as e:
139  if e.errno != errno.ENOENT:
140  raise
141  st = None
142  else:
143  # Don't overwrite a read-only destination file (gEDA bug #698565)
144  if not overwrite and not os.access(filename, os.W_OK):
145  raise IOError(errno.EACCES, os.strerror(errno.EACCES), filename)
146 
147  if not stat.S_ISREG(st.st_mode):
148  raise Exception, \
149  'Refusing to overwrite non-regular file: ' + filename
150 
151  # Get the directory in which the real filename lives
152  dirname, basename = os.path.split(filename)
153  if not dirname:
154  dirname = '.'
155 
156  # If there is not an existing file with that name, compute the
157  # permissions and uid/gid that we will use for the newly-created file.
158 
159  if st is None:
160  # Use default permissions
161  mode = 0666 & ~umask()
162  uid = os.getuid()
163 
164  dir_st = os.stat(dirname)
165  if dir_st.st_mode & stat.S_ISGID:
166  gid = dir_st.st_gid
167  else:
168  gid = os.getgid()
169  else:
170  mode = st.st_mode
171  uid = st.st_uid
172  gid = st.st_gid
173 
174  f = tempfile.NamedTemporaryFile(
175  dir = dirname, prefix = basename + '.', delete = False)
176 
177  try:
178  try:
179  write_func(f)
180 
181  # We want to sync the newly written file to ensure the data is
182  # on disk when we rename over the destination. Otherwise if
183  # we get a system crash we can lose both the new and the old
184  # file on some filesystems (i.e. those that don't guarantee
185  # the data is written to the disk before the metadata).
186 
187  if fsync:
188  os.fsync(f.fileno())
189  finally:
190  f.close()
191 
192  # Do a backup unless requested otherwise.
193  if backup and st is not None:
194  try:
195  os.rename(filename, filename + '~')
196  except OSError as e:
197  sys.stderr.write(_("Failed to back up '%s': %s\n")
198  % (filename, e.strerror))
199 
200  os.rename(f.name, filename)
201  except:
202  os.unlink(f.name)
203  raise
204 
205  # Restore permissions/ownership. We restore both on a best-effort
206  # basis; rather than treating failure as an error, we just log a
207  # warning.
208 
209  try:
210  os.chmod(filename, mode)
211  except OSError as e:
212  sys.stderr.write(_("Failed to restore permissions on '%s': %s\n")
213  % (filename, e.strerror))
214  try:
215  os.chown(filename, uid, gid)
216  except OSError as e:
217  sys.stderr.write(_("Failed to restore ownership on '%s': %s\n")
218  % (filename, e.strerror))
def write
Write some data to a file in a reasonably safe way.
Definition: fileutils.py:132
def follow_symlinks
Follow a series of symbolic links and return the destination path.
Definition: fileutils.py:39
def umask
Return the process's file mode creation mask.
Definition: fileutils.py:75