- The first tool genfsdb is used to create a database from a tree.
- The second tool cmpfsdb is used to compare 2 databases. The database comparison output indicates timestamp changes, deletions, and new files or directories.
#Copyright 2006 George Peter Staplin
proc generate.file.system.database {db root} {
proc out data "[list puts [set fd [open $db w]]] \$data"
recurse $root
close $fd
}
proc recurse {dir} {
foreach f [lsort -dictionary [glob -nocomplain [file join $dir *]]] {
#puts FILE:$f
if {![file exists $f]} {
#
# The file is a symbolic link that doesn't point to anything.
#
continue
}
file stat $f stats
#
# It's critical that we use list here, because the filename
# may have spaces.
#
out [list $stats(ctime) $stats(mtime) $f]
if {[file isdirectory $f]} {
#
# XXX we could use a trampoline here to eliminate the recursion
# The wiki has an example for such a trampoline by RS.
# XXX in unix we also have the issue of symbolic links.
# We need a circular link test to make this complete.
#
recurse $f
}
}
}
proc main {argc argv} {
if {2 != $argc} {
puts stderr "syntax is: [info script] database filesystem-root"
return 1
}
generate.file.system.database [lindex $argv 0] [lindex $argv 1]
return 0
}
exit [main $::argc $::argv]
----
'''cmpfsdb-5.tcl:'''
# Copyright 2006 George Peter Staplin
# Revision 5
# May 31, 2006 fixed a DELETED NEW pattern with proc filter.invalid.
array set ::records {}
array set ::changes {}
proc read.records id {
global records
#
# Read 500 chars, unless that would exceed the amount remaining.
#
set amount 500
if {$amount > $records($id,remaining)} {
set amount $records($id,remaining)
}
#
# Concatenate the partial record (if there was one) with the new data.
#
set data [split $records($id,partial)[read $id $amount] \n]
#puts DATA:$data
#
#XXX check for [eof $id] just in case the db is changed by another program?
#
#
# Recalculate the remaining data.
#
set records($id,remaining) [expr {$records($id,remaining) - $amount}]
#
# Set the valid records (terminated by \n) in the records array.
#
set records($id,records) [lrange $data 0 [expr {[llength $data] - 2}]]
#puts RECORDS:$records($id,records)
#
# There may be a partial record at the very end, so save that for use later.
#
set records($id,partial) [lindex $data end]
#puts PARTIAL:$records($id,partial)
set records($id,offset) [tell $id]
}
proc init.record {id f} {
global records
set records($id,file) $f
set records($id,fd) $id
set records($id,offset) 0
set records($id,size) [file size $f]
set records($id,remaining) $records($id,size)
set records($id,partial) ""
set records($id,records) [list]
read.records $id
}
proc compare.records {a b} {
foreach {a_ctime a_mtime a_f} $a break
foreach {b_ctime b_mtime b_f} $b break
global changes
if {$a_f eq $b_f} {
if {$a_ctime != $b_ctime} {
lappend changes($a_f) CTIME
}
if {$a_mtime != $b_mtime} {
lappend changes($a_f) MTIME
}
return 0
} else {
#puts "a_f $a_f"
#puts "b_f $b_f"
return [string compare $a_f $b_f]
}
}
proc next.record id {
global records
if {![llength $records($id,records)]} {
#
# We need to attempt to read more records, because the list is empty.
#
if {$records($id,remaining) <= 0} {
#
# This record database has reached the end.
#
return [list]
}
read.records $id
}
set r [lindex $records($id,records) 0]
set records($id,records) [lrange $records($id,records) 1 end]
#puts REC:$r
return $r
}
proc compare.databases {a b} {
global records changes
set ar [next.record $a]
set br [next.record $b]
while {[llength $ar] && [llength $br]} {
set a_f [lindex $ar 2]
set b_f [lindex $br 2]
#puts "CMP $a_f $b_f"
switch -- [compare.records $ar $br] {
-1 {
#
# $a_f < $b_f in character value
# $a_f was deleted
#
lappend changes($a_f) DELETED
set ar [next.record $a]
}
0 {
set ar [next.record $a]
set br [next.record $b]
}
1 {
#
# $a_f > $b_f in character value
# Therefore the file $b_f is a new file.
# XXX is this always right? It seems like it should be, because
# the other operations go a record at a time, and the values are pre-sorted.
#
#puts NEW
lappend changes($b_f) NEW
set br [next.record $b]
}
}
}
#puts AR:$ar
#puts BR:$br
#
# One or both of the lists are exhausted now.
# We must see which it is, and then list the files
# remaining as NEW or DELETED.
#
if {![llength $ar]} {
#
# We have a remaining file unhandled by the loop above.
#
if {[llength $br]} {
lappend changes([lindex $br 2]) NEW
}
#
# The files remaining are new in the 2nd database/b.
#
while {[llength [set br [next.record $b]]]} {
lappend changes([lindex $br 2]) NEW
}
}
if {![llength $br]} {
#
# This record wasn't handled by the loop above.
#
if {[llength $ar]} {
lappend changes([lindex $ar 2]) DELETED
}
#
# The files remaining were deleted from the 2nd database/b.
#
while {[llength [set ar [next.record $a]]]} {
lappend changes([lindex $ar 2]) DELETED
}
}
}
proc filter.invalid ar_var {
upvar $ar_var ar
foreach {key value} [array get ar] {
if {[set a [lsearch -exact $value DELETED]] >= 0 \
&& [lsearch -exact $value NEW] >= 0} {
set value [lreplace $value $a $a]
set b [lsearch -exact $value NEW]
set value [lreplace $value $b $b]
if {![llength $value]} {
unset ar($key)
continue
}
set ar($key) $value
}
}
}
proc main {argc argv} {
if {2 != $argc} {
puts stderr "syntax is: [info script] database-1 database-2"
return 1
}
foreach {f1 f2} $argv break
set id1 [open $f1 r]
set id2 [open $f2 r]
init.record $id1 $f1
init.record $id2 $f2
compare.databases $id1 $id2
filter.invalid ::changes
parray ::changes
return 0
}
exit [main $::argc $::argv]schlenk For a tripwire like tool, which can also check differences between directories see:
#!/bin/sh
#
# trip'em a small script to create filesystem reports similar
# to tripwire
#
# -*- tcl -*- \
exec tclsh "$0" ${1+"$@"}
package require fileutil
package require logger
package require cmdline
package require md5 2
set version 0.3
set DEBUG 0
# Options
set subcmd [list save check]
set options {
{database.arg "stolper.dat" \
{file used to store the hashes and other info}}
{dir.arg "." \
{directory to process}}
{loglevel.arg "info" \
{loglevel to use \
(debug,info, notice,warn,error,critical)}}
{recursive \
{process subdirectories recusively}}
}
set usage " save | check\nOptions:"
proc optionenFehler {fehler {kopfzeile {}} } {
global options
if {![string equal $kopfzeile ""]} {
puts stderr $kopfzeile
}
puts stderr "[info script] save ?optionen?"
puts stderr "[info script] check ?optionen?"
puts stderr [::cmdline::usage $options "Options:" ]
exit $fehler
}
if {[llength $argv] > 0} {
set cmd [lindex $argv 0]
if {[lsearch -exact $subcmd $cmd] == -1} {
optionenFehler 1 "Invalid command \"$cmd\"."
}
set argv [lrange $argv 1 end]
} else {
optionenFehler 2
exit 2
}
# parse options
if {[catch {::cmdline::getoptions argv $options $usage} opts ]} {
puts stderr $opts
exit 1
}
foreach {option wert} $opts {
if {$::DEBUG} {puts stdout "Option: $option Value: $wert"}
switch -glob -- $option {
da* {
if {![file exists $wert] && [string equal $cmd check]} {
puts stderr "Error: database \"$wert\" does not exist."
exit 3
}
set Config(DB) $wert
}
di* {
if {![file isdirectory $wert]} {
puts stderr "Error: \"$wert\" is not an existing directory."
exit 4
}
set Config(Startverzeichnis) [file normalize $wert]
}
logl* {
if {[lsearch -exact [::logger::levels] $wert]==-1} {
puts stderr "Error: unknown loglevel \"$wert\" ."
exit 5
}
set Config(Loglevel) $wert
}
r* {
set Config(rekursiv) $wert
}
default {}
}
}
unset opts
set log [::logger::init stolper]
${log}::setlevel $Config(Loglevel)
proc check {log datenbank start rekursiv } {
if {[catch {open $datenbank} fd]} {
${log}::critical "failed to open database file \"$datenbank\""
exit 5
}
set line [gets $fd]
if {[string compare "# trip'em $::version" $line]} {
${log}::critical "file \"$datenbank\" is not \
a trip'em $::version database."
exit 6
}
set line [gets $fd]
set verzeichnis ""
regexp {# rootdir:\t(.*)} $line -> verzeichnis
if {[string compare $verzeichnis $start]} {
${log}::critical "root dirs are different \
database used \"$verzeichnis\"."
exit 7
}
set line [gets $fd]
set seconds 0
regexp {# created at:\t([0-9]+)} $line -> seconds
${log}::info "created at: [clock format $seconds]"
set line [gets $fd]
set dbrekursiv 0
regexp {# recursive:\t([01])} $line -> dbrekursiv
${log}::info "creating file list"
if {!$rekursiv} {
set dateien [glob -nocomplain -directory $start *]
} else {
set dateien [::fileutil::find $start]
}
set maxlen 0
while {![eof $fd] && [gets $fd line]} {
if {[llength $line] == 3} {
set name [lindex $line 2]
set dbmtime($name) [lindex $line 0]
set dbmd5($name) [lindex $line 1]
if {[string length $name] > $maxlen} {set maxlen [string length $name]}
}
}
${log}::notice "[array size dbmtime] files processed"
close $fd
set haveChanges 0
foreach datei [lsort -ascii $dateien] {
if {![file isfile $datei]} {continue}
if {[string length $datei] > $maxlen } {set maxlen [string length $datei]}
set md5change 0
set mtimechange 0
set neu 0
if {[catch {file mtime $datei} mtime]} {
set mtime no
}
set md5 [::md5::md5 -hex -filename $datei]
if {[info exists dbmtime($datei)]} {
if {[string compare -nocase $md5 $dbmd5($datei)]} {
set md5change 1
}
if {$mtime != $dbmtime($datei)} {
set mtimechange 1
}
unset dbmtime($datei)
unset dbmd5($datei)
} else {
set neu 1
}
if {$mtimechange || $md5change || $neu} {
incr haveChanges
puts -nonewline stdout "[format "%-[expr {$maxlen+5}]s" $datei]"
if {$neu} {
puts stdout "NEW"
continue
}
if {$mtimechange} {
puts -nonewline stdout "MTIME "
}
if {$md5change} {
puts -nonewline stdout "MD5 "
}
puts stdout ""
}
}
if {[array size dbmtime]} {
foreach datei [array keys dbmtime] {
puts stdout "[format "%-[expr {$maxlen+5}]s\tMISSING" $datei]"
incr haveChanges
}
}
if {$haveChanges == 0} {
puts "NO CHANGES"
} else {
puts "DETECTED $haveChanges CHANGES"
}
}
proc save {log datenbank start rekursiv } {
${log}::info "creating file list"
if {!$rekursiv} {
set dateien [glob -nocomplain -directory $start *]
} else {
set dateien [::fileutil::find $start]
}
${log}::info "[llength $dateien] files found"
if {[catch {open $datenbank {CREAT WRONLY}} fd]} {
${log}::critical "could not open database file \"$datenbank\""
exit 5
}
${log}::notice "opened database file \"$datenbank\" ."
puts $fd "# trip'em $::version"
puts $fd "# rootdir:\t$start"
puts $fd "# created at:\t[clock seconds]"
puts $fd "# recursive:\t$rekursiv"
foreach datei [lsort -ascii $dateien] {
if {![file isfile $datei]} {continue}
if {[catch {file mtime $datei} mtime]} {
set mtime "no"
}
set md5 [::md5::md5 -hex -filename $datei]
puts $fd [list $mtime $md5 $datei]
${log}::info "processed file $datei"
}
close $fd
${log}::notice "completed database \"$datenbank\" ."
}
$cmd $log $Config(DB) $Config(Startverzeichnis) $Config(rekursiv)
