file.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # file.py -- Safe access to git files
  2. # Copyright (C) 2010 Google, Inc.
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; version 2
  7. # of the License or (at your option) a later version of the License.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  17. # MA 02110-1301, USA.
  18. """Safe access to git files."""
  19. import errno
  20. import os
  21. import tempfile
  22. def ensure_dir_exists(dirname):
  23. """Ensure a directory exists, creating if necessary."""
  24. try:
  25. os.makedirs(dirname)
  26. except OSError as e:
  27. if e.errno != errno.EEXIST:
  28. raise
  29. def fancy_rename(oldname, newname):
  30. """Rename file with temporary backup file to rollback if rename fails"""
  31. if not os.path.exists(newname):
  32. try:
  33. os.rename(oldname, newname)
  34. except OSError as e:
  35. raise
  36. return
  37. # destination file exists
  38. try:
  39. (fd, tmpfile) = tempfile.mkstemp(".tmp", prefix=oldname+".", dir=".")
  40. os.close(fd)
  41. os.remove(tmpfile)
  42. except OSError as e:
  43. # either file could not be created (e.g. permission problem)
  44. # or could not be deleted (e.g. rude virus scanner)
  45. raise
  46. try:
  47. os.rename(newname, tmpfile)
  48. except OSError as e:
  49. raise # no rename occurred
  50. try:
  51. os.rename(oldname, newname)
  52. except OSError as e:
  53. os.rename(tmpfile, newname)
  54. raise
  55. os.remove(tmpfile)
  56. def GitFile(filename, mode='rb', bufsize=-1):
  57. """Create a file object that obeys the git file locking protocol.
  58. :return: a builtin file object or a _GitFile object
  59. :note: See _GitFile for a description of the file locking protocol.
  60. Only read-only and write-only (binary) modes are supported; r+, w+, and a
  61. are not. To read and write from the same file, you can take advantage of
  62. the fact that opening a file for write does not actually open the file you
  63. request.
  64. """
  65. if 'a' in mode:
  66. raise IOError('append mode not supported for Git files')
  67. if '+' in mode:
  68. raise IOError('read/write mode not supported for Git files')
  69. if 'b' not in mode:
  70. raise IOError('text mode not supported for Git files')
  71. if 'w' in mode:
  72. return _GitFile(filename, mode, bufsize)
  73. else:
  74. return file(filename, mode, bufsize)
  75. class _GitFile(object):
  76. """File that follows the git locking protocol for writes.
  77. All writes to a file foo will be written into foo.lock in the same
  78. directory, and the lockfile will be renamed to overwrite the original file
  79. on close.
  80. :note: You *must* call close() or abort() on a _GitFile for the lock to be
  81. released. Typically this will happen in a finally block.
  82. """
  83. PROXY_PROPERTIES = set(['closed', 'encoding', 'errors', 'mode', 'name',
  84. 'newlines', 'softspace'])
  85. PROXY_METHODS = ('__iter__', 'flush', 'fileno', 'isatty', 'next', 'read',
  86. 'readline', 'readlines', 'xreadlines', 'seek', 'tell',
  87. 'truncate', 'write', 'writelines')
  88. def __init__(self, filename, mode, bufsize):
  89. self._filename = filename
  90. self._lockfilename = '%s.lock' % self._filename
  91. fd = os.open(self._lockfilename,
  92. os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0))
  93. self._file = os.fdopen(fd, mode, bufsize)
  94. self._closed = False
  95. for method in self.PROXY_METHODS:
  96. setattr(self, method, getattr(self._file, method))
  97. def abort(self):
  98. """Close and discard the lockfile without overwriting the target.
  99. If the file is already closed, this is a no-op.
  100. """
  101. if self._closed:
  102. return
  103. self._file.close()
  104. try:
  105. os.remove(self._lockfilename)
  106. self._closed = True
  107. except OSError as e:
  108. # The file may have been removed already, which is ok.
  109. if e.errno != errno.ENOENT:
  110. raise
  111. self._closed = True
  112. def close(self):
  113. """Close this file, saving the lockfile over the original.
  114. :note: If this method fails, it will attempt to delete the lockfile.
  115. However, it is not guaranteed to do so (e.g. if a filesystem becomes
  116. suddenly read-only), which will prevent future writes to this file
  117. until the lockfile is removed manually.
  118. :raises OSError: if the original file could not be overwritten. The lock
  119. file is still closed, so further attempts to write to the same file
  120. object will raise ValueError.
  121. """
  122. if self._closed:
  123. return
  124. self._file.close()
  125. try:
  126. try:
  127. os.rename(self._lockfilename, self._filename)
  128. except OSError as e:
  129. # Windows versions prior to Vista don't support atomic renames
  130. if e.errno != errno.EEXIST:
  131. raise
  132. fancy_rename(self._lockfilename, self._filename)
  133. finally:
  134. self.abort()
  135. def __getattr__(self, name):
  136. """Proxy property calls to the underlying file."""
  137. if name in self.PROXY_PROPERTIES:
  138. return getattr(self._file, name)
  139. raise AttributeError(name)