#!/usr/bin/perl
# $Id: oddb_mkimage 19 2008-04-11 14:27:11Z winex $

use strict;
use Switch;
use Cwd;
use File::Copy;

use vars qw($prog $isodir $datadir);
use vars qw($odtitle @dirlist $dev);
use vars qw($opt_force $opt_check $opt_skip $opt_md5only);

$prog = $0;
$prog =~ s/\/*.*\///g;

# config
# path where created isos will be placed, W/O TRAILING SLASH!
#$isodir = '.2burn';
$isodir = '/srv/ftp/oddb/.2burn';
$datadir = '/srv/ftp/oddb/.data';

sub printusage()
{
	print("Usage: $prog [OPTIONS] DEVICE\n");
	print("       $prog [OPTIONS] ISO_ROOT ...\n");
	print("\n");

	return 0;
}

sub printhelp()
{
	print("Options:\n");
	print("    -        read list from stdin, in case of spaces in dirnames,\n");
	print("             where each line is a ISO_ROOT_==_TITLE!\n");
	print("    --help   print this help\n");
	print("    --force  force overwriting existing files, use with caution!\n");
	print("    --check  check media in DEVICE with MEDIALABEL.iso.md5 found under DATADIR\n");
	print("             in case of making isos from dirs, verify their md5lst and move to DATADIR\n");
	print("    --skip   skip creating iso itself, create md5 only or smth(?)\n");
	print("             SEE SOURCE!!! md5 is skipped too, just info is grabbed!!!\n");
	print("    --md5only  skip creating iso itself, create md5 only. THIS IS REAL. pipe is used\n");
	print("    --config print current config\n");
	print("    DEVICE   use this block device to make an image of it's _isoinfo's-size_\n");
	print("\n");
	print("Notes:\n");
	print("    if an argument is a block device, iso image of it's media will be created\n");
	print("    if an argument is a directory, iso image of it's contents will be created\n");
	print("    ALL PROCESSING IS DONE USING PATHS SPECIFIED IN CONFIG, see $prog --config\n");
	print("\n");
	print("Examples:\n");
	print("    $prog /dev/cdrom   # make an iso image of media in specified device\n");
	print("    $prog /mnt/isos/*  # make iso images of each directory in /mnt/isos/\n");
	print("                            # with title=dirname\n");
	print("\n");

	return 0;
}

sub printconfig()
{
	print("ISODIR:  $isodir\n");
	print("DATADIR: $datadir\n");

	return 0;
}

my $argc = $#ARGV+1;
if ($argc == 0)
{
	printusage();
	exit(0);
}

foreach my $arg (@ARGV)
{
	switch ($arg)
	{
		case /^--help$/
		{
			printusage();
			printhelp();
			exit(0);
		}
		case /^--config$/
		{
			printconfig();
			exit(0);
		}
		case /^-$/
		{
			my $tmp = '';
			while (<>)
			{
				$tmp = $_;
				chomp($tmp);
				$tmp = getcwd() if ($tmp =~ /\.\/*?/);
				push(@dirlist, $tmp);
			}
		}
		case /^--force$/
		{
			$opt_force=1;
		}
		case /^--check$/
		{
			$opt_check=1;
		}
		case /^--skip$/
		{
			$opt_skip=1;
		}
		case /^--md5only$/
		{
			$opt_md5only=1;
		}
		case { -b $arg }
		{
			$dev=$arg;
			last;    # process only 1 device
		}
		else
		{
			$arg = getcwd() if ($arg =~ /^\.[^\.]\/*/);
			push(@dirlist, $arg);
		}
	}
}


# grab all needed info from device/iso/anything
sub grab_info($$)
{
	my $retval = 0;
	my $fd = shift;		# file descriptor, file or block device is used
	my $volid = shift;	# volume identifier

	my $outfile = "$datadir/$volid";	# NOTE: without any suffixes!!!
	my $target_option = '';				# -i "$fd"  OR  dev="$fd"

	print("grab_info($fd, $volid);\n");
	if ( -b $fd )
	{
		print("dvd+rw-mediainfo...\n");
		# TODO: 20080325 winex: check whether cd, dvd or any other
		`dvd+rw-mediainfo "$fd" >"$outfile.media" 2>&1` if (! -s "$outfile.media");
		$target_option = "dev=\"$fd\"";

		`egrep -q 'not a DVD unit|non-DVD media mounted' $outfile.media`;
		if ($? == 0)
		{
			print("wodim...\n");
			`wodim -v -toc $target_option >"$outfile.media" 2>&1`;
		}
	}
	elsif ( -f $fd )
	{
		$target_option = "-i \"$fd\"";
	}
	else
	{
		print(STDERR "$prog: ERROR: not a file or a block device '$fd'\n");
		return -1;
	}

	print("isoinfo x2...\n");
	# 'Joliet escape sequence' stuff go to stderr, here you may nullify it
	`isoinfo -debug -d $target_option >"$outfile.iso.info" 2>&1` if ( ! -s "$outfile.iso.info" );
	$retval += $?;

	# TODO: 20080326 winex: check whether Rock Ridge or Joliet is present
	# even move here the `isoinfo -debug -d` from mkimage_from_dev()
	# for Joliet we assume that cp1251 is used
	`isoinfo -debug -l -J -j cp1251 -R $target_option | iconv -f cp1251 -t utf8 >"$outfile.lst"` if ( ! -s "$outfile.lst" );

	# if NO Joliet is present then 0-sized $outfile.lst is overwritten by
	`isoinfo -debug -l -R $target_option >"$outfile.lst"` if ( ! -s "$outfile.lst" );

	$retval += $?;

	return $retval;
}


sub mkimage_from_dev()
{
	my $info;
	my $volid;
	my $isofile;
	my $blocksize;
	my $volsize;
	my $retval;

	# TODO: 20080129 winex: for DVD and/or UDF we need something like dvd+rw-mediainfo
	# example of isoinfo output:
	#[winex@zealot oddb]$ isoinfo -d dev=/dev/sr0
	#CD-ROM is in ISO 9660 format
	#System id: LINUX
	#Volume id: Medieval2-TotalWar.dvd1
	#Volume set id:
	#Publisher id:
	#Data preparer id:
	#Application id: GENISOIMAGE ISO 9660/HFS FILESYSTEM CREATOR (C) 1993 E.YOUNGDALE (C) 1997-2006 J.PEARSON/J.SCHILLING (C) 2006-2007 CDRKIT TEAM
	#Copyright File id:
	#Abstract File id:
	#Bibliographic File id:
	#Volume set size is: 1
	#Volume set sequence number is: 1
	#Logical block size is: 2048
	#Volume size is: 1968849
	#Joliet with UCS level 3 found
	#Rock Ridge signatures version 1 found

	$info = `isoinfo -debug -d dev=$dev 2>/dev/null`;
	$info =~ s/\n/\t/g;
	$_ = $info;
	if ( ! /CD-ROM is in ISO 9660 format/ )
	{
		print("$prog: Media is not in ISO 9660 format!\n");
		return -1;
	}

	if ( /Volume id: ([^\t]*).*Logical block size is: (\d*)[\t]Volume size is: (\d*)/ )
	{
		$volid = $1;
		$blocksize = $2;
		$volsize = $3;
	}
	print("LABEL: $volid\nBSIZE: $blocksize\nVSIZE: $volsize\n");

	# grab iso/media info
	grab_info($dev, $volid);

	$isofile = $volid.'.iso';

	if ($opt_md5only)
	{
		$retval = 0;
		my $isomd5 = "$datadir/$isofile.md5";
		if ( -s $isomd5 )
		{
			print("$prog: ERROR: md5sum file already exists '$isomd5'\n");
			$retval = 1;
		}
		else
		{
			print("Calculating MD5 of image from '$dev' to '$isomd5'... please wait...\n");
			`dd if=$dev bs=$blocksize count=$volsize 2>/dev/null | md5sum - >"$isomd5"`;
			if ( $? != 0 )
			{
				print("$prog: dd|md5sum failed ($?) :(\n");
				$retval = -2;
			}
			`sed -i 's/  -/  $isofile/' "$isomd5"`;
		}

		return $retval;
	}


	if ($opt_check)
	{
		$retval = 0;
		my $isomd5 = "$datadir/$isofile.md5";
		if ( ! -s $isomd5 )
		{
			print("$prog: ERROR: md5sum file not found '$isomd5'\n");
			$retval = -1;
		}
		else
		{
			my $md5tmp="/tmp/$isofile.md5";
			my $md5sum=`cat "$isomd5" | head -1 | cut -b-32`;
			chomp($md5sum);
			`echo "$md5sum  -" >"$md5tmp"`;
			print("Checking media in '$dev' using '$isomd5'... please wait...\n");
			`dd if=$dev bs=$blocksize count=$volsize 2>/dev/null | md5sum --status -c "$md5tmp"`;
			if ( $? != 0 )
			{
				print("$prog: dd|md5sum failed ($?) :(\n");
				$retval = -2;
			}
			`rm -f "$md5tmp"`;
		}

		return $retval;
	}


	if ( -s "$isodir/$isofile" && ! $opt_force)
	{
		print("$prog: file already exists '$isodir/$isofile'\n");
		return 1;
	}


	return 0 if ( $opt_skip );


	print("DD'ing image... please wait...\n");
	`dd if=$dev of="$isodir/$isofile" bs=$blocksize count=$volsize`;
	if ( $? != 0 )
	{
		print("$prog: dd failed ($?) :(\n");
		return -2;
	}

	my $isomd5 = "$datadir/$isofile.md5";
	if ( ! -s $isomd5 )
	{
		print("Calculating MD5...\n");
		my $olddir=cwd();
		chdir($isodir);
		#`md5sum "$isofile" >>"$isomd5"`;
		`md5sum "$isofile" >"$isomd5"`;
		$retval = $?;
		if ( $retval != 0 )
		{
			print("$prog: md5sum failed ($?) :(\n");
		}
		$retval = $?;
		chdir($olddir);
	}
	else
	{
		print("Checking MD5 from '$isomd5'...");
		my $olddir=cwd();
		chdir($isodir);
		`md5sum --status -c "$isomd5"`;
		$retval = $?;
		if ( $retval != 0 )
		{
			print(" FAILED\n$prog: ERROR: md5sum check failed!!! ($?) :(\n");
		}
		else
		{ print(" OK\n"); }
		chdir($olddir);
	}

	return $retval;
}

sub mkimage_from_dir()
{
	my $user = getlogin || (getpwuid($<))[0] ;
	# my $user = $ENV{USER};

	my $curdir='';
	my $odtitle='';
	my $odfile='';

	my $curdir_esc='';
	my $odtitle_esc='';

	my $n = 0;
	my $retval = 0;
	foreach $curdir (@dirlist)
	{
		# debug
		if (0)
		{
			print("$curdir"."\n");
			next;
		}

		# remove trailing slash if exists
		$curdir =~ s/\/$//;

		# FORCE renaming of dirs that has backticks in it's name
		# i hadn't got good results of shell escaping from perl with these :(
		my $cd = $curdir;
		if ( $cd =~ /\140/ )
		{
			$cd =~ s/\140/\47/g;
			rename("$curdir", "$cd");
			$curdir = $cd;
		}

		# escaping curdir
		# metachars: "$&'();<>?[\]^`{|}~
		$curdir_esc = $curdir;
		my $metachars = " \"\$\&\'\(\)\*\;\<\>\?\[\\\]\^\{\|\}~";

		#$curdir_esc = quotemeta($curdir);	# quotemeta suxx, utf-8 is broken this way :(
		#$curdir_esc =~ s/([\$\&\'\;\[\\\]\{\|\}])/\\$1/g;
		#$curdir_esc =~ s/([$metachars])/\\$1/g;
		$curdir_esc =~ s/([\40\44\46\47\73\133\134\135\173\174\175])/\\$1/g;
		
		#print("$curdir\n");
		#print(`echo "$curdir_esc"`);
		#next;

		if ( ! -d $curdir )
		{
			print("$prog: not a directory '$curdir'\n");
			next;
		}

		$odtitle = $curdir;
		# howto get filename, or last dir_without_trslash!
		# 1st variant
		#my $junk;
		#($junk, $odtitle) = $odtitle =~ m/(.*\/)(.*)$/;
		# 2nd variant
		#$odtitle =~ m/(.*\/)(.*)$/;
		#$odtitle = $2;
		# 3rd variant
		# remove up to last slash
		$odtitle =~ s|.*/||;
		$odtitle_esc = $odtitle;
		$odtitle_esc =~ s/([\40\44\46\47\73\133\134\135\173\174\175])/\\$1/g;

		$odfile = "$isodir/$odtitle.iso";
		if ( -s $odfile && ! $opt_force && ! $opt_skip)
		{
			print("$prog: file already exists '$odfile'\n");
			next;
		}

		# If we already have $datadir/$odtitle.md5lst, it is assumed that md5
		# of that files in $curdir were checked before and they are correct,
		# so just genisoimage it, unless --check-again? is specified
		my $md5lst = "$datadir/$odtitle.md5lst";
		my $md5lst_this = "$curdir/md5lst";

		# A special case is if we have both those files and:
		#   they are identical                   - remove this file?
		#   this is newer than that in $datadir/ - check this, remove old
		if ( (-s $md5lst) && (-s $md5lst_this) )
		{
			print(STDERR "ERROR: $md5lst already exists in db... Whom you are trying to cheat? Do smth please\n");
			next;
		}

		if ( ! -s $md5lst )
		{
			if ( -s $md5lst_this )
			{
				# We have md5lst in $curdir, verify then move into $datadir/
				# TODO: 20080126 winex: check filelist, sizes, verify md5 (in this order)!
				# TODO: 20080314 winex: fix nested file counting!
				print("CHECKING: md5lst of '$curdir'...\n");

				my $nfiles_lst = `cat "$md5lst_this" | wc -l`;
				my $nfiles_real = `find "$curdir" -type f ! -name "md5lst" | wc -l`;
				chomp($nfiles_lst);
				chomp($nfiles_real);

				my $olddir = cwd();
				chdir($curdir);
# 				if ( ! opendir(DIR, '.') )
# 				{
# 					print(STDERR "$prog: can\'t open directory \'$curdir\'\n");
# 					next;
# 				}
# 				my @files_this;
# 				while ($_ = readdir(DIR))
# 				{
# 					# TODO: 20080314 winex: it's not as dumb-delete below, just can't write the right regex :(
# 					#if ( ! m|^(\.)?(\.\.)?(md5lst)?$| )
# 					$_ =~ s|^\.$||;
# 					$_ =~ s|^\.\.$||;
# 					$_ =~ s|^md5lst$||;
# 					$nfiles_real++ if ($_ ne "");
# 				}
# 				closedir(DIR);

				if ($nfiles_lst != $nfiles_real)
				{
					print(STDERR "$prog: real file count $nfiles_real doesn\'t match $nfiles_lst listed in \'$md5lst_this\'\n");
					next;
				}

				#for (my $i = 0; $i < length(@files_this); $i+=2)
				#{
				#	my ($fn, $sz) = $files_this[$i];
				#	print("$fn\t$sz\n");
				#}

				my $nerrors = 0;
				# we already in $curdir, so open 'md5lst' file itself
				if ( ! open(MD5LST, 'md5lst') )
				{
					print(STDERR "$prog: can\'t open \'$md5lst_this\'\n");
					next;
				}
				while (<MD5LST>)
				{
					chomp();
					my ($md5sum, $size, $fname) = split('\t');
					my $size_real = -s "$fname";
					if ( ! -f "$fname" )
					{
						print(STDERR "$prog: no such file: \'$curdir/$fname\'\n");
						last;	# if (!$opt_force_verify);
					}
					if ($size_real != $size)
					{
						print(STDERR "$prog: real size of \'$fname\' NOT EQUAL to listed in \'$md5lst_this\'\n");
						$nerrors++;
						last;	# if (!$opt_force_verify);
					}

					# TODO: 20080411 winex: replace double quotes in $fname and other bad chars
					# of which next shell call may be affected. Of course, no file should
					# contain double qoutes in it's name, but it's necessary and you know
					# what for! :P
					print("checking \'$fname\'...\n");
					`echo "$md5sum  $fname" | md5sum -c`;
					if ($? != 0)
					{
						print(STDERR "$fname: FAILED\n");
						$nerrors++;
						last; # if (!$opt_force_verify);
					}
				}
				chdir($olddir);
				close(MD5LST);

				print(STDERR "Total errors: $nerrors\n");
				next if ($nerrors > 0);
# DEBUG
next if (0);
# DEBUG
				move($md5lst_this, $md5lst);
			}
			elsif (!$opt_check)
			{
				# NO md5lst anywhere, make one in $datadir/
				print("PROCESSING: md5lst of '$curdir'...\n");
				`oddb_lst "$curdir" >"$md5lst"`;
				if ( $? != 0 )
				{ print("FAILED\n"); next; }
			}
		}

		next if ($opt_check);

		print("PROCESSING: '$curdir' to '$odfile'...\n");

		$odfile = "$isodir/$odtitle_esc.iso";
		if ( ! $opt_skip )
		{
			# TODO: 20080126 winex: don't know how to redirect only progress to stdout(or stderr) :(
			# and do we really need log file?
	
			#`genisoimage -udf -R -log-file "$datadir/$odtitle_esc.iso.log" -V "$odtitle" -publisher "$user" -o "$odfile" "$curdir_esc" 2>/dev/null`;
	
			# -iso-level 4 here is ISO9660:1999	
			`genisoimage -print-size -R -iso-level 4 "$curdir_esc"`;
			`genisoimage -R -iso-level 4 -V "$odtitle" -publisher "$user" -o "$odfile" "$curdir_esc" 2>&1 | grep -v 'done, estimate' | tee "$datadir/$odtitle_esc.iso.log"`;
			$retval = $?;
		}
		else { $retval = 0; }

		if ($retval == 0)
		{
			# TODO: 20080126 winex: is cwd and other shit rational here? maybe just kill abs_path in md5 file?
			print("PROCESSING: md5 of '$isodir/$odtitle_esc.iso' to '$datadir/$odtitle_esc.iso.md5'...\n");
			my $olddir=cwd();
			chdir($isodir);
			`md5sum "$odtitle_esc.iso" >"$datadir/$odtitle_esc.iso.md5"`;
			$retval = $?;
			chdir($olddir);
		}

		grab_info($odfile, $odtitle);

		if ($retval == 0)
		{
			$n++;
		}
	}

	return $n;
}

if ($dev ne "")
{
	mkimage_from_dev();
}
else
{
	mkimage_from_dir();
}
