[go: up one dir, main page]

File: duplicity

package info (click to toggle)
duplicity 0.4.2-10.1
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 504 kB
  • ctags: 868
  • sloc: python: 5,382; ansic: 367; sh: 112; makefile: 57
file content (373 lines) | stat: -rw-r--r-- 14,192 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!/usr/bin/env python
# duplicity -- Encrypted bandwidth efficient backup
# Version 0.4.2 released September 29, 2002
#
# Copyright (C) 2002 Ben Escoto <bescoto@stanford.edu>
#
# This file is part of duplicity.
#
# Duplicity is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# Duplicity is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# See http://www.nongnu.org/duplicity for more information.
# Please send mail to me or the mailing list if you find bugs or have
# any suggestions.

from __future__ import generators
import getpass, gzip, os, sys, time, types
from duplicity import collections, commandline, diffdir, dup_temp, \
	 dup_time, file_naming, globals, gpg, log, manifest, patchdir, \
	 path, robust

# If exit_val is not None, exit with given value at end.
exit_val = None


def get_passphrase():
	"""Get passphrase from environment or, failing that, from user"""
	try: return os.environ['PASSPHRASE']
	except KeyError: pass
	if not globals.encryption: return "" # assume we don't need passphrase

	log.Log("PASSPHRASE variable not set, asking user.", 5)
	while 1:
		pass1 = getpass.getpass("GnuPG passphrase: ")
		pass2 = getpass.getpass("Retype to confirm: ")
		if pass1 == pass2: return pass1
		print "First and second passphrases do not match!  Please try again."

def write_multivol(backup_type, tarblock_iter, backend):
	"""Encrypt volumes of tarblock_iter and write to backend

	backup_type should be "inc" or "full" and only matters here when
	picking the filenames.  The path_prefix will determine the names
	of the files written to backend.  Also writes manifest file.
	Returns number of bytes written.

	"""
	def get_indicies(tarblock_iter):
		"""Return start_index and end_index of previous volume"""
		start_index = tarblock_iter.recall_index()
		if start_index is None: start_index = ()
		end_index = tarblock_iter.get_previous_index()
		if end_index is None: end_index = start_index
		return start_index, end_index

	mf = manifest.Manifest().set_dirinfo()
	vol_num = 1; bytes_written = 0; at_end = 0
	while not at_end:
		# Create volume
		tarblock_iter.remember_next_index() # keep track of start index
		dest_filename = file_naming.get(backup_type, vol_num,
										encrypted = globals.encryption,
										gzipped = not globals.encryption)
		tdp = dup_temp.new_tempduppath(file_naming.parse(dest_filename))
		if globals.encryption:
			at_end = gpg.GPGWriteFile(tarblock_iter, tdp.name,
									  globals.gpg_profile)
		else: at_end = gpg.GzipWriteFile(tarblock_iter, tdp.name)
		tdp.setdata()

		# Add volume information to manifest
		vi = manifest.VolumeInfo()
		start_index, end_index = get_indicies(tarblock_iter)
		vi.set_info(vol_num, start_index, end_index)
		vi.set_hash("SHA1", gpg.get_hash("SHA1", tdp))
		mf.add_volume_info(vi)

		backend.put(tdp, dest_filename)
		vol_num += 1; bytes_written += tdp.getsize()
		tdp.delete()

	bytes_written += write_manifest(mf, backup_type, backend)
	return bytes_written

def write_manifest(mf, backup_type, backend):
	"""Write manifest to file in archive_dir and encrypted to backend

	Returns number of bytes written

	"""
	mf_string = mf.to_string()
	if globals.archive_dir:
		local_mf_name = file_naming.get(backup_type, manifest = 1)
		fin = dup_temp.get_fileobj_duppath(globals.archive_dir, local_mf_name)
		fin.write(mf_string)
		fin.close()

	sizelist = [] # will hold length of file in bytes
	remote_mf_name = file_naming.get(backup_type, manifest = 1,
									 encrypted = globals.encryption)
	remote_fin = backend.get_fileobj_write(remote_mf_name, sizelist = sizelist)
	remote_fin.write(mf_string)
	remote_fin.close()
	return sizelist[0]

def get_sig_fileobj(sig_type):
	"""Return a fileobj opened for writing, save results as signature

	If globals.archive_dir is available, save signatures there
	gzipped.  Otherwise save them on the backend encrypted.

	"""
	assert sig_type == "full-sig" or sig_type == "new-sig"

	sig_filename = file_naming.get(sig_type, encrypted = globals.encryption,
								   gzipped = not globals.encryption)
	fh = globals.backend.get_fileobj_write(sig_filename)

	# by MDR.  This was changed to use addfilehandle so we get both
	# remote and local sig files
	if globals.archive_dir:
		local_sig_filename = file_naming.get(sig_type, gzipped = 1)
		fh.addfilehandle(dup_temp.get_fileobj_duppath(globals.archive_dir,
													  local_sig_filename))
	return fh

def full_backup(col_stats):
	"""Do full backup of directory to backend, using archive_dir"""
	sig_outfp = get_sig_fileobj("full-sig")
	tarblock_iter = diffdir.DirFull_WriteSig(globals.select, sig_outfp)
	bytes_written = write_multivol("full", tarblock_iter, globals.backend)
	sig_outfp.close()
	col_stats.set_values(sig_chain_warning = None).cleanup_signatures()
	print_statistics(diffdir.stats, bytes_written)
	
def check_sig_chain(col_stats):
	"""Get last signature chain for inc backup, or None if none available"""
	if not col_stats.matched_chain_pair:
		if globals.incremental:
			log.FatalError(
"""Fatal Error: Unable to start incremental backup.  Old signatures
not found and --incremental specified""")
		else: log.Warn("No signatures found, switching to full backup.")
		return None
	return col_stats.matched_chain_pair[0]

def print_statistics(stats, bytes_written):
	"""If globals.print_statistics, print stats after adding bytes_written"""
	if globals.print_statistics:
		diffdir.stats.TotalDestinationSizeChange = bytes_written
		print diffdir.stats.get_stats_logstring("Backup Statistics")	

def incremental_backup(sig_chain):
	"""Do incremental backup of directory to backend, using archive_dir"""
	dup_time.setprevtime(sig_chain.end_time)
	new_sig_outfp = get_sig_fileobj("new-sig")
	tarblock_iter = diffdir.DirDelta_WriteSig(globals.select,
							  sig_chain.get_fileobjs(), new_sig_outfp)
	bytes_written = write_multivol("inc", tarblock_iter, globals.backend)
	new_sig_outfp.close()
	print_statistics(diffdir.stats, bytes_written)


def list_current(col_stats):
	"""List the files current in the archive (examining signature only)"""
	sig_chain = check_sig_chain(col_stats)
	if not sig_chain:
		log.FatalError("No signature data found, unable to list files.")
	path_iter = diffdir.get_combined_path_iter(sig_chain.get_fileobjs())
	for path in path_iter:
		if path.difftype != "deleted":
			print dup_time.timetopretty(path.getmtime()), \
				  path.get_relative_path()


def restore(col_stats):
	"""Restore archive in globals.backend to globals.local_path"""
	if not patchdir.Write_ROPaths(globals.local_path, 
								  restore_get_patched_rop_iter(col_stats)):
		if globals.restore_dir:
			log.FatalError("%s not found in archive, no files restored."
						   % (globals.restore_dir,))
		else: log.FatalError("No files found in archive - nothing restored.")

def restore_get_patched_rop_iter(col_stats):
	"""Return iterator of patched ROPaths of desired restore data"""
	if globals.restore_dir: index = tuple(globals.restore_dir.split("/"))
	else: index = ()
	time = globals.restore_time or dup_time.curtime
	backup_chain = col_stats.get_backup_chain_at_time(time)
	assert backup_chain, col_stats.all_backup_chains
	backup_setlist = backup_chain.get_sets_at_time(time)

	def get_fileobj_iter(backup_set):
		"""Get file object iterator from backup_set contain given index"""
		manifest = backup_set.get_manifest()
		for vol_num in manifest.get_containing_volumes(index):
			yield restore_get_enc_fileobj(backup_set.backend,
										  backup_set.volume_name_dict[vol_num],
										  manifest.volume_info_dict[vol_num])

	fileobj_iters = map(get_fileobj_iter, backup_setlist)
	tarfiles = map(patchdir.TarFile_FromFileobjs, fileobj_iters)
	return patchdir.tarfiles2rop_iter(tarfiles, index)

def restore_get_enc_fileobj(backend, filename, volume_info):
	"""Return plaintext fileobj from encrypted filename on backend

	If volume_info is set, the hash of the file will be checked,
	assuming some hash is available.  Also, if globals.sign_key is
	set, a fatal error will be raised if file not signed by sign_key.

	"""
	parseresults = file_naming.parse(filename)
	tdp = dup_temp.new_tempduppath(parseresults)
	backend.get(filename, tdp)
	restore_check_hash(volume_info, tdp)
	
	fileobj = tdp.filtered_open_with_delete("rb")
	if parseresults.encrypted and globals.gpg_profile.sign_key:
		restore_add_sig_check(fileobj)
	return fileobj

def restore_check_hash(volume_info, vol_path):
	"""Check the hash of vol_path path against data in volume_info"""
	hash_pair = volume_info.get_best_hash()
	if hash_pair:
		calculated_hash = gpg.get_hash(hash_pair[0], vol_path)
		if calculated_hash != hash_pair[1]:
			log.FatalError("Invalid data - %s hash mismatch:\n"
						   "Calculated hash: %s\n"
						   "Manifest hash: %s\n" %
						   (hash_pair[0], calculated_hash, hash_pair[1]))

def restore_add_sig_check(fileobj):
	"""Require signature when closing fileobj matches sig in gpg_profile"""
	assert (isinstance(fileobj, dup_temp.FileobjHooked) and
			isinstance(fileobj.fileobj, gpg.GPGFile)), fileobj
	def check_signature():
		"""Thunk run when closing volume file"""
		actual_sig = fileobj.fileobj.get_signature()
		if actual_sig != globals.gpg_profile.sign_key:
			log.FatalError("Volume was not signed by key %s, not %s" %
						   (actual_sig, globals.gpg_profile.sign_key))
	fileobj.addhook(check_signature)


def verify(col_stats):
	"""Verify files, logging differences"""
	global exit_val
	collated = diffdir.collate2iters(restore_get_patched_rop_iter(col_stats),
									 globals.select)
	diff_count = 0; total_count = 0
	for backup_ropath, current_path in collated:
		if not backup_ropath: backup_ropath = path.ROPath(current_path.index)
		if not current_path: current_path = path.ROPath(backup_ropath.index)
		if not backup_ropath.compare_verbose(current_path): diff_count += 1
		total_count += 1
	log.Log("Verify complete: %s %s compared, %s %s found." %
			(total_count, total_count == 1 and "file" or "files",
			 diff_count, diff_count == 1 and "difference" or "differences"), 3)
	if diff_count >= 1: exit_val = 1


def cleanup(col_stats):
	"""Delete the extraneous files in the current backend"""
	extraneous = col_stats.get_extraneous()
	if not extraneous:
		log.Warn("No extraneous files found, nothing deleted in cleanup.")
		return

	filestr = "\n".join(extraneous)
	if globals.force:
		if len(extraneous) > 1:
			log.Log("Deleting these files from backend:\n"+filestr, 3)
		else: log.Log("Deleting this file from backend:\n"+filestr, 3)
		col_stats.backend.delete(extraneous)
	else:
		if len(extraneous) > 1:
			log.Warn("Found the following files to delete:")
		else: log.Warn("Found the following file to delete:")
		log.Warn(filestr + "\nRun duplicity again with the --force "
				 "option to actually delete.")


def remove_old(col_stats):
	"""Remove backup files older than globals.remove_time from backend"""
	assert globals.remove_time is not None
	def set_times_str(setlist):
		"""Return string listing times of sets in setlist"""
		return "\n".join(map(lambda s: dup_time.timetopretty(s.get_time()),
							 setlist))

	req_list = col_stats.get_older_than_required(globals.remove_time)
	if req_list:
		log.Warn("There are backup set(s) at time(s):\n%s\nWhich can't be "
				 "deleted because newer sets depend on them." %
				 set_times_str(req_list))

	if (col_stats.matched_chain_pair and
		col_stats.matched_chain_pair[1].end_time < globals.remove_time):
		log.Warn("Current active backup chain is older than specified time.\n"
			 "However, it will not be deleted.  To remove all your backups,\n"
				 "manually purge the repository.")

	setlist = col_stats.get_older_than(globals.remove_time)
	if not setlist:
		log.Warn("No old backup sets found, nothing deleted.")
		return
	if globals.force:
		if len(setlist) > 1:
			log.Log("Deleting backup sets at times:\n" +
					set_times_str(setlist), 3)
		else: log.Log("Deleting backup set at times:\n" +
					  set_times_str(setlist), 3)
		setlist.reverse() # save oldest for last
		for set in setlist: set.delete()
		col_stats.set_values(sig_chain_warning = None).cleanup_signatures()
	else:
		if len(setlist) > 1:
			log.Warn("Found old backup sets at the following times:")
		else: log.Warn("Found old backup set at the following time:")
		log.Warn(set_times_str(setlist) + 
				 "\nRerun command with --force option to actually delete.")


def check_last_manifest(col_stats):
	"""Check consistency and hostname/directory of last manifest"""
	if not col_stats.all_backup_chains: return
	last_backup_set = col_stats.all_backup_chains[-1].get_last()
	last_backup_set.check_manifests()

def main():
	"""Start/end here"""
	dup_time.setcurtime()
	action = commandline.ProcessCommandLine(sys.argv[1:])
	col_stats = collections.CollectionsStatus(globals.backend,
											  globals.archive_dir).set_values()
	log.Log("Collection Status\n-----------------\n" + str(col_stats), 8)

	os.umask(077)
	globals.gpg_profile.passphrase = get_passphrase()
	if action == "restore": restore(col_stats)
	elif action == "verify": verify(col_stats)
	elif action == "list-current": list_current(col_stats)
	elif action == "collection-status": print str(col_stats)
	elif action == "cleanup": cleanup(col_stats)
	elif action == "remove-old": remove_old(col_stats)
	else:
		assert action == "inc" or action == "full", action
		check_last_manifest(col_stats)
		if action == "full": full_backup(col_stats)
		else:
			sig_chain = check_sig_chain(col_stats)
			if not sig_chain: full_backup(col_stats)
			else: incremental_backup(sig_chain)
	globals.backend.close()
	dup_temp.cleanup()
	if exit_val is not None: sys.exit(exit_val)


if __name__ == "__main__": main()