##// END OF EJS Templates
addbackup: handle file in subdirectory...
Pierre-Yves David -
r23315:66275ecc default
parent child Browse files
Show More
@@ -1,500 +1,504 b''
1 1 # transaction.py - simple journaling scheme for mercurial
2 2 #
3 3 # This transaction scheme is intended to gracefully handle program
4 4 # errors and interruptions. More serious failures like system crashes
5 5 # can be recovered with an fsck-like tool. As the whole repository is
6 6 # effectively log-structured, this should amount to simply truncating
7 7 # anything that isn't referenced in the changelog.
8 8 #
9 9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
10 10 #
11 11 # This software may be used and distributed according to the terms of the
12 12 # GNU General Public License version 2 or any later version.
13 13
14 14 from i18n import _
15 import os
15 16 import errno
16 17 import error, util
17 18
18 19 version = 2
19 20
20 21 def active(func):
21 22 def _active(self, *args, **kwds):
22 23 if self.count == 0:
23 24 raise error.Abort(_(
24 25 'cannot use transaction when it is already committed/aborted'))
25 26 return func(self, *args, **kwds)
26 27 return _active
27 28
28 29 def _playback(journal, report, opener, vfsmap, entries, backupentries,
29 30 unlink=True):
30 31 for f, o, _ignore in entries:
31 32 if o or not unlink:
32 33 try:
33 34 fp = opener(f, 'a')
34 35 fp.truncate(o)
35 36 fp.close()
36 37 except IOError:
37 38 report(_("failed to truncate %s\n") % f)
38 39 raise
39 40 else:
40 41 try:
41 42 opener.unlink(f)
42 43 except (IOError, OSError), inst:
43 44 if inst.errno != errno.ENOENT:
44 45 raise
45 46
46 47 backupfiles = []
47 48 for l, f, b, c in backupentries:
48 49 if l not in vfsmap and c:
49 50 report("couldn't handle %s: unknown cache location %s\n"
50 51 % (b, l))
51 52 vfs = vfsmap[l]
52 53 try:
53 54 if f and b:
54 55 filepath = vfs.join(f)
55 56 backuppath = vfs.join(b)
56 57 try:
57 58 util.copyfile(backuppath, filepath)
58 59 backupfiles.append(b)
59 60 except IOError:
60 61 report(_("failed to recover %s\n") % f)
61 62 else:
62 63 target = f or b
63 64 try:
64 65 vfs.unlink(target)
65 66 except (IOError, OSError), inst:
66 67 if inst.errno != errno.ENOENT:
67 68 raise
68 69 except (IOError, OSError, util.Abort), inst:
69 70 if not c:
70 71 raise
71 72
72 73 opener.unlink(journal)
73 74 backuppath = "%s.backupfiles" % journal
74 75 if opener.exists(backuppath):
75 76 opener.unlink(backuppath)
76 77 try:
77 78 for f in backupfiles:
78 79 if opener.exists(f):
79 80 opener.unlink(f)
80 81 except (IOError, OSError, util.Abort), inst:
81 82 # only pure backup file remains, it is sage to ignore any error
82 83 pass
83 84
84 85 class transaction(object):
85 86 def __init__(self, report, opener, vfsmap, journal, after=None,
86 87 createmode=None, onclose=None, onabort=None):
87 88 """Begin a new transaction
88 89
89 90 Begins a new transaction that allows rolling back writes in the event of
90 91 an exception.
91 92
92 93 * `after`: called after the transaction has been committed
93 94 * `createmode`: the mode of the journal file that will be created
94 95 * `onclose`: called as the transaction is closing, but before it is
95 96 closed
96 97 * `onabort`: called as the transaction is aborting, but before any files
97 98 have been truncated
98 99 """
99 100 self.count = 1
100 101 self.usages = 1
101 102 self.report = report
102 103 # a vfs to the store content
103 104 self.opener = opener
104 105 # a map to access file in various {location -> vfs}
105 106 vfsmap = vfsmap.copy()
106 107 vfsmap[''] = opener # set default value
107 108 self._vfsmap = vfsmap
108 109 self.after = after
109 110 self.onclose = onclose
110 111 self.onabort = onabort
111 112 self.entries = []
112 113 self.map = {}
113 114 self.journal = journal
114 115 self._queue = []
115 116 # a dict of arguments to be passed to hooks
116 117 self.hookargs = {}
117 118 self.file = opener.open(self.journal, "w")
118 119
119 120 # a list of ('location', 'path', 'backuppath', cache) entries.
120 121 # - if 'backuppath' is empty, no file existed at backup time
121 122 # - if 'path' is empty, this is a temporary transaction file
122 123 # - if 'location' is not empty, the path is outside main opener reach.
123 124 # use 'location' value as a key in a vfsmap to find the right 'vfs'
124 125 # (cache is currently unused)
125 126 self._backupentries = []
126 127 self._backupmap = {}
127 128 self._backupjournal = "%s.backupfiles" % journal
128 129 self._backupsfile = opener.open(self._backupjournal, 'w')
129 130 self._backupsfile.write('%d\n' % version)
130 131
131 132 if createmode is not None:
132 133 opener.chmod(self.journal, createmode & 0666)
133 134 opener.chmod(self._backupjournal, createmode & 0666)
134 135
135 136 # hold file generations to be performed on commit
136 137 self._filegenerators = {}
137 138 # hold callbalk to write pending data for hooks
138 139 self._pendingcallback = {}
139 140 # True is any pending data have been written ever
140 141 self._anypending = False
141 142 # holds callback to call when writing the transaction
142 143 self._finalizecallback = {}
143 144 # hold callbalk for post transaction close
144 145 self._postclosecallback = {}
145 146
146 147 def __del__(self):
147 148 if self.journal:
148 149 self._abort()
149 150
150 151 @active
151 152 def startgroup(self):
152 153 """delay registration of file entry
153 154
154 155 This is used by strip to delay vision of strip offset. The transaction
155 156 sees either none or all of the strip actions to be done."""
156 157 self._queue.append([])
157 158
158 159 @active
159 160 def endgroup(self):
160 161 """apply delayed registration of file entry.
161 162
162 163 This is used by strip to delay vision of strip offset. The transaction
163 164 sees either none or all of the strip actions to be done."""
164 165 q = self._queue.pop()
165 166 for f, o, data in q:
166 167 self._addentry(f, o, data)
167 168
168 169 @active
169 170 def add(self, file, offset, data=None):
170 171 """record the state of an append-only file before update"""
171 172 if file in self.map or file in self._backupmap:
172 173 return
173 174 if self._queue:
174 175 self._queue[-1].append((file, offset, data))
175 176 return
176 177
177 178 self._addentry(file, offset, data)
178 179
179 180 def _addentry(self, file, offset, data):
180 181 """add a append-only entry to memory and on-disk state"""
181 182 if file in self.map or file in self._backupmap:
182 183 return
183 184 self.entries.append((file, offset, data))
184 185 self.map[file] = len(self.entries) - 1
185 186 # add enough data to the journal to do the truncate
186 187 self.file.write("%s\0%d\n" % (file, offset))
187 188 self.file.flush()
188 189
189 190 @active
190 191 def addbackup(self, file, hardlink=True, vfs=None):
191 192 """Adds a backup of the file to the transaction
192 193
193 194 Calling addbackup() creates a hardlink backup of the specified file
194 195 that is used to recover the file in the event of the transaction
195 196 aborting.
196 197
197 198 * `file`: the file path, relative to .hg/store
198 199 * `hardlink`: use a hardlink to quickly create the backup
199 200 """
200 201 if self._queue:
201 202 msg = 'cannot use transaction.addbackup inside "group"'
202 203 raise RuntimeError(msg)
203 204
204 205 if file in self.map or file in self._backupmap:
205 206 return
206 backupfile = "%s.backup.%s" % (self.journal, file)
207 dirname, filename = os.path.split(file)
208
209 backupfilename = "%s.backup.%s" % (self.journal, filename)
210 backupfile = os.path.join(dirname, backupfilename)
207 211 if vfs is None:
208 212 vfs = self.opener
209 213 if vfs.exists(file):
210 214 filepath = vfs.join(file)
211 215 backuppath = vfs.join(backupfile)
212 216 util.copyfiles(filepath, backuppath, hardlink=hardlink)
213 217 else:
214 218 backupfile = ''
215 219
216 220 self._addbackupentry(('', file, backupfile, False))
217 221
218 222 def _addbackupentry(self, entry):
219 223 """register a new backup entry and write it to disk"""
220 224 self._backupentries.append(entry)
221 225 self._backupmap[file] = len(self._backupentries) - 1
222 226 self._backupsfile.write("%s\0%s\0%s\0%d\n" % entry)
223 227 self._backupsfile.flush()
224 228
225 229 @active
226 230 def registertmp(self, tmpfile):
227 231 """register a temporary transaction file
228 232
229 233 Such file will be delete when the transaction exit (on both failure and
230 234 success).
231 235 """
232 236 self._addbackupentry(('', '', tmpfile, False))
233 237
234 238 @active
235 239 def addfilegenerator(self, genid, filenames, genfunc, order=0, vfs=None):
236 240 """add a function to generates some files at transaction commit
237 241
238 242 The `genfunc` argument is a function capable of generating proper
239 243 content of each entry in the `filename` tuple.
240 244
241 245 At transaction close time, `genfunc` will be called with one file
242 246 object argument per entries in `filenames`.
243 247
244 248 The transaction itself is responsible for the backup, creation and
245 249 final write of such file.
246 250
247 251 The `genid` argument is used to ensure the same set of file is only
248 252 generated once. Call to `addfilegenerator` for a `genid` already
249 253 present will overwrite the old entry.
250 254
251 255 The `order` argument may be used to control the order in which multiple
252 256 generator will be executed.
253 257 """
254 258 # For now, we are unable to do proper backup and restore of custom vfs
255 259 # but for bookmarks that are handled outside this mechanism.
256 260 assert vfs is None or filenames == ('bookmarks',)
257 261 self._filegenerators[genid] = (order, filenames, genfunc, vfs)
258 262
259 263 def _generatefiles(self):
260 264 # write files registered for generation
261 265 for entry in sorted(self._filegenerators.values()):
262 266 order, filenames, genfunc, vfs = entry
263 267 if vfs is None:
264 268 vfs = self.opener
265 269 files = []
266 270 try:
267 271 for name in filenames:
268 272 # Some files are already backed up when creating the
269 273 # localrepo. Until this is properly fixed we disable the
270 274 # backup for them.
271 275 if name not in ('phaseroots', 'bookmarks'):
272 276 self.addbackup(name)
273 277 files.append(vfs(name, 'w', atomictemp=True))
274 278 genfunc(*files)
275 279 finally:
276 280 for f in files:
277 281 f.close()
278 282
279 283 @active
280 284 def find(self, file):
281 285 if file in self.map:
282 286 return self.entries[self.map[file]]
283 287 if file in self._backupmap:
284 288 return self._backupentries[self._backupmap[file]]
285 289 return None
286 290
287 291 @active
288 292 def replace(self, file, offset, data=None):
289 293 '''
290 294 replace can only replace already committed entries
291 295 that are not pending in the queue
292 296 '''
293 297
294 298 if file not in self.map:
295 299 raise KeyError(file)
296 300 index = self.map[file]
297 301 self.entries[index] = (file, offset, data)
298 302 self.file.write("%s\0%d\n" % (file, offset))
299 303 self.file.flush()
300 304
301 305 @active
302 306 def nest(self):
303 307 self.count += 1
304 308 self.usages += 1
305 309 return self
306 310
307 311 def release(self):
308 312 if self.count > 0:
309 313 self.usages -= 1
310 314 # if the transaction scopes are left without being closed, fail
311 315 if self.count > 0 and self.usages == 0:
312 316 self._abort()
313 317
314 318 def running(self):
315 319 return self.count > 0
316 320
317 321 def addpending(self, category, callback):
318 322 """add a callback to be called when the transaction is pending
319 323
320 324 The transaction will be given as callback's first argument.
321 325
322 326 Category is a unique identifier to allow overwriting an old callback
323 327 with a newer callback.
324 328 """
325 329 self._pendingcallback[category] = callback
326 330
327 331 @active
328 332 def writepending(self):
329 333 '''write pending file to temporary version
330 334
331 335 This is used to allow hooks to view a transaction before commit'''
332 336 categories = sorted(self._pendingcallback)
333 337 for cat in categories:
334 338 # remove callback since the data will have been flushed
335 339 any = self._pendingcallback.pop(cat)(self)
336 340 self._anypending = self._anypending or any
337 341 return self._anypending
338 342
339 343 @active
340 344 def addfinalize(self, category, callback):
341 345 """add a callback to be called when the transaction is closed
342 346
343 347 The transaction will be given as callback's first argument.
344 348
345 349 Category is a unique identifier to allow overwriting old callbacks with
346 350 newer callbacks.
347 351 """
348 352 self._finalizecallback[category] = callback
349 353
350 354 @active
351 355 def addpostclose(self, category, callback):
352 356 """add a callback to be called after the transaction is closed
353 357
354 358 The transaction will be given as callback's first argument.
355 359
356 360 Category is a unique identifier to allow overwriting an old callback
357 361 with a newer callback.
358 362 """
359 363 self._postclosecallback[category] = callback
360 364
361 365 @active
362 366 def close(self):
363 367 '''commit the transaction'''
364 368 if self.count == 1:
365 369 self._generatefiles()
366 370 categories = sorted(self._finalizecallback)
367 371 for cat in categories:
368 372 self._finalizecallback[cat](self)
369 373 if self.onclose is not None:
370 374 self.onclose()
371 375
372 376 self.count -= 1
373 377 if self.count != 0:
374 378 return
375 379 self.file.close()
376 380 self._backupsfile.close()
377 381 # cleanup temporary files
378 382 for l, f, b, c in self._backupentries:
379 383 if l not in self._vfsmap and c:
380 384 self.report("couldn't remote %s: unknown cache location %s\n"
381 385 % (b, l))
382 386 continue
383 387 vfs = self._vfsmap[l]
384 388 if not f and b and vfs.exists(b):
385 389 try:
386 390 vfs.unlink(b)
387 391 except (IOError, OSError, util.Abort), inst:
388 392 if not c:
389 393 raise
390 394 # Abort may be raise by read only opener
391 395 self.report("couldn't remote %s: %s\n"
392 396 % (vfs.join(b), inst))
393 397 self.entries = []
394 398 if self.after:
395 399 self.after()
396 400 if self.opener.isfile(self.journal):
397 401 self.opener.unlink(self.journal)
398 402 if self.opener.isfile(self._backupjournal):
399 403 self.opener.unlink(self._backupjournal)
400 404 for _l, _f, b, c in self._backupentries:
401 405 if l not in self._vfsmap and c:
402 406 self.report("couldn't remote %s: unknown cache location"
403 407 "%s\n" % (b, l))
404 408 continue
405 409 vfs = self._vfsmap[l]
406 410 if b and vfs.exists(b):
407 411 try:
408 412 vfs.unlink(b)
409 413 except (IOError, OSError, util.Abort), inst:
410 414 if not c:
411 415 raise
412 416 # Abort may be raise by read only opener
413 417 self.report("couldn't remote %s: %s\n"
414 418 % (vfs.join(b), inst))
415 419 self._backupentries = []
416 420 self.journal = None
417 421 # run post close action
418 422 categories = sorted(self._postclosecallback)
419 423 for cat in categories:
420 424 self._postclosecallback[cat](self)
421 425
422 426 @active
423 427 def abort(self):
424 428 '''abort the transaction (generally called on error, or when the
425 429 transaction is not explicitly committed before going out of
426 430 scope)'''
427 431 self._abort()
428 432
429 433 def _abort(self):
430 434 self.count = 0
431 435 self.usages = 0
432 436 self.file.close()
433 437 self._backupsfile.close()
434 438
435 439 if self.onabort is not None:
436 440 self.onabort()
437 441
438 442 try:
439 443 if not self.entries and not self._backupentries:
440 444 if self.journal:
441 445 self.opener.unlink(self.journal)
442 446 if self._backupjournal:
443 447 self.opener.unlink(self._backupjournal)
444 448 return
445 449
446 450 self.report(_("transaction abort!\n"))
447 451
448 452 try:
449 453 _playback(self.journal, self.report, self.opener, self._vfsmap,
450 454 self.entries, self._backupentries, False)
451 455 self.report(_("rollback completed\n"))
452 456 except Exception:
453 457 self.report(_("rollback failed - please run hg recover\n"))
454 458 finally:
455 459 self.journal = None
456 460
457 461
458 462 def rollback(opener, vfsmap, file, report):
459 463 """Rolls back the transaction contained in the given file
460 464
461 465 Reads the entries in the specified file, and the corresponding
462 466 '*.backupfiles' file, to recover from an incomplete transaction.
463 467
464 468 * `file`: a file containing a list of entries, specifying where
465 469 to truncate each file. The file should contain a list of
466 470 file\0offset pairs, delimited by newlines. The corresponding
467 471 '*.backupfiles' file should contain a list of file\0backupfile
468 472 pairs, delimited by \0.
469 473 """
470 474 entries = []
471 475 backupentries = []
472 476
473 477 fp = opener.open(file)
474 478 lines = fp.readlines()
475 479 fp.close()
476 480 for l in lines:
477 481 try:
478 482 f, o = l.split('\0')
479 483 entries.append((f, int(o), None))
480 484 except ValueError:
481 485 report(_("couldn't read journal entry %r!\n") % l)
482 486
483 487 backupjournal = "%s.backupfiles" % file
484 488 if opener.exists(backupjournal):
485 489 fp = opener.open(backupjournal)
486 490 lines = fp.readlines()
487 491 if lines:
488 492 ver = lines[0][:-1]
489 493 if ver == str(version):
490 494 for line in lines[1:]:
491 495 if line:
492 496 # Shave off the trailing newline
493 497 line = line[:-1]
494 498 l, f, b, c = line.split('\0')
495 499 backupentries.append((l, f, b, bool(c)))
496 500 else:
497 501 report(_("journal was created by a different version of "
498 502 "Mercurial"))
499 503
500 504 _playback(file, report, opener, vfsmap, entries, backupentries)
General Comments 0
You need to be logged in to leave comments. Login now