]> git.decadent.org.uk Git - dak.git/blob - daklib/fstransactions.py
cron.daily should have an own logfile
[dak.git] / daklib / fstransactions.py
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
17 """Transactions for filesystem actions
18 """
19
20 import errno
21 import os
22 import shutil
23
24 class _FilesystemAction(object):
25     @property
26     def temporary_name(self):
27         raise NotImplementedError()
28
29     def check_for_temporary(self):
30         try:
31             if os.path.exists(self.temporary_name):
32                 raise IOError("Temporary file '{0}' already exists.".format(self.temporary_name))
33         except NotImplementedError:
34             pass
35
36 class _FilesystemCopyAction(_FilesystemAction):
37     def __init__(self, source, destination, link=True, symlink=False, mode=None):
38         self.destination = destination
39         self.need_cleanup = False
40
41         dirmode = 0o2755
42         if mode is not None:
43             dirmode = 0o2700 | mode
44             # Allow +x for group and others if they have +r.
45             if dirmode & 0o0040:
46                 dirmode = dirmode | 0o0010
47             if dirmode & 0o0004:
48                 dirmode = dirmode | 0o0001
49
50         self.check_for_temporary()
51         destdir = os.path.dirname(self.destination)
52         if not os.path.exists(destdir):
53             os.makedirs(destdir, dirmode)
54         if symlink:
55             os.symlink(source, self.destination)
56         elif link:
57             try:
58                 os.link(source, self.destination)
59             except OSError:
60                 shutil.copy2(source, self.destination)
61         else:
62             shutil.copy2(source, self.destination)
63
64         self.need_cleanup = True
65         if mode is not None:
66             os.chmod(self.destination, mode)
67
68     @property
69     def temporary_name(self):
70         return self.destination
71
72     def commit(self):
73         pass
74
75     def rollback(self):
76         if self.need_cleanup:
77             os.unlink(self.destination)
78             self.need_cleanup = False
79
80 class _FilesystemUnlinkAction(_FilesystemAction):
81     def __init__(self, path):
82         self.path = path
83         self.need_cleanup = False
84
85         self.check_for_temporary()
86         os.rename(self.path, self.temporary_name)
87         self.need_cleanup = True
88
89     @property
90     def temporary_name(self):
91         return "{0}.dak-rm".format(self.path)
92
93     def commit(self):
94         if self.need_cleanup:
95             os.unlink(self.temporary_name)
96             self.need_cleanup = False
97
98     def rollback(self):
99         if self.need_cleanup:
100             os.rename(self.temporary_name, self.path)
101             self.need_cleanup = False
102
103 class _FilesystemCreateAction(_FilesystemAction):
104     def __init__(self, path):
105         self.path = path
106         self.need_cleanup = True
107
108     @property
109     def temporary_name(self):
110         return self.path
111
112     def commit(self):
113         pass
114
115     def rollback(self):
116         if self.need_cleanup:
117             os.unlink(self.path)
118             self.need_cleanup = False
119
120 class FilesystemTransaction(object):
121     """transactions for filesystem actions"""
122     def __init__(self):
123         self.actions = []
124
125     def copy(self, source, destination, link=False, symlink=False, mode=None):
126         """copy C{source} to C{destination}
127
128         @type  source: str
129         @param source: source file
130
131         @type  destination: str
132         @param destination: destination file
133
134         @type  link: bool
135         @param link: try hardlinking, falling back to copying
136
137         @type  symlink: bool
138         @param symlink: create a symlink instead of copying
139
140         @type  mode: int
141         @param mode: permissions to change C{destination} to
142         """
143         if isinstance(mode, str) or isinstance(mode, unicode):
144             mode = int(mode, 8)
145
146         self.actions.append(_FilesystemCopyAction(source, destination, link=link, symlink=symlink, mode=mode))
147
148     def move(self, source, destination, mode=None):
149         """move C{source} to C{destination}
150
151         @type  source: str
152         @param source: source file
153
154         @type  destination: str
155         @param destination: destination file
156
157         @type  mode: int
158         @param mode: permissions to change C{destination} to
159         """
160         self.copy(source, destination, link=True, mode=mode)
161         self.unlink(source)
162
163     def unlink(self, path):
164         """unlink C{path}
165
166         @type  path: str
167         @param path: file to unlink
168         """
169         self.actions.append(_FilesystemUnlinkAction(path))
170
171     def create(self, path, mode=None):
172         """create C{filename} and return file handle
173
174         @type  filename: str
175         @param filename: file to create
176
177         @type  mode: int
178         @param mode: permissions for the new file
179
180         @return: file handle of the new file
181         """
182         if isinstance(mode, str) or isinstance(mode, unicode):
183             mode = int(mode, 8)
184
185         destdir = os.path.dirname(path)
186         if not os.path.exists(destdir):
187             os.makedirs(destdir, 0o2775)
188         if os.path.exists(path):
189             raise IOError("File '{0}' already exists.".format(path))
190         fh = open(path, 'w')
191         self.actions.append(_FilesystemCreateAction(path))
192         if mode is not None:
193             os.chmod(path, mode)
194         return fh
195
196     def commit(self):
197         """Commit all recorded actions."""
198         try:
199             for action in self.actions:
200                 action.commit()
201         except:
202             self.rollback()
203             raise
204         finally:
205             self.actions = []
206
207     def rollback(self):
208         """Undo all recorded actions."""
209         try:
210             for action in self.actions:
211                 action.rollback()
212         finally:
213             self.actions = []
214
215     def __enter__(self):
216         return self
217
218     def __exit__(self, type, value, traceback):
219         if type is None:
220             self.commit()
221         else:
222             self.rollback()
223         return None