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