Kommandozeilen Werkzeug rsplit

Winfried Mueller, www.reintechnisch.de, Start: 08.04.2005, Stand: 23.04.2005

Windows hat kein Systemtool an Board, mit dem man lange Dateien aufsplitten kann. Nötig ist sowas z.B., wenn man lange Image-Dateien hat, die man auf mehrere CD's wegbrennen möchte. Man teilt das Image dann in rund 650 MB große Teile auf, brennt diese einzeln weg und kann sie später wieder zusammensetzen.

Auch kann man damit große Dateien auf mehrere Memorysticks verteilen. Ein Split-Werkzeug ist einfach etwas ganz grundlegendes, was man immer mal wieder braucht.

Also hab ich mal ein Split-Programm in Ruby geschrieben. Benötigt wird Ruby 1.8, getestet ist es unter Debian Linux und Windows 2000. Folgende Möglichkeiten hat man:

 
Datei splitten:
# rsplit -s 650M meinfile.bin

MD5SUM über alle gesplitteten Teile bilden:
# rsplit -c meinfile.bin

Teile wieder zusammenfügen:
# rsplit -j meinfile.bin

Datei splitten und Windows Batch Datei
erzeugen, die wieder alles zusammenfügt:
# rsplit -s 1.4M -b meinfile.bin

Das Programm kann also auch ein Batch-File erzeugen, um die Teile wieder zusammenzufügen. Das ist günstig, wenn jemand auch ohne das rsplit-Programm aus den Teilen wieder ein Ganzes machen will. Unter Linux tut es auch ein cat meinfile.bin.0* >meinfile.bin.join.

Den MD5SUM Check habe ich eingebaut, damit man wirklich nochmal überprüfen kann, ob die gesplitteten Teile korrekt sind. Ist auch das Orignal-File vorhanden, wird darüber ebenfalls die md5sum gebildet und dann verglichen. Ist beides identisch, erhält man eine positive Rückmeldung.

Von der Geschwindigkeit ist es recht brauchbar. Dateizugriffe scheinen unter Ruby recht fix zu sein. Es ist wesentlich schneller, als das beliebte Freeware-Tool jsplit. 800 MB auf einem 450 MHz Rechner werden in etwa 1.5 Minuten gesplittet. In etwa 2 Minuten ist die Checksum über das Original und die gesplitteten Dateien gebildet. Für die Split-Aktion hat jsplit dagegen 15 Minuten gebraucht, obwohl es in einer kompilierten Version vorliegt! Möchte man es wirklich uneingeschränkt nutzen, muss man auch noch 19 Dollar dafür bezahlen.

rsplit ist noch in einem Alpha Status, funktionierte bei Experimenten einwandfrei, muss aber noch den Alltagstest bestehen.

Wer gerne mal einen Überblick über den Zeitaufwand solcher Programme bekommen möchte: Ich habe mit einigen Tests etwa 10-15 Stunden dran gearbeitet. Eingesetzt habe ich hier keine neuen Techniken, in die ich mich hätte erst einarbeiten müssen. Es ist wie immer, die grundsätzliche Problemlösung ist in vielleicht 4 Stunden erledigt, die restlichen 6-11 Stunden braucht man dafür, es wirklich rund zu machen. Es sollte schließlich kein Wegwerfskript werden sondern halbwegs sauber designt und wartbar. Wenn man jetzt mal 50 Euro pro Programmier-Stunde ansetzen würde, wäre so eine Lösung 500-750 Euro wert. Das Programm ist dabei relativ trivial. Damit sollte klar werden, welch unglaublichen Werte im Bereich der freien Software zu finden sind. Die Entwicklung von Ruby selbst wird wohl Ressourcen gebunden haben, die viele Millionen Euro wert sind.

Das Design nutzt eine interessante Abstraktion. SplittetFileWriter kann über #write so bedient werden, als wäre es ein normales File. Es kümmert sich für den Nutzer unsichtbar darum, den Datenstrom in mehrere Dateien (filename.000-filename.999) aufzusplitten. Bei SplittetFileReader ist es ganz ähnlich, es besitzt eine read-Methode, die für den Nutzer die gesplitteten Files verbirgt. Für den Nutzer der Klasse sieht es so aus, als würde von einer Datei gelesen. Beide Klassen kapseln also die Komplexität und stellen einfach überschaubare Schnittstellen zur Verfügung.

Da diese Klassen universell sein sollten, dürfen sie keine Ausgaben auf STDOUT produzieren. Andererseits möchte man bei der Ausführung ein Feedback auf dem Bilschirm haben. Hierfür gibt es die hookNextFile Methode, die man anwendungsspezifisch überschreibt/implementiert. Hier ist es eine simple puts Ausgabe.

Programm

 
# rsplit.rb - Dateien splitten und wieder zusammenfügen.
#              
#              
#
# Winfried Mueller, www.reintechnisch.de, (c) 2005, GPL


require 'optparse'
require 'digest/md5'

Version = "0.0.3 - 2005/04/15"

module Config
  BUF_LENGTH = 102400
end

# Strings as '1K', '10M', '1024' to Integer
def size_to_i( size )
  mult = 1
  rx_KILOBYTE = /K$/i
  rx_MEGABYTE = /M$/i
  if size =~ rx_KILOBYTE
    mult = 1024
  elsif size =~ rx_MEGABYTE
    mult = 1024 * 1024
  end
  (size.scan( /[0-9\.]+/ )[0].to_f * mult).to_i
end


module SplittedFileName
  def f_ending( num = @file_num )
    sprintf( ".%3.3d", num )
  end
  private :f_ending

  def full_filename( num = @file_num )
    @filename + f_ending( num )
  end
  private :full_filename
end


class SplittedFileWriter
  include SplittedFileName

  def initialize( filename, length )
    @filename = filename
    @file_num = 1
    @curr_file = nil
    @file_length = length
    @curr_wrote = 0
    open
  end
  def open
    @file_num = 1
    @curr_file = File.open( full_filename, "w+b" )
    hookNextFile( full_filename, @file_num )
  end  
  def close
    @curr_file.close
    @curr_file = nil
  end

  def open_next
    close
    @file_num += 1
    @curr_file = File.open( full_filename, "w+b" )
    hookNextFile( full_filename, @file_num )
  end
  private :open_next

  def write( buf )
    raise "File not open." unless @curr_file
    if @curr_wrote + buf.length > @file_length
      split_pos = @file_length - @curr_wrote
      if split_pos > 0
        @curr_file.write( buf[0..(split_pos-1)] )
      end
      open_next
      @curr_wrote = @curr_file.write( buf[split_pos..-1] )
    else
      @curr_wrote += @curr_file.write( buf )
    end
  end

  def gen_bat
    File.open( "join.bat", "w+" ) do |f|
      f.puts %!type "#{full_filename(1)}" > "#{@filename}.join"!
      (2..@file_num).each do |n|
        f.puts %!type "#{full_filename(n)}" >> "#{@filename}.join"!
      end
    end
  end

  def hookNextFile( filename, filenum )
    # App must be implement
  end

  def f_ending( num = @file_num )
    sprintf( ".%3.3d", num )
  end
  private :f_ending

  def full_filename( num = @file_num )
    @filename + f_ending( num )
  end
  private :full_filename
end

class SplittedFileReader
  include SplittedFileName

  def initialize( filename )
    @filename = filename
    @curr_file = nil
    @eof = true
    open(filename)
  end

  def open( filename )
    @file_num = 1
    @curr_file = File.open( full_filename, "rb" )
    @eof = @curr_file.eof?
    hookNextFile( full_filename, @file_num )
  end

  def open_next()
    if File.file?( full_filename(@file_num + 1) )
      @file_num += 1
      @curr_file.close
      @curr_file = File.open( full_filename, "rb" )
      hookNextFile( full_filename, @file_num )
      true
    else
      false
    end  
  end

  def eof?
    @eof
  end

  def read( length, buf )
    @curr_file.read( length, buf )
    if buf.length < length
     if open_next
       next_buf = ""
       self.read( length - buf.length, next_buf )
       buf << next_buf
     else
       @eof = true
       raise "Assert" unless @curr_file.eof? 
     end
    end
    raise "Assert" if (@eof == false && buf.length != length)
    buf
  end

  def close()
    @curr_file.close if @curr_file
  end

  def hookNextFile( filename, filenum )
    # App must be implement
  end

end



# ----

$file_size = size_to_i( "1.44M" )
$with_batch = false
$action = :split

ARGV.options do |o|
  o.banner = "Usage: rsplit [options] file"
  o.separator( "Split files." )
  o.separator( "" )

  o.on( "-s", "--size SIZE", "Size of splitted Files." ) do |s|
    $file_size = size_to_i( s ) 
  end
  o.on( "-b", "--with-bat", "Generate Windows Batch File to join." ) do
    $with_batch = true
  end
  o.on( "-j", "--join", "Join splitted Files." ) do
    $action = :join
  end
  o.on( "-c", "--check-md5", "Eval Checksum over splitted files." ) do
    $action = :md5
  end
  o.on_tail("-h", "--help", "Diese Hilfe." ) do
    puts o
    exit
  end
  o.on_tail("--version", "Show version") do
    puts o.ver
    puts ""
    puts "Copyright (C) 2005 Winfried Mueller, www.reintechnisch.de"
    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

class App
  def self.splitFile
    infile = ARGV[0]
    raise "Input File not found." unless (infile && File.file?(infile))
    File.open(infile, "rb") do |f|
      sf = SplittedFileWriter.new( infile, $file_size )
      buf = ""

      while !f.eof? 
        f.read( Config::BUF_LENGTH, buf )
        sf.write( buf )
      end
      if $with_batch
        puts "Write join.bat"
        sf.gen_bat
      end
      sf.close
    end
    0
  end

  def self.joinFile
    infile = ARGV[0]

    raise "No File specified." unless infile

    outfile = infile + ".join"
    puts "Generate #{outfile}..."
    File.open( outfile , "w+b" ) do |of|

      sf = SplittedFileReader.new( infile )
        buf = ""
        while !sf.eof?
          sf.read( Config::BUF_LENGTH, buf )
          of.write( buf )
        end
      sf.close
    end
    0
  end

  def self.md5Checksum
    infile = ARGV[0]
    rval = 0
    raise "No File specified." unless infile

    puts "Evaluate Checksum"
    puts "Read splitted Files..."
    d = Digest::MD5.new
    sf = SplittedFileReader.new( infile )
      buf = ""
      while !sf.eof?
        sf.read( Config::BUF_LENGTH, buf )
        d << buf
      end
    sf.close
    printf "%s %s (splitted)\n", d.hexdigest, infile

    if File.file?( infile )
      puts "Read original File..."
      dr = Digest::MD5.new
      File.open( infile, "rb" ) do |f|
        buf = ""
        while !f.eof?
          f.read( Config::BUF_LENGTH, buf )
          dr << buf
        end
      end
      printf "%s %s (original)\n", dr.hexdigest, infile
      if dr == d
        puts "Check ok."
      else
        puts "Check failed."
        rval = 2
      end
    end
    rval
  end
end



class SplittedFileWriter
  def hookNextFile( filename, filenum )
    puts "Write #{filename}"
  end
end

class SplittedFileReader
  def hookNextFile( filename, filenum )
    puts "Read File: #{filename}"
  end
end


begin
  err = 0
  ARGV.parse!
  case $action
    when :join
      err = App.joinFile    
    when :split
      err = App.splitFile
    when :md5
      err = App.md5Checksum
    else
      raise "Undefined action."
  end
  exit err
rescue => exc
  STDERR.puts "E: #{exc.message}"
  STDERR.puts ARGV.options.to_s
  # include it for Debuging
  # raise
  exit 1
end