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