]> git.decadent.org.uk Git - dak.git/blob - daklib/fstransactions.py
add module for filesystem transactions
[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         self.check_for_temporary()
42         destdir = os.path.dirname(self.destination)
43         if not os.path.exists(destdir):
44             os.makedirs(destdir, 0o2775)
45         if symlink:
46             os.symlink(source, self.destination)
47         elif link:
48             try:
49                 os.link(source, self.destination)
50             except OSError:
51                 shutil.copy2(source, self.destination)
52         else:
53             shutil.copy2(source, self.destination)
54
55         self.need_cleanup = True
56         if mode is not None:
57             os.chmod(self.destination, mode)
58
59     @property
60     def temporary_name(self):
61         return self.destination
62
63     def commit(self):
64         pass
65
66     def rollback(self):
67         if self.need_cleanup:
68             os.unlink(self.destination)
69             self.need_cleanup = False
70
71 class _FilesystemUnlinkAction(_FilesystemAction):
72     def __init__(self, path):
73         self.path = path
74         self.need_cleanup = False
75
76         self.check_for_temporary()
77         os.rename(self.path, self.temporary_name)
78         self.need_cleanup = True
79
80     @property
81     def temporary_name(self):
82         return "{0}.dak-rm".format(self.path)
83
84     def commit(self):
85         if self.need_cleanup:
86             os.unlink(self.temporary_name)
87             self.need_cleanup = False
88
89     def rollback(self):
90         if self.need_cleanup:
91             os.rename(self.temporary_name, self.path)
92             self.need_cleanup = False
93
94 class _FilesystemCreateAction(_FilesystemAction):
95     def __init__(self, path):
96         self.path = path
97         self.need_cleanup = True
98
99     @property
100     def temporary_name(self):
101         return self.path
102
103     def commit(self):
104         pass
105
106     def rollback(self):
107         if self.need_cleanup:
108             os.unlink(self.path)
109             self.need_cleanup = False
110
111 class FilesystemTransaction(object):
112     """transactions for filesystem actions"""
113     def __init__(self):
114         self.actions = []
115
116     def copy(self, source, destination, link=True, symlink=False, mode=None):
117         """copy `source` to `destination`
118
119         Args:
120            source (str): source file
121            destination (str): destination file
122
123         Kwargs:
124            link (bool): Try hardlinking, falling back to copying.
125            symlink (bool): Create a symlink instead
126            mode (int): Permissions to change `destination` to.
127         """
128         self.actions.append(_FilesystemCopyAction(source, destination, link=link, symlink=symlink, mode=mode))
129
130     def move(self, source, destination, mode=None):
131         """move `source` to `destination`
132
133         Args:
134            source (str): source file
135            destination (str): destination file
136
137         Kwargs:
138            mode (int): Permissions to change `destination` to.
139         """
140         self.copy(source, destination, link=True, mode=mode)
141         self.unlink(source)
142
143     def unlink(self, path):
144         """unlink `path`
145
146         Args:
147            path (str): file to unlink
148         """
149         self.actions.append(_FilesystemUnlinkAction(path))
150
151     def create(self, path, mode=None):
152         """create `filename` and return file handle
153
154         Args:
155            filename (str): file to create
156
157         Kwargs:
158            mode (int): Permissions for the new file
159
160         Returns:
161            file handle of the new file
162         """
163         destdir = os.path.dirname(path)
164         if not os.path.exists(destdir):
165             os.makedirs(destdir, 0o2775)
166         if os.path.exists(path):
167             raise IOError("File '{0}' already exists.".format(path))
168         fh = open(path, 'w')
169         self.actions.append(_FilesystemCreateAction(path))
170         if mode is not None:
171             os.chmod(path, mode)
172         return fh
173
174     def commit(self):
175         """Commit all recorded actions."""
176         try:
177             for action in self.actions:
178                 action.commit()
179         except:
180             self.rollback()
181             raise
182         finally:
183             self.actions = []
184
185     def rollback(self):
186         """Undo all recorded actions."""
187         try:
188             for action in self.actions:
189                 action.rollback()
190         finally:
191             self.actions = []
192
193     def __enter__(self):
194         return self
195
196     def __exit__(self, type, value, traceback):
197         if type is None:
198             self.commit()
199         else:
200             self.rollback()
201         return None