fstransactions.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # Copyright (C) 2012, Ansgar Burchardt <ansgar@debian.org>
  2. #
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; either version 2 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along
  14. # with this program; if not, write to the Free Software Foundation, Inc.,
  15. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  16. """Transactions for filesystem actions
  17. """
  18. import os
  19. import shutil
  20. class _FilesystemAction(object):
  21. @property
  22. def temporary_name(self):
  23. raise NotImplementedError()
  24. def check_for_temporary(self):
  25. try:
  26. if os.path.exists(self.temporary_name):
  27. raise IOError("Temporary file '{0}' already exists.".format(self.temporary_name))
  28. except NotImplementedError:
  29. pass
  30. class _FilesystemCopyAction(_FilesystemAction):
  31. def __init__(self, source, destination, link=True, symlink=False, mode=None):
  32. self.destination = destination
  33. self.need_cleanup = False
  34. dirmode = 0o2755
  35. if mode is not None:
  36. dirmode = 0o2700 | mode
  37. # Allow +x for group and others if they have +r.
  38. if dirmode & 0o0040:
  39. dirmode = dirmode | 0o0010
  40. if dirmode & 0o0004:
  41. dirmode = dirmode | 0o0001
  42. self.check_for_temporary()
  43. destdir = os.path.dirname(self.destination)
  44. if not os.path.exists(destdir):
  45. os.makedirs(destdir, dirmode)
  46. if symlink:
  47. os.symlink(source, self.destination)
  48. elif link:
  49. try:
  50. os.link(source, self.destination)
  51. except OSError:
  52. shutil.copy2(source, self.destination)
  53. else:
  54. shutil.copy2(source, self.destination)
  55. self.need_cleanup = True
  56. if mode is not None:
  57. os.chmod(self.destination, mode)
  58. @property
  59. def temporary_name(self):
  60. return self.destination
  61. def commit(self):
  62. pass
  63. def rollback(self):
  64. if self.need_cleanup:
  65. os.unlink(self.destination)
  66. self.need_cleanup = False
  67. class _FilesystemUnlinkAction(_FilesystemAction):
  68. def __init__(self, path):
  69. self.path = path
  70. self.need_cleanup = False
  71. self.check_for_temporary()
  72. os.rename(self.path, self.temporary_name)
  73. self.need_cleanup = True
  74. @property
  75. def temporary_name(self):
  76. return "{0}.dak-rm".format(self.path)
  77. def commit(self):
  78. if self.need_cleanup:
  79. os.unlink(self.temporary_name)
  80. self.need_cleanup = False
  81. def rollback(self):
  82. if self.need_cleanup:
  83. os.rename(self.temporary_name, self.path)
  84. self.need_cleanup = False
  85. class _FilesystemCreateAction(_FilesystemAction):
  86. def __init__(self, path):
  87. self.path = path
  88. self.need_cleanup = True
  89. @property
  90. def temporary_name(self):
  91. return self.path
  92. def commit(self):
  93. pass
  94. def rollback(self):
  95. if self.need_cleanup:
  96. os.unlink(self.path)
  97. self.need_cleanup = False
  98. class FilesystemTransaction(object):
  99. """transactions for filesystem actions"""
  100. def __init__(self):
  101. self.actions = []
  102. def copy(self, source, destination, link=False, symlink=False, mode=None):
  103. """copy C{source} to C{destination}
  104. @type source: str
  105. @param source: source file
  106. @type destination: str
  107. @param destination: destination file
  108. @type link: bool
  109. @param link: try hardlinking, falling back to copying
  110. @type symlink: bool
  111. @param symlink: create a symlink instead of copying
  112. @type mode: int
  113. @param mode: permissions to change C{destination} to
  114. """
  115. if isinstance(mode, str) or isinstance(mode, unicode):
  116. mode = int(mode, 8)
  117. self.actions.append(_FilesystemCopyAction(source, destination, link=link, symlink=symlink, mode=mode))
  118. def move(self, source, destination, mode=None):
  119. """move C{source} to C{destination}
  120. @type source: str
  121. @param source: source file
  122. @type destination: str
  123. @param destination: destination file
  124. @type mode: int
  125. @param mode: permissions to change C{destination} to
  126. """
  127. self.copy(source, destination, link=True, mode=mode)
  128. self.unlink(source)
  129. def unlink(self, path):
  130. """unlink C{path}
  131. @type path: str
  132. @param path: file to unlink
  133. """
  134. self.actions.append(_FilesystemUnlinkAction(path))
  135. def create(self, path, mode=None):
  136. """create C{filename} and return file handle
  137. @type filename: str
  138. @param filename: file to create
  139. @type mode: int
  140. @param mode: permissions for the new file
  141. @return: file handle of the new file
  142. """
  143. if isinstance(mode, str) or isinstance(mode, unicode):
  144. mode = int(mode, 8)
  145. destdir = os.path.dirname(path)
  146. if not os.path.exists(destdir):
  147. os.makedirs(destdir, 0o2775)
  148. if os.path.exists(path):
  149. raise IOError("File '{0}' already exists.".format(path))
  150. fh = open(path, 'w')
  151. self.actions.append(_FilesystemCreateAction(path))
  152. if mode is not None:
  153. os.chmod(path, mode)
  154. return fh
  155. def commit(self):
  156. """Commit all recorded actions."""
  157. try:
  158. for action in self.actions:
  159. action.commit()
  160. except:
  161. self.rollback()
  162. raise
  163. finally:
  164. self.actions = []
  165. def rollback(self):
  166. """Undo all recorded actions."""
  167. try:
  168. for action in self.actions:
  169. action.rollback()
  170. finally:
  171. self.actions = []
  172. def __enter__(self):
  173. return self
  174. def __exit__(self, type, value, traceback):
  175. if type is None:
  176. self.commit()
  177. else:
  178. self.rollback()
  179. return None