Kommandozeilen Werkzeug md5check

Winfried Mueller, www.reintechnisch.de, Start: 26.12.2006, Stand: 26.12.2006

Die Dateikonistenz ganzer Verzeichnisbäume zu überprüfen ist die Aufgabe dieses Werkzeugs. Das ist z.B. praktisch, wenn man ein Verzeichnis woanders hinkopiert hat und nochmal auf "Nummer sicher" gehen will, ob wirklich jede Datei Byte für Byte korrekt kopiert wurde. Auch für die Datensicherung kann dies ein sehr hilfreiches Werkzeug sein. Oder, um Veränderungen an Dateien aufzuspüren, wo sich eigentlich nichts verändern darf.

Das Werkzeug funktioniert ähnlich, wie das unter Unix bekannte md5sum, ist jedoch auf Verzeichnisbäume ausgelegt und funktioniert plattformübergreifend. Mit dem Befehl:

 
md5check -l /home/klaus > klaus.lst

erzeugt man eine Liste namens klaus.lst, in der alle Dateinamen unterhalb des Verzeichnisses /home/klaus inkl. der dazugehörigen md5sum abgelegt werden. Hierzu wird jede Datei Byte für Byte eingelesen und die md5sum berechnet. Natürlich braucht dies entsprechend Zeit. Trotzdem ist Ruby hier recht fix.

Diese Liste kann man nun später für eine Überprüfung benutzen. Nehmen wir an, wir haben das Verzeichnis /home/klaus nach /home/test/klaus kopiert. Dann führt folgender Befehl zur Überprüfung mit der zuvor erstellten Liste:

 
md5check -c klaus.lst /home/test/klaus

Bei einer Datensicherung kann man das gut nutzen, in dem man vor der Sicherung von jeder Datei die md5sum berechnen lässt und die erzeugte Liste ebenso mit sichert. So kann man später genau erkennen, wo Dateien evtl. defekt sind. Datenträger können so von Zeit zu Zeit automatisiert überprüft werden.

Oder man vergleicht die aktuellen Dateien in einem Verzeichnis mit einer älteren Liste, um die Veränderungen im Verzeichnisbaum zu erkennen. Für Programmverzeichnisse kann man so sehr gut unerlaubte Veränderungen entdecken, z.B. bei Virenbefall. Man könnte z.B. das Verzeichnis c:\winnt überprüfen, wo sich ja alle ausführbaren Programme nicht verändern sollten, solange man keine neue Software einspielt oder Updates macht.

Das Programm war ein Schnell-Hack und ist grob unter Windows 2000 und Ubuntu Linux mit Ruby 1.8.x getestet. Wenn man bedenkt, dass jedes Byte einer jeden Datei gelesen werden muss, ist die Geschwindigkeit des Werkzeugs beeindruckend. Das liegt vor allem an der groß gewählten Blockgröße und das digest/md5 ein C-Modul ist.

Quellcode

 
#!/usr/bin/ruby1.8

# md5check - Erzeuge und teste md5checksum-Liste
# Winfried Mueller, www.reintechnisch.de, (c) 2006, GPL
# Start: 26.12.2006, Stand: 26.12.2006
# 
# Generate List
#   md5check -l /home/klaus > klaus.lst
#
# Check List
#   md5check -c klaus.lst /home/klaus
# 

DEBUG = true
Version = "2006/12/26"


require 'find'
require 'optparse'
require 'ostruct'
require 'digest/md5'


# md5sum einer Datei berechnen
# 
def getmd5sum( file )
  result = nil
  if File.file?( file )
    dr = Digest::MD5.new
    begin
      File.open( file, "rb" ) do |f|
        buf = ""
        while !f.eof?
          f.read( 102400, buf )
          dr << buf
        end
      end
      result = dr
    rescue
      #kein Fehler produzieren
    end
  end
  return result
end

# md5 Checksum-Liste erzeugen, beginnend bei basedir rekursiv
# Ausgabe auf stdout
#
def makemd5list( basedir )
  Find.find( basedir ) do |path|
    next if File.directory?(path)
    spath = path.sub( /^#{basedir}\/?/, "")
    next if spath.length ==  0
    #puts path
    digest = getmd5sum(path)
    if !digest
      $stderr.puts "File not accessible: #{path}"
    else
      printf "%s  %s\n", digest.hexdigest, spath
    end
  end
end

# Checksum-Liste mit Verzeichnis prüfen
#
def checkmd5list( list, basedir, verbose=false )
  line_nr = 1
  File.foreach( list ) do |line|
    if line =~ /^([0-9a-f]+)\s+(.*)$/
      lchecksum = $1
      lfname = $2.chomp
      #puts lchecksum+ "----"+lfname      
    else
      raise( "List Format error line: #{line_nr}" )
    end
    dstfile = File.join( basedir, lfname )
    if !File.file?(dstfile)
      printf( "%s -- not found\n", lfname )
    else
      digest = getmd5sum( dstfile )
      if !digest
        printf( "%s -- not accessible.\n", lfname )
      elsif digest.hexdigest != lchecksum
        printf( "%s -- different\n", lfname )
      else
        if verbose
          printf( "%s -- ok\n", lfname )
        end
      end
    end    
    line_nr += 1
  end
end


# ----------------------------------------------------------------

ARGV.options do |o|
  o.banner = "Usage: md5check [options] basedir"
  o.separator( "md5check Check md5sum for a filelist or make a filelist." )
  o.separator( "" )

  o.on( "-l", "--mklist", "Make a md5sum Filelist recursive from basedir." ) do
    $options.mklist = true
  end

  o.on( "-c", "--check FILELIST", "Check Filelist with basedir." ) do |f|
    $options.check = f
  end

  o.on( "-v", "--verbose", "All Check results (default only different Files)." ) do
    $options.verbose = true
  end

  o.on_tail("-h", "--help", "This help." ) do
    puts o
    exit
  end
  o.on_tail("--version", "Show version") do
    puts o.ver
    puts "Written by Winfried Mueller, www.reintechnisch.de"
    puts ""
    puts "Copyright (C) 2006 Winfried Mueller"
    puts "This is free software; see the source for copying conditions."
    puts "There is NO warranty; not even for MERCHANTABILITY or" 
    puts "FITNESS FOR A PARTICULAR PURPOSE."
    exit
  end
end


# ----------------------------------------------------------------
begin
  $options = OpenStruct.new
  $options.verbose = false
  err_code = 0

  ARGV.parse!

  if ARGV.length == 0
    # Help if no Arguments
    STDERR.puts ARGV.options.to_s
  else
    # process
    fn_config = ARGV 
    unless fn_config != nil 
      #puts fn_config
      raise( "No basedir specified." )
    end

    basedir = fn_config[0].dup
    basedir.gsub!( /\\/, "/" ) #Trenner für Pfad auf / anstatt \ umschreiben
    if !File.directory?( basedir )
      raise( "basedir not found." )
    end

    if $options.mklist != nil
      makemd5list( basedir )
    elsif $options.check != nil
      checkmd5list( $options.check, basedir, $options.verbose )
    else
      STDERR.puts ARGV.options.to_s
    end
  end
rescue SystemExit
rescue Exception => exc
  STDERR.puts "E: #{exc.message}"
  #STDERR.puts ARGV.options.to_s
  raise if DEBUG
end
exit err_code