1 | #!/usr/bin/python |
---|
2 | """Recovers a disk image from a log file generated by salvage_data.py. |
---|
3 | """ |
---|
4 | import sys |
---|
5 | |
---|
6 | def coalesce_extents(one, two): |
---|
7 | """ASSUMES the extents overlap.""" |
---|
8 | return (min(one[0], two[0]), max(one[1], two[1])) |
---|
9 | |
---|
10 | def extents_overlap(one, two): |
---|
11 | """returns True if they touch or overlap in some way, False if they don't. |
---|
12 | """ |
---|
13 | return one[0] <= two[0] <= one[1] or \ |
---|
14 | one[0] <= two[1] <= one[1] or \ |
---|
15 | two[0] <= one[0] <= two[1] or \ |
---|
16 | two[0] <= one[1] <= two[1] |
---|
17 | |
---|
18 | class Extents(object): |
---|
19 | """Tracks the extents that have been covered by the log.""" |
---|
20 | def __init__(self): |
---|
21 | self.extents = [] |
---|
22 | |
---|
23 | def add(self, offset, length): |
---|
24 | """Adds an extent at offset of the given length.""" |
---|
25 | self.add_extent((offset, offset+length)) |
---|
26 | |
---|
27 | def add_extent(self, extent): |
---|
28 | """Adds an extent.""" |
---|
29 | start, end = extent |
---|
30 | if start == end: # nothing to add |
---|
31 | return |
---|
32 | |
---|
33 | # find any existing extent that overlaps with the one being added. |
---|
34 | for i in range(len(self.extents)): |
---|
35 | current_extent = self.extents[i] |
---|
36 | if extents_overlap(extent, current_extent): |
---|
37 | self.extents.pop(i) |
---|
38 | new_extent = coalesce_extents(current_extent, extent) |
---|
39 | self.add_extent(new_extent) |
---|
40 | return |
---|
41 | |
---|
42 | # No coalescing needed |
---|
43 | self.extents.append(extent) |
---|
44 | |
---|
45 | def bytes_covered(self): |
---|
46 | """Returns the number of bytes covered by the extents.""" |
---|
47 | return sum([b-a for a, b in self.extents]) |
---|
48 | |
---|
49 | def byte_range(self): |
---|
50 | """Returns a tuple of the starting and ending offsets covered by these |
---|
51 | extents. |
---|
52 | """ |
---|
53 | self.extents.sort() |
---|
54 | return (self.extents[0][0], self.extents[-1][1]) |
---|
55 | |
---|
56 | def __str__(self): |
---|
57 | start, end = self.byte_range() |
---|
58 | covered = self.bytes_covered() |
---|
59 | return ' '.join([repr(e) for e in self.extents]) + \ |
---|
60 | "\n%s of %s bytes covered (%s remain)" % (covered, end-start, |
---|
61 | end-start-covered) |
---|
62 | |
---|
63 | def report_log(good, bad): |
---|
64 | """let the user know what extents have been accounted for""" |
---|
65 | sys.stderr.write("Good extents: %s\nBad extents: %s\n" % (good, bad)) |
---|
66 | |
---|
67 | def write_image_from_log(log, image): |
---|
68 | """Reads from the log file object, and writes the data to the image file |
---|
69 | object. |
---|
70 | """ |
---|
71 | good_extents = Extents() |
---|
72 | bad_extents = Extents() |
---|
73 | try: |
---|
74 | while True: |
---|
75 | meta = log.readline().split() |
---|
76 | if not meta: |
---|
77 | break |
---|
78 | if meta[0] == 'D': # data |
---|
79 | offset = long(meta[1]) |
---|
80 | length = long(meta[2]) |
---|
81 | data = log.read(length) |
---|
82 | if len(data) != length: |
---|
83 | raise Exception("Short line: %s of %s bytes at offset %s" \ |
---|
84 | % (len(data), length, offset)) |
---|
85 | log.read(1) # the extra newline |
---|
86 | |
---|
87 | sys.stderr.write("writing %s bytes at %s\n" % (length, offset)) |
---|
88 | image.seek(offset) |
---|
89 | image.write(data) |
---|
90 | good_extents.add(offset, length) |
---|
91 | elif meta[0] == 'E': |
---|
92 | offset = long(meta[1]) |
---|
93 | if len(meta) > 2: |
---|
94 | length = long(meta[2]) |
---|
95 | else: |
---|
96 | length = 1 |
---|
97 | sys.stderr.write("skipping %s bad bytes at %s\n" % (length, |
---|
98 | offset)) |
---|
99 | bad_extents.add(offset, length) |
---|
100 | else: |
---|
101 | raise Exception("Invalid line: %r" % (meta,)) |
---|
102 | except: |
---|
103 | report_log(good_extents, bad_extents) |
---|
104 | raise |
---|
105 | |
---|
106 | report_log(good_extents, bad_extents) |
---|
107 | return good_extents, bad_extents |
---|
108 | |
---|
109 | def write_log_from_image(image, out, good_extents, bad_extents): |
---|
110 | """Write out a concise log file with the same information as the input |
---|
111 | file. |
---|
112 | """ |
---|
113 | max_extent_size = 10*1024**2 |
---|
114 | |
---|
115 | extents = [(s, e, 'D') for s, e in good_extents.extents] + \ |
---|
116 | [(s, e, 'E') for s, e in bad_extents.extents] |
---|
117 | extents.sort() |
---|
118 | for start, end, state in extents: |
---|
119 | if state == 'E': |
---|
120 | out.write("E %s %s\n" % (start, end - start)) |
---|
121 | out.flush() |
---|
122 | elif state == 'D': |
---|
123 | offset = start |
---|
124 | while offset < end: |
---|
125 | image.seek(offset) |
---|
126 | chunk = min(max_extent_size, end-offset) |
---|
127 | data = image.read(chunk) |
---|
128 | if len(data) != chunk: |
---|
129 | raise Exception("Short read from image file") |
---|
130 | out.write("D %s %s\n%s\n" % (offset, chunk, data)) |
---|
131 | out.flush() |
---|
132 | offset += chunk |
---|
133 | else: |
---|
134 | raise Exception("INTERNAL ERROR: Invalid state \"%s\"" % state) |
---|
135 | |
---|
136 | |
---|
137 | def usage(out): |
---|
138 | """outputs help message""" |
---|
139 | out.write("Syntax Error: %s [-l] <imagefilename>\n" |
---|
140 | "Requires the file to which to write the image, and reads the " |
---|
141 | "recovery log from stdin.\n" |
---|
142 | "/dev/null may be specified as the image filename to just show " |
---|
143 | "summary information.\n" |
---|
144 | ) |
---|
145 | |
---|
146 | def main(args): |
---|
147 | """Reads log from standard in, writes an image to the image file""" |
---|
148 | if len(args) < 1: |
---|
149 | usage(sys.stderr) |
---|
150 | sys.exit(1) |
---|
151 | |
---|
152 | output_log = False |
---|
153 | if args[0] == '-l': |
---|
154 | output_log = True |
---|
155 | if len(args) != 2: |
---|
156 | usage(sys.stderr) |
---|
157 | sys.exit(1) |
---|
158 | filename = args[1] |
---|
159 | else: |
---|
160 | if len(args) != 1: |
---|
161 | usage(sys.stderr) |
---|
162 | sys.exit(1) |
---|
163 | filename = args[0] |
---|
164 | |
---|
165 | image = open(filename, 'w') |
---|
166 | |
---|
167 | good, bad = write_image_from_log(sys.stdin, image) |
---|
168 | if output_log: |
---|
169 | write_log_from_image(open(filename, 'r'), sys.stdout, good, bad) |
---|
170 | |
---|
171 | if __name__ == '__main__': |
---|
172 | main(sys.argv[1:]) |
---|