summaryrefslogtreecommitdiff
path: root/rdiff-backup/rdiff_backup/longname.py
blob: e8872ce3a1f8fca53a4f5f007f5a6a6507fa68fd (plain)
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
# Copyright 2005 Ben Escoto
#
# This file is part of rdiff-backup.
#
# rdiff-backup is free software; you can redistribute it and/or modify
# 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.
#
# rdiff-backup 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 rdiff-backup; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA

"""Handle long filenames

rdiff-backup sometimes wants to write filenames longer than allowed by
the destination directory.  This can happen in 3 ways:

1)  Because the destination directory has a low maximum length limit.
2)  When the source directory has a filename close to the limit, so
    that its increments would be above the limit.
3)  When quoting is enabled, so that even the mirror filenames are too
    long.

When rdiff-backup would otherwise write a file whose name is too long,
instead it either skips the operation altogether (for non-regular
files), or writes the data to a unique file in the
rdiff-backup-data/long-filename directory.  This file will have an
arbitrary basename, but if it's an increment the suffix will be the
same.  The name will be recorded in the mirror_metadata so we can find
it later.

"""

import types, errno
import log, Globals, restore, rpath, FilenameMapping, regress

long_name_dir = "long_filename_data"
rootrp = None

def get_long_rp(base = None):
	"""Return an rpath in long name directory with given base"""
	global rootrp
	if not rootrp:
		rootrp = Globals.rbdir.append(long_name_dir)
		if not rootrp.lstat(): rootrp.mkdir()
	if base: return rootrp.append(base)
	else: return rootrp


# ------------------------------------------------------------------
# These functions used mainly for backing up

# integer number of next free prefix.  Names will be created from
# integers consecutively like '1', '2', and so on.
free_name_counter = None

# Filename which holds the next available free name in it
counter_filename = "next_free"

def get_next_free():
	"""Return next free filename available in the long filename directory"""
	global free_name_counter
	def scan_next_free():
		"""Return value of free_name_counter by listing long filename dir"""
		log.Log("Setting next free from long filenames dir", 5)
		cur_high = 0
		for filename in get_long_rp().listdir():
			try: i = int(filename.split('.')[0])
			except ValueError: continue
			if i > cur_high: cur_high = i
		return cur_high + 1

	def read_next_free():
		"""Return next int free by reading the next_free file, or None"""
		rp = get_long_rp(counter_filename)
		if not rp.lstat(): return None
		return int(rp.get_data())

	def write_next_free(i):
		"""Write value i into the counter file"""
		rp = get_long_rp(counter_filename)
		if rp.lstat(): rp.delete()
		rp.write_string(str(free_name_counter))
		rp.fsync_with_dir()

	if not free_name_counter: free_name_counter = read_next_free()
	if not free_name_counter: free_name_counter = scan_next_free()
	filename = str(free_name_counter)
	rp = get_long_rp(filename)
	assert not rp.lstat(), "Unexpected file at %s found" % (rp.path,)
	free_name_counter += 1
	write_next_free(free_name_counter)
	return filename


def check_new_index(base, index, make_dirs = 0):
	"""Return new rpath with given index, or None if that is too long

	If make_dir is True, make any parent directories to assure that
	file is really too long, and not just in directories that don't exist.

	"""
	def wrap_call(func, *args):
		try: result = func(*args)
		except EnvironmentError, exc:
			if (errno.errorcode.has_key(exc[0]) and
				errno.errorcode[exc[0]] == 'ENAMETOOLONG'):
				return None
			if (exc[1] == "The filename or extension is too long"):
				return None
			raise
		return result

	def make_parent(rp):
		parent = rp.get_parent_rp()
		if parent.lstat(): return 1
		parent.makedirs()
		return 2

	rp = wrap_call(base.new_index, index)
	if not make_dirs or not rp or rp.lstat(): return rp

	parent_result = wrap_call(make_parent, rp)
	if not parent_result: return None
	elif parent_result == 1: return rp
	else: return wrap_call(base.new_index, index)

def get_mirror_rp(mirror_base, mirror_rorp):
	"""Get the mirror_rp for reading a regular file

	This will just be in the mirror_base, unless rorp has an alt
	mirror name specified.  Use new_rorp, unless it is None or empty,
	and mirror_rorp exists.

	"""
	if mirror_rorp.has_alt_mirror_name():
		return get_long_rp(mirror_rorp.get_alt_mirror_name())
	else:
		rp = check_new_index(mirror_base, mirror_rorp.index)
		if rp: return rp
		else: return mirror_base.new_index_empty(index)

def get_mirror_inc_rps(rorp_pair, mirror_root, inc_root = None):
	"""Get (mirror_rp, inc_rp) pair, possibly making new longname base

	To test inc_rp, pad incbase with 50 random (non-quoted) characters
	and see if that raises an error.

	"""
	if not inc_root: # make fake inc_root if not available
		inc_root = mirror_root.append_path('rdiff-backup-data/increments')

	def mir_triple_old(old_rorp):
		"""Return (mirror_rp, alt_mirror, alt_inc) from old_rorp"""
		if old_rorp.has_alt_mirror_name():
			alt_mirror = old_rorp.get_alt_mirror_name()
			return (get_long_rp(alt_mirror), alt_mirror, None)
		else:
			mirror_rp = mirror_root.new_index(old_rorp.index)
			if old_rorp.has_alt_inc_name():
				return (mirror_rp, None, old_rorp.get_alt_inc_name())
			else: return (mirror_rp, None, None)

	def mir_triple_new(new_rorp):
		"""Return (mirror_rp, alt_mirror, None) from new_rorp"""
		mirror_rp = check_new_index(mirror_root, new_rorp.index)
		if mirror_rp: return (mirror_rp, None, None)
		alt_mirror = get_next_free()
		return (get_long_rp(alt_mirror), alt_mirror, None)

	def update_rorp(new_rorp, alt_mirror, alt_inc):
		"""Update new_rorp with alternate mirror/inc information"""
		if not new_rorp or not new_rorp.lstat(): return
		if alt_mirror: new_rorp.set_alt_mirror_name(alt_mirror)
		elif alt_inc: new_rorp.set_alt_inc_name(alt_inc)

	def find_inc_pair(index, mirror_rp, alt_mirror, alt_inc):
		"""Return (alt_inc, inc_rp) pair"""
		if alt_mirror: return (None, mirror_rp)
		elif alt_inc: return (alt_inc, get_long_rp(alt_inc))
		elif not index: return (None, inc_root)

		trial_inc_index = index[:-1] + (index[-1] + ('a'*50),)
		if check_new_index(inc_root, trial_inc_index, make_dirs = 1):
			return (None, inc_root.new_index(index))
		alt_inc = get_next_free()
		return (alt_inc, get_long_rp(alt_inc))
		
	(new_rorp, old_rorp) = rorp_pair
	if old_rorp and old_rorp.lstat():
		mirror_rp, alt_mirror, alt_inc = mir_triple_old(old_rorp)
		index = old_rorp.index
	else:
		assert new_rorp and new_rorp.lstat(), (old_rorp, new_rorp)
		mirror_rp, alt_mirror, alt_inc = mir_triple_new(new_rorp)
		index = new_rorp.index

	alt_inc, inc_rp = find_inc_pair(index, mirror_rp, alt_mirror, alt_inc)
	update_rorp(new_rorp, alt_mirror, alt_inc)
	return mirror_rp, inc_rp


# ------------------------------------------------------------------
# The following section is for restoring

# This holds a dictionary {incbase: inclist}.  The keys are increment
# bases like '1' or '23', and the values are lists containing the
# associated increments.
restore_inc_cache = None

def set_restore_cache():
	"""Initialize restore_inc_cache based on long filename dir"""
	global restore_inc_cache
	restore_inc_cache = {}
	root_rf = restore.RestoreFile(get_long_rp(), get_long_rp(), [])
	for incbase_rp, inclist in root_rf.yield_inc_complexes(get_long_rp()):
		restore_inc_cache[incbase_rp.index[-1]] = inclist

def get_inclist(inc_base_name):
	if not restore_inc_cache: set_restore_cache()
	try: return restore_inc_cache[inc_base_name]
	except KeyError: return []

def update_rf(rf, rorp, mirror_root):
	"""Return new or updated restorefile based on alt name info in rorp"""
	def update_incs(rf, inc_base):
		"""Swap inclist in rf with those with base inc_base and return"""
		log.Log("Restoring with increment base %s for file %s" %
				(inc_base, rorp.get_indexpath()), 6)		
		rf.inc_rp = get_long_rp(inc_base)
		rf.inc_list = get_inclist(inc_base)
		rf.set_relevant_incs()

	def update_existing_rf(rf, rorp):
		"""Update rf based on rorp, don't make new one"""
		if rorp.has_alt_mirror_name():
			inc_name = rorp.get_alt_mirror_name()
			rf.mirror_rp = get_long_rp(mirror_name)
		elif rorp.has_alt_inc_name(): inc_name = rorp.get_alt_inc_name()
		else: inc_name = None

		if inc_name: update_incs(rf, inc_name)

	def make_new_rf(rorp, mirror_root):
		"""Make a new rf when long name info is available"""
		if rorp.has_alt_mirror_name():
			inc_name = rorp.get_alt_mirror_name()
			mirror_rp = get_long_rp(inc_name)
		else:
			mirror_rp = mirror_root.new_index(rorp.index)
			if rorp.has_alt_inc_name(): inc_name = rorp.get_alt_inc_name()
			else: return restore.RestoreFile(mirror_rp, None, [])

		rf = restore.RestoreFile(mirror_rp, None, [])
		update_incs(rf, inc_name)
		return rf
		
	if not rorp: return rf
	if rf and not rorp.has_alt_mirror_name() and not rorp.has_alt_inc_name():
		return rf # Most common case
	if rf:
		update_existing_rf(rf, rorp)
		return rf
	else: return make_new_rf(rorp, mirror_root)

def update_regressfile(rf, rorp, mirror_root):
	"""Like update_rf except return a regress file object"""
	rf = update_rf(rf, rorp, mirror_root)
	if isinstance(rf, regress.RegressFile): return rf
	return regress.RegressFile(rf.mirror_rp, rf.inc_rp, rf.inc_list)