3 # Directory integrity scanner.
8 from os
.path
import join
10 from cPickle
import dump
, load
16 """Root of directory generator"""
17 topstat
= os
.lstat(top
)
18 for x
in walker(top
, '.', topstat
):
21 def walker(path
, name
, dirstat
):
22 """Directory tree generator.
24 At one point, this started as a copy of os.walk from Python's
25 library. Even the arguments are different now.
29 names
= os
.listdir(path
)
31 sys
.stderr
.write("Warning, can't read dir: %s\n" % path
)
34 # The verification algorithm requires the names to be sorted.
37 # Stat each name found, and put the result in one of two lists.
38 dirs
, nondirs
= [], []
40 if path
== '.' and (onename
== "0sure.dat.gz" or
41 onename
== "0sure.bak.gz" or
42 onename
== "0sure.0.gz"):
44 st
= os
.lstat(join(path
, onename
))
45 if S_ISDIR(st
.st_mode
):
46 dirs
.append((onename
, st
))
48 nondirs
.append((onename
, st
))
50 # Indicate "entering" the directory.
51 yield 'd', name
, convert_stat(dirstat
)
53 # Then recursively walk into all of the subdirectories.
54 for (onename
, st
) in dirs
:
55 subpath
= join(path
, onename
)
56 if st
.st_dev
== dirstat
.st_dev
:
57 for x
in walker(subpath
, onename
, st
):
60 # Then yield each entry that is not a subdirectory.
61 for (onename
, st
) in nondirs
:
62 yield '-', onename
, convert_stat(st
)
64 # Last, yield the leaving.
67 # Convert the passed stat info into an association of the information
68 # itself. Does not do anything that requires reading the file (such
69 # as readlink or md5).
71 if S_ISDIR(st
.st_mode
):
72 return { 'kind': 'dir',
75 'perm': S_IMODE(st
.st_mode
) }
77 elif S_ISREG(st
.st_mode
):
78 return { 'kind': 'file',
84 'perm': S_IMODE(st
.st_mode
) }
86 elif S_ISLNK(st
.st_mode
):
87 return { 'kind': 'lnk' }
90 return { 'kind': 'sock',
93 'perm': S_IMODE(st
.st_mode
) }
96 return { 'kind': 'fifo',
99 'perm': S_IMODE(st
.st_mode
) }
102 return { 'kind': 'blk',
105 'devmaj': os
.major(st
.st_rdev
),
106 'devmin': os
.minor(st
.st_rdev
),
107 'perm': S_IMODE(st
.st_mode
) }
110 return { 'kind': 'chr',
113 'devmaj': os
.major(st
.st_rdev
),
114 'devmin': os
.minor(st
.st_rdev
),
115 'perm': S_IMODE(st
.st_mode
) }
118 raise "Unknown file kind"
121 """Make an empty tree. No meaningful attributes for the root
127 def empty_generator():
132 """Class for comparing two directory iterations. Keeps track of
133 state, and allows child classes to define handlers for the various
134 types of differences found."""
136 def __init__(self
, left
, right
):
140 # Default handlers for the 6 possible changes (or not changes)
141 # that can happen in a directory. The adds and deletes take an
142 # additional argument that will be set to true if this added or
143 # remoted entity is contained in an entirely new directory. Some
144 # handlers may want to avoid printing verbose messages for the
145 # contents of added or deleted directories, and can use this
147 def handle_same_dir(self
, path
, a
, b
):
148 #print "same_dir(%s, %s, %s)" % (path, a, b)
149 return empty_generator()
150 def handle_delete_dir(self
, path
, a
, recursing
):
151 #print "delete_dir(%s, %s, %s)" % (path, a, recursing)
152 return empty_generator()
153 def handle_add_dir(self
, path
, a
, recursing
):
154 #print "add_dir(%s, %s, %s)" % (path, a, recursing)
155 return empty_generator()
156 def handle_same_nondir(self
, path
, a
, b
):
157 #print "same_nondir(%s, %s, %s)" % (path, a, b)
158 return empty_generator()
159 def handle_delete_nondir(self
, path
, a
, recursing
):
160 #print "delete_nondir(%s, %s, %s)" % (path, a, recursing)
161 return empty_generator()
162 def handle_add_nondir(self
, path
, a
, recursing
):
163 #print "add_nondir(%s, %s, %s)" % (path, a, recursing)
164 return empty_generator()
165 def handle_leave(self
, path
, recursing
):
166 return empty_generator()
169 a
= self
.__left
.next()
171 raise "Scan doesn't start with a directory"
172 b
= self
.__right
.next()
174 raise "Tree walk doesn't start with a directory"
175 for x
in self
.handle_same_dir(".", a
, b
):
177 for x
in self
.__run
(b
[1], 1):
180 def __run(self
, path
, depth
):
181 """Iterate both pairs of directories equally
183 Processes the contents of a single directory, recursively
184 calling itself to handle child directories. Returns with both
185 iterators advanced past the 'u' node that ends the dir."""
186 # print "run(%d): '%s'" % (depth, path)
187 a
= self
.__left
.next()
188 b
= self
.__right
.next()
191 # print "Comparing (%d) %s and %s" % (depth, a, b)
192 if a
[0] == 'u' and b
[0] == 'u':
193 # Both are leaving the directory.
194 # print "leave(%d): '%s'" % (depth, path)
195 for x
in self
.handle_leave(path
, False):
199 elif a
[0] == 'd' and b
[0] == 'd':
200 # Both looking at a directory entry.
203 # if the name is the same, walk the tree.
204 for x
in self
.handle_same_dir(path
, a
, b
):
206 for x
in self
.__run
(os
.path
.join(path
, a
[1]), depth
+ 1):
208 a
= self
.__left
.next()
209 b
= self
.__right
.next()
213 # A directory has been deleted.
214 for x
in self
.handle_delete_dir(path
, a
, False):
216 for x
in self
.delete_whole_dir(self
.__left
,
217 os
.path
.join(path
, a
[1])):
219 a
= self
.__left
.next()
223 # A directory has been added.
224 for x
in self
.handle_add_dir(path
, b
, False):
227 for x
in self
.add_whole_dir(self
.__right
,
228 os
.path
.join(path
, b
[1])):
230 b
= self
.__right
.next()
233 elif a
[0] == '-' and b
[0] == '-':
234 # Both are looking at a non-dir.
238 for x
in self
.handle_same_nondir(path
, a
, b
):
240 a
= self
.__left
.next()
241 b
= self
.__right
.next()
246 for x
in self
.handle_delete_nondir(path
, a
, False):
248 a
= self
.__left
.next()
253 for x
in self
.handle_add_nondir(path
, b
, False):
255 b
= self
.__right
.next()
258 elif a
[0] == '-' and b
[0] == 'u':
259 for x
in self
.handle_delete_nondir(path
, a
, False):
261 a
= self
.__left
.next()
264 elif a
[0] == 'u' and b
[0] == '-':
265 for x
in self
.handle_add_nondir(path
, b
, False):
267 b
= self
.__right
.next()
270 elif a
[0] == 'd' and (b
[0] == '-' or b
[0] == 'u'):
271 for x
in self
.handle_delete_dir(path
, a
, False):
273 for x
in self
.delete_whole_dir(self
.__left
,
274 os
.path
.join(path
, a
[1])):
276 a
= self
.__left
.next()
279 elif (a
[0] == '-' or a
[0] == 'u') and b
[0] == 'd':
280 for x
in self
.handle_add_dir(path
, b
, False):
282 for x
in self
.add_whole_dir(self
.__right
,
283 os
.path
.join(path
, b
[1])):
285 b
= self
.__right
.next()
289 print "Unhandled case: '%s' and '%s'" % (a
[0], b
[0])
292 def add_whole_dir(self
, iter, path
):
293 "Consume entries until this directory has been added"
294 # print "add_whole_dir: %s" % path
298 for x
in self
.handle_leave(path
, True):
302 for x
in self
.handle_add_dir(path
, a
, True):
304 for x
in self
.add_whole_dir(iter, os
.path
.join(path
, a
[1])):
307 for x
in self
.handle_add_nondir(path
, a
, True):
310 def delete_whole_dir(self
, iter, path
):
311 "Consume entries until this directory has been deleted"
312 # print "delete_whole_dir: %s" % path
316 for x
in self
.handle_leave(path
, True):
320 for x
in self
.handle_delete_dir(path
, a
, True):
322 for x
in self
.delete_whole_dir(iter, os
.path
.join(path
, a
[1])):
325 for x
in self
.handle_delete_nondir(path
, a
, True):
329 'dir': ['uid', 'gid', 'perm'],
330 'file': ['uid', 'gid', 'mtime', 'perm', 'md5'],
332 'sock': ['uid', 'gid', 'perm'],
333 'fifo': ['uid', 'gid', 'perm'],
334 'blk': ['uid', 'gid', 'perm', 'devmaj', 'devmin'],
335 'chr': ['uid', 'gid', 'perm', 'devmaj', 'devmin'],
337 def compare_entries(path
, a
, b
):
338 if a
['kind'] != b
['kind']:
339 yield "- %-4s %s" % (a
['kind'], path
)
340 yield "+ %-4s %s" % (b
['kind'], path
)
343 for item
in __must_match
[a
['kind']]:
344 if not (a
.has_key(item
) and b
.has_key(item
)):
346 elif a
[item
] != b
[item
]:
349 yield "[%s] %s" % (",".join(misses
), path
)
351 if a
.has_key('targ'):
352 yield " old targ: %s" % a
['targ']
353 if b
.has_key('targ'):
354 yield " new targ: %s" % b
['targ']
357 class check_comparer(comparer
):
358 """Comparer for comparing either two trees, or a tree and a
359 filesystem. 'right' should be the newer tree.
360 Yields strings giving the tree differences.
362 def handle_same_dir(self
, path
, a
, b
):
363 return compare_entries(os
.path
.join(path
, a
[1]), a
[2], b
[2])
365 def handle_delete_dir(self
, path
, a
, recursing
):
369 yield "- dir %s" % (os
.path
.join(path
, a
[1]))
370 def handle_add_dir(self
, path
, a
, recursing
):
374 yield "+ dir %s" % (os
.path
.join(path
, a
[1]))
375 def handle_same_nondir(self
, path
, a
, b
):
376 return compare_entries(os
.path
.join(path
, a
[1]), a
[2], b
[2])
378 def handle_delete_nondir(self
, path
, a
, recursing
):
382 yield "- %s" % (os
.path
.join(path
, a
[1]))
383 def handle_add_nondir(self
, path
, a
, recursing
):
387 yield "+ %s" % (os
.path
.join(path
, a
[1]))
389 def update_link(assoc
, path
, name
):
390 if assoc
['kind'] == 'lnk':
391 assoc
['targ'] = os
.readlink(os
.path
.join(path
, name
))
393 class update_comparer(comparer
):
394 """Yields a tree equivalent to the right tree, which should be
395 coming from a live filesystem. Fills in symlink destinations and
396 file md5sums (if possible)."""
398 def handle_same_dir(self
, path
, a
, b
):
402 def handle_add_dir(self
, path
, a
, recursing
):
406 def handle_same_nondir(self
, path
, a
, b
):
407 update_link(b
[2], path
, b
[1])
411 def handle_add_nondir(self
, path
, a
, recursing
):
412 update_link(a
[2], path
, a
[1])
413 if a
[2]['kind'] == 'file':
414 a
[2]['md5'] = hashing
.hashof(os
.path
.join(path
, a
[1]))
418 def handle_leave(self
, path
, recursing
):
422 version
= 'Asure scan version 1.0'
425 """Iterate over a previously written dump"""
426 fd
= gzip
.open(path
, 'rb')
429 raise "incompatible version of asure file"
436 def writer(path
, iter):
437 """Write the given item (probably assembled iterator)"""
438 fd
= gzip
.open(path
, 'wb')
439 dump(version
, fd
, -1)
446 """Perform a fresh scan of the filesystem"""
447 tree
= update_comparer(empty_tree(), walk('.'))
448 writer('0sure.0.gz', tree
.run())
450 os
.rename('0sure.dat.gz', '0sure.bak.gz')
453 os
.rename('0sure.0.gz', '0sure.dat.gz')
456 """Perform a scan of the filesystem, and compare it with the scan
457 file. reports differences."""
458 prior
= reader('0sure.dat.gz')
459 cur
= update_comparer(empty_tree(), walk('.')).run()
460 # compare_trees(prior, cur)
461 for x
in check_comparer(prior
, cur
).run():
465 """Compare the previous scan with the current."""
466 prior
= reader('0sure.bak.gz')
467 cur
= reader('0sure.dat.gz')
468 for x
in check_comparer(prior
, cur
).run():
474 if argv
[0] == 'scan':
476 elif argv
[0] == 'update':
478 elif argv
[0] == 'check':
480 elif argv
[0] == 'signoff':
482 elif argv
[0] == 'show':
483 for i
in reader('0sure.dat.gz'):
487 print "Usage: asure {scan|update|check}"
490 if __name__
== '__main__':