Blog: PAR2 file mover: par2mover.rb

File par2mover.rb, 4.9 KB (added by RevRagnarok, 10 years ago)
 
#!/bin/env ruby
# PAR2 File Parser for Ruby
# Bare essentials (but with room to grow) to get a listing of all files
#
# Note: This was written on Cygwin, there may be things that need to be tweaked.
# [email protected]
#
# Ref: http://parchive.sourceforge.net/docs/specifications/parity-volume-spec/article-spec.html
require 'FileUtils'
# Example - Par2FileListing will return Par2FileDescriptionPackets only
# This would normally be our main() equivalent
class Par2FileListing
def initialize(file)
@pkFile = Par2PacketFile.new(file)
end # init
def each
debug = false
@pkFile.each{|thepkt|
# Option 1: Iterate over each packet type calling Test():
# print "FileDesc!\n" if Par2FileDescriptionPacket.Test(thepkt)
# Option 2: Use a case statement checking Sig:
case thepkt['hdr'].type
when Par2FileDescriptionPacket.Sig
descPkt = Par2FileDescriptionPacket.new(thepkt)
print "#{descPkt.fileName} (#{descPkt.fileLength} bytes)\n" if debug
yield descPkt
# At this time, I don't care about any others...
else
print "Unknown packet type: '#{thepkt['hdr'].type}'\n" if debug
end # case
} # pkFile
end # next_file
end # Par2FileListing
# The header of every packet type
class Par2PacketHeader
attr_accessor :magic, :length, :hash, :setID, :type
def initialize(raw_header)
@magic = raw_header[0..7]
throw RuntimeError.new("Header check failed") if @magic.slice(/PAR2\0PKT/).nil?
@length = raw_header[8..15].unpack('Q')[0]
@hash = raw_header[16..31] # TODO: Need to convert to something useful
@setID = raw_header[32..47] # TODO: Need to convert to something useful
@type = raw_header[48..63]
end # init
end # Par2PacketHeader
# The file I/O class
class Par2PacketFile
# attr_accessor :data
def initialize(infile)
begin
@data = Array.new
fh = File.open(infile, "r");
while true
# Read in a PAR2 header
header = Par2PacketHeader.new(fh.sysread(64));
@data << {'hdr' => header, 'data' => fh.sysread(header.length-64)}
end # while
rescue EOFError
fh.close
end # rescue
end
def each
@data.each{|x| yield x}
end
end # Par2PacketFile
# A base interface for all packet types
class Par2BasePacket
attr_accessor :setID
def initialize(thepkt)
@setID = thepkt['hdr'].setID
end # init
def Par2BasePacket.Test(pkt)
throw RuntimeError.new("Must override base class!")
end # Test
def Par2BasePacket.Sig
throw RuntimeError.new("Must override base class!")
end # Sig
end # Par2BasePacket
class WrongPacketType < RuntimeError; end
# The File Description Packet
class Par2FileDescriptionPacket < Par2BasePacket
attr_accessor :fileID, :fileMD5, :fileMD5_16, :fileLength, :fileName
def Par2FileDescriptionPacket.Sig
"PAR 2.0\0FileDesc"
end # Sig
def Par2FileDescriptionPacket.Test(thepkt)
thepkt['hdr'].type == Sig()
end
def initialize(thepkt)
super(thepkt)
throw WrongPacketType.new("Par2FileDescriptionPacket but not valid!") if !Par2FileDescriptionPacket.Test(thepkt)
@fileID = thepkt['data'][ 0..15]; # TODO: Need to convert to something useful
@fileMD5 = thepkt['data'][16..31]; # TODO: Need to convert to something useful
@fileMD5_16 = thepkt['data'][32..47]; # TODO: Need to convert to something useful
@fileLength = thepkt['data'][48..55].unpack('Q')[0]
@fileName = thepkt['data'][56..-1].chomp("\0").chomp("\0").chomp("\0") # There can be up to 3 NULLs
end # init
end # Par2FileDescriptionPacket
########################################################################### End of support objects (old "par2packet.rb")
begin
debug = false
procd = {} # Need to keep track; don't want to process files repeatedly. (This may not be needed since we move them out and check existence...)
Dir.glob('*.par2', File::FNM_CASEFOLD){|parfile|
mylist = Par2FileListing.new(parfile)
target = File.basename(parfile, ".par2")
target = $1 if target =~ /^(.*)\.par2/i # Above may miss all-caps
target = $1 if target =~ /^(.*)\.mp3/i
target = $1 if target =~ /^(.*)\.vol/i
print "Processing #{target}... ";
filecnt = 0
FileUtils.mkdir target unless File.exist? target
FileUtils.move parfile, target, :verbose => debug
mylist.each{|thepkt|
if procd[thepkt.fileName].nil?
# We have never seen this file before
# print "#{parfile}:\n\t" << thepkt.fileName << "\n"
if File.exist? thepkt.fileName # Don't crash if file missing
procd[thepkt.fileName] = thepkt.setID # Store for later
FileUtils.move thepkt.fileName, target, :verbose => debug
filecnt = filecnt+1
end # exists
else # seen it before
print "Skipping #{thepkt.fileName} - already processed!\n" if debug
next
end # diff file
} # mylist
print "#{filecnt} file(s).\n"
} # glob
end