file.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. # file.py -- Safe access to git files
  2. # Copyright (C) 2010 Google, Inc.
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Safe access to git files."""
  21. import errno
  22. import io
  23. import os
  24. import sys
  25. import tempfile
  26. def ensure_dir_exists(dirname):
  27. """Ensure a directory exists, creating if necessary."""
  28. try:
  29. os.makedirs(dirname)
  30. except OSError as e:
  31. if e.errno != errno.EEXIST:
  32. raise
  33. def _fancy_rename(oldname, newname):
  34. """Rename file with temporary backup file to rollback if rename fails"""
  35. if not os.path.exists(newname):
  36. try:
  37. os.rename(oldname, newname)
  38. except OSError:
  39. raise
  40. return
  41. # destination file exists
  42. try:
  43. (fd, tmpfile) = tempfile.mkstemp(".tmp", prefix=oldname, dir=".")
  44. os.close(fd)
  45. os.remove(tmpfile)
  46. except OSError:
  47. # either file could not be created (e.g. permission problem)
  48. # or could not be deleted (e.g. rude virus scanner)
  49. raise
  50. try:
  51. os.rename(newname, tmpfile)
  52. except OSError:
  53. raise # no rename occurred
  54. try:
  55. os.rename(oldname, newname)
  56. except OSError:
  57. os.rename(tmpfile, newname)
  58. raise
  59. os.remove(tmpfile)
  60. def GitFile(filename, mode='rb', bufsize=-1):
  61. """Create a file object that obeys the git file locking protocol.
  62. :return: a builtin file object or a _GitFile object
  63. :note: See _GitFile for a description of the file locking protocol.
  64. Only read-only and write-only (binary) modes are supported; r+, w+, and a
  65. are not. To read and write from the same file, you can take advantage of
  66. the fact that opening a file for write does not actually open the file you
  67. request.
  68. """
  69. if 'a' in mode:
  70. raise IOError('append mode not supported for Git files')
  71. if '+' in mode:
  72. raise IOError('read/write mode not supported for Git files')
  73. if 'b' not in mode:
  74. raise IOError('text mode not supported for Git files')
  75. if 'w' in mode:
  76. return _GitFile(filename, mode, bufsize)
  77. else:
  78. return io.open(filename, mode, bufsize)
  79. class FileLocked(Exception):
  80. """File is already locked."""
  81. def __init__(self, filename, lockfilename):
  82. self.filename = filename
  83. self.lockfilename = lockfilename
  84. super(FileLocked, self).__init__(filename, lockfilename)
  85. class _GitFile(object):
  86. """File that follows the git locking protocol for writes.
  87. All writes to a file foo will be written into foo.lock in the same
  88. directory, and the lockfile will be renamed to overwrite the original file
  89. on close.
  90. :note: You *must* call close() or abort() on a _GitFile for the lock to be
  91. released. Typically this will happen in a finally block.
  92. """
  93. PROXY_PROPERTIES = set(['closed', 'encoding', 'errors', 'mode', 'name',
  94. 'newlines', 'softspace'])
  95. PROXY_METHODS = ('__iter__', 'flush', 'fileno', 'isatty', 'read',
  96. 'readline', 'readlines', 'seek', 'tell',
  97. 'truncate', 'write', 'writelines')
  98. def __init__(self, filename, mode, bufsize):
  99. self._filename = filename
  100. if isinstance(self._filename, bytes):
  101. self._lockfilename = self._filename + b'.lock'
  102. else:
  103. self._lockfilename = self._filename + '.lock'
  104. try:
  105. fd = os.open(
  106. self._lockfilename,
  107. os.O_RDWR | os.O_CREAT | os.O_EXCL |
  108. getattr(os, "O_BINARY", 0))
  109. except OSError as e:
  110. if e.errno == errno.EEXIST:
  111. raise FileLocked(filename, self._lockfilename)
  112. raise
  113. self._file = os.fdopen(fd, mode, bufsize)
  114. self._closed = False
  115. for method in self.PROXY_METHODS:
  116. setattr(self, method, getattr(self._file, method))
  117. def abort(self):
  118. """Close and discard the lockfile without overwriting the target.
  119. If the file is already closed, this is a no-op.
  120. """
  121. if self._closed:
  122. return
  123. self._file.close()
  124. try:
  125. os.remove(self._lockfilename)
  126. self._closed = True
  127. except OSError as e:
  128. # The file may have been removed already, which is ok.
  129. if e.errno != errno.ENOENT:
  130. raise
  131. self._closed = True
  132. def close(self):
  133. """Close this file, saving the lockfile over the original.
  134. :note: If this method fails, it will attempt to delete the lockfile.
  135. However, it is not guaranteed to do so (e.g. if a filesystem
  136. becomes suddenly read-only), which will prevent future writes to
  137. this file until the lockfile is removed manually.
  138. :raises OSError: if the original file could not be overwritten. The
  139. lock file is still closed, so further attempts to write to the same
  140. file object will raise ValueError.
  141. """
  142. if self._closed:
  143. return
  144. os.fsync(self._file.fileno())
  145. self._file.close()
  146. try:
  147. if getattr(os, 'replace', None) is not None:
  148. os.replace(self._lockfilename, self._filename)
  149. else:
  150. if sys.platform != 'win32':
  151. os.rename(self._lockfilename, self._filename)
  152. else:
  153. # Windows versions prior to Vista don't support atomic
  154. # renames
  155. _fancy_rename(self._lockfilename, self._filename)
  156. finally:
  157. self.abort()
  158. def __enter__(self):
  159. return self
  160. def __exit__(self, exc_type, exc_val, exc_tb):
  161. self.close()
  162. def __getattr__(self, name):
  163. """Proxy property calls to the underlying file."""
  164. if name in self.PROXY_PROPERTIES:
  165. return getattr(self._file, name)
  166. raise AttributeError(name)