#!/usr/bin/perl -w # # $Id: PmHSM.pl,v 0.6 2002/02/11 22:00:01 mjacobs Exp $ # ############################################################################### # # PmHSM - tool for creating CD/DVD-images from directory trees and replace # every file (HSM) or the base directory with symbolic link to # file/directory position in the exported image # Copyright (C) 2002 Martin Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # ############################################################################### # # **************************** # ** PmHSM - Poor Man's HSM ** # **************************** # # Idea: Store locally saved files transparently on an external media # like RW-CD-ROM or MO-discs (future option). mkisofs and cdrecord # are our companions to generate images and write them to CD/DVD # media. # # Exported files will be replaced by symbolic links so they can # be accessed if the export media is mounted. # # Example There is a file on the local file system # # /home/username/download/ImageMagick-5.1.1.tar.gz # # which will be replaced by a link to the external media # # /cdrom/home/username/download/ImageMagick-5.1.1.tar.gz # # PmHSM's purpose is to move seldom used files to an external media. Usage is # quite simple. PmHSM has several steps of operation: # - create an image; # - verify a mounted image with existing data; # - replace local data with links to external data; # It's planned to extend PmHSM with mode ARCHIVE to replace a full directory # tree with one link to it's external representation. # # PmHSM needs the following information: # - the prefix of the exported directory hierarchy, see the above # example, there it is /cdrom/hsm; # - one or more directory paths which should be processed; # - for generating CD-Images a target file name; # - optional filter criteria (last file usage, include pattern, # exclude pattern); # # ToDo: - apropriate reaction to signals; # - more differenciated messages (warning, error, ...), skipping a # file or directory in HSM mode is harmless, but it's a reason to # abort ARCHIVE mode! # - more user friendly front-end, maybe gui; # - add a session tag to be used as virtual root above the # mount-point, think of versioning, autoincrement number in hidden # file (e.g .pmshm); # - remove longest common source path; # - optional do copying files instead of building an iso-image, this # may be useful with MO-drives and media or UDF on CD-ROM/DVD; # - extend this tool for archieving purposes, add an image id; # - remove complete subtree (Albrecht) and replace the root of the # subtree with a symbolic links (seems to be the same as archieving); # - generate file list with image id; require 5.004; # This is the version under which this code was developed. use strict; use English; use Getopt::Long; use vars qw(); use Config; use Cwd; # We need cwd() use File::Path; use File::Basename; use File::Compare; # # Callback routine as handler for non-option arguments which shall be # regarded as directory source names and collected in the global array # SrcDirs::main. # sub CollectDirnames { my $source = shift; push @SrcDirs::main, $source; } # # Scan through directories, push every file with it's full pathname # onto the file stack, recurse into directories. # sub Scan { my ($rFiles, $rDirs, $dir, $Prefix, $notaccessedfor, $level) = @_; my @allentries = (); my $workingdir = cwd(); # Needed to make absolute source pathnames. my ($target, $tmp_source); if ($SigCount::main > 0) { $nRecursion::main--; return; } # open this directory and read it's entries. opendir MYDIR, $dir or return -1; @allentries = readdir MYDIR; closedir MYDIR; # Push the name of this directory onto the stack: if (-w $dir and -x $dir) { my $source = $dir; # Prepare the path name to which the symbolic link will # show on the external media. $target = $Prefix . (($Prefix =~ m/\/$/o) ? '' : '/'); if ($IsDosWin::main) { # To catenate the pathnames we may have to strip off a leading # drive letter colon combination: $target .= ($source =~ s/^[a-zA-Z]\://o); } else { $target .= $source; } $tmp_source = $dir; if ($IsDosWin::main) { # Strip off a leading drive letter: $tmp_source = $tmp_source =~ s/^[a-zA-Z]\://io; } $tmp_source = $TargetDir::main . ($tmp_source =~ m+^/+ ? '' : '/') . $tmp_source; $Summary::main{'scan_diradd'}++; push @$rDirs, { 'stat' => [ stat($dir) ], source => $dir, target => $target, # absolute path on external media. tmp_source => $tmp_source, level => $level, # tree level relative to source dir. }; } else { $Summary::main{'scan_skipdir'}++; print "Skip directory $dir.\n" if ($VerboseFlag::main); $nRecursion::main--; return; } # Now scan through the entries: foreach my $entry (@allentries) { if ($SigCount::main > 0) { last; } my $source = $dir . '/' . $entry; # ignore '.' and '..' entries; next if ($entry =~ m/^\.{1,2}$/o); if (-d $source) { # Recurse into next hierarchy: $nRecursion::main++; Scan(\@$rFiles, \@$rDirs, $source, $Prefix, $notaccessedfor, $level + 1); next; } my $abssource = $source; # Does abssource start with workingdir? Then it's an absolute path. # This should be OS independend, hopefully. if (not $abssource =~ m/^$workingdir/) { # No, so this should be a relative path, make it absolute: $abssource = $workingdir . '/' . $source; } # Prepare the path name to which the symbolic link will # show on the external media. $target = $Prefix . (($Prefix =~ m/\/$/o) ? '' : '/'); if ($IsDosWin::main) { # To catenate the pathnames we may have to strip off a leading # drive letter colon combination: $target .= ($source =~ s/^[a-zA-Z]\://o); } else { $target .= $source; } # Prepare the path name of the file on the shadow tree. $tmp_source = $source; if ($IsDosWin::main) { # Strip a leading drive letter: $tmp_source = $tmp_source =~ s/^[a-zA-Z]\://io; } $tmp_source = $TargetDir::main . ($tmp_source =~ m+^/+ ? '' : '/') . $tmp_source; # Process plain files and symbolic links which can be deleted: my @filestat = stat($source); my $fileage = -A _; my $IsPlainFile = -f _; my $IsSymLink = -l $source; if ($IsPlainFile) { # ignore access time stamp on exported file. if ($IsSymLink or $notaccessedfor <= $fileage) { push @$rFiles, { 'stat' => [ @filestat ], # Save the dir-info for later use. source => $source, # absolute or relative path to source file. abssource => $abssource, # absolute path to source file. target => $target, # absolute path on external media. tmp_source => $tmp_source, # absolute path on shadow tree. level => $level, # tree level, relative to source dir. }; $Summary::main{'scan_fileadd'}++; print "Add file $source\n" if ($VerboseFlag::main); next; } else { # File does not satisfy last access condition. $Summary::main{'scan_skipage'}++; print "Skip file $source, age in days since last access is less than $notaccessedfor days.\n" if ($VerboseFlag::main); next; } } if ($IsSymLink) { $Summary::main{'scan_skipsym'}++; print "Skip sym link $source.\n" if ($VerboseFlag::main); next; } $Summary::main{'scan_skipother'}++; print "Skip $source, it's neither a plain file nor a directory.\n" if ($VerboseFlag::main); } $nRecursion::main--; return; } # # Create the shadow tree, which contains symbolical links to the source # files. This tree will be source for image generation. # sub CreateShadowTree { my ($rDirs, $rFiles) = @_; # First create the directory tree: foreach my $entry (@$rDirs) { if ($SigCount::main > 0) { return 2; } mkpath($entry->{tmp_source}, $VerboseFlag::main, $TempPermission::main) or die "Failed to create directory $entry->{tmp_source}\n"; } # Second create the symbolic links: foreach my $entry (@$rFiles) { if ($SigCount::main > 0) { return 2; } if (symlink($entry->{abssource}, $entry->{tmp_source}) == 0) { print "Error: failed to create symbolic link $entry->{tmp_source}\n"; return 1; } } # Third restore time-, ownership and permissions. foreach my $entry (@$rDirs) { if ($SigCount::main > 0) { return 2; } my ($rc, $action); $rc = 1; if ($rc == 1) { $action = 'set time stamp of'; $rc = utime $entry->{'stat'}[8], $entry->{'stat'}[9], $entry->{tmp_source} if ($rc == 1); } if ($rc == 1) { $action = 'set permissions of'; $rc = chmod $entry->{'stat'}[2], $entry->{tmp_source} if ($rc == 1); } if ($rc == 1) { $action = 'change ownership of'; $rc = chown $entry->{'stat'}[4], $entry->{'stat'}[5], $entry->{tmp_source}; } if ($rc == 1) { next; } print "Error: failed to $action directory $entry->{tmp_source} ($!)\n"; return 1; } return 0; } # # Create isofs: # sub GenIsoFile { my ($image, $rootdir) = (@_); my @args; push @args, $MkisofsBin::main; push @args, "-o$image"; push @args, '-f'; # follow symlinks push @args, '-N'; # omit version numbers push @args, '-allow-multidot'; push @args, '-allow-lowercase'; push @args, '-D'; # Do not use deep directory relocation. push @args, '-l'; # Allow full 31 character file names. push @args, '-relaxed-filenames'; push @args, '-R'; # Use Rockridge extensions. push @args, '-J'; # Generate Joliet extensions for Win9X/WinNT push @args, "$rootdir"; return system(@args); } # # Blank media # sub BlankMedia { my ($method) = @_; my @args; push @args, $CdrecordBin::main; push @args, "-blank=$method"; return system(@args); } # # Burn image onto media # sub BurnImage { my ($source) = @_; my @args; push @args, $CdrecordBin::main; push @args, '-v' if ($VerboseFlag::main); push @args, $source; return system(@args); } # # Do the Cleanup: # sub CleanUp { my ($RmImage) = @_; print "Step: Cleanup...\n"; unlink $ImageFilename::main if ($RmImage); rmtree $TargetDir::main, ($VerboseFlag::main) ? 1 : 0, 0; } #################################################################### # # Miscellaneous # sub CheckBinaries { my ($BinMkisofs, $BinCdrecord, $OptBurnIt, $OptBlankMethod) = @_; my ($FoundMkisofs, $FoundCdrecord) = (0, 0); if ($SkipCreateImage::main == 0 or $OptBurnIt or $OptBlankMethod) { # Get file search path: my @paths; if ($IsDosWin::main) { @paths = split /;/, $ENV{PATH}; } else { @paths = split /:/, $ENV{PATH}; } # # Loop through found paths: # foreach my $path (@paths) { stat($path . '/' . $BinMkisofs); if (-e _ and -x _) { # file exists and we can execute it. $FoundMkisofs = 1; } stat($path . '/' . $BinCdrecord); if (-e _ and -x _) { # file exists and we can execute it. $FoundCdrecord = 1; } } } die "Binary $BinMkisofs not found or no execute rights.\n" if ($SkipCreateImage::main == 0 and !$FoundMkisofs); die "Binary $BinCdrecord not found or no execute rights.\n" if (($OptBurnIt or $OptBlankMethod) and !$FoundCdrecord); } # # Validate given directories. # sub CheckSourceDirs { my $rSrcDir = shift; my $ErrorsFound = 0; my $wdir = cwd(); foreach my $thisdir (@{$rSrcDir}) { print " Checking path '$thisdir' ...\n" if ($VerboseFlag::main); # Is this a directory? if (! -d $thisdir) { $ErrorsFound++; print " Argument '$thisdir' isn't a directory.\n"; next; } # We do not allow relative paths down to root. if ($thisdir =~ m/\.\./o) { $ErrorsFound++; print " Directory $thisdir refused: contains '..'\n"; next; } # Try to chdir to the given directory: if (chdir $thisdir) { my $newwdir = cwd(); # So far this seams to be ok. Now check, that this directory # is not a subpath of $wdir: if ($wdir =~ m/^$newwdir/) { $ErrorsFound++; print " Directory '$thisdir' refused: is subpath of '$wdir'\n"; next; } } else { $ErrorsFound++; print " Can't chdir to '$thisdir', refused: reason '$!'\n"; } } # Restore original working directory. chdir $wdir; return $ErrorsFound == 0; } #################################################################### # # Signal handlers # sub catch_pipe { $SigPipe::main++; $SigCount::main++; } sub catch_term { $SigTerm::main++; $SigCount::main++ } sub catch_int { $SigInt::main++; $SigCount::main++; } ################## # Das ist main() # ################## defined $Config::Config{d_symlink} or die "Symbolic links needed.\n"; # # Wir müssen Signale haben! # defined $Config{sig_name} or die "No sigs?\n"; # To establish array and/or hash with names and numbers of signals # see at the Perl book, page 338. # # To handle OS specific pathname structure (drive letter) # $IsDosWin::main = $OSNAME =~ /^(?:MSDOS|MSWin32)/i; # # Global array (list) for source directory names: # @SrcDirs::main = (); # # Global array (list) storing all directory objects of directory hierarchy # @Directories::main = (); # # Global array with all source file objects: # @Files::main = (); # # Global mode of operation, defaults to 'hsm' # $opmode::main = 'HSM'; # Permission for temporary created files and directories. $TempPermission::main = 0700; # Initialize with defaults: my $MountPoint = ''; my $HelpFlag; my $rc = 0; my $CompareErrors = 0; my $BlankMethod = ''; my $BurnIt = 0; $CdrecordBin::main = 'cdrecord'; $ImageFilename::main = ($ENV{TEMP} || '/tmp') . "/$$.iso"; $TargetDir::main = ($ENV{TEMP} || '/tmp') . "/$$" . '.pmhsm'; $VerboseFlag::main = 0; $VersionFlag::main = 0; $TotalSize::main = 0; $TotalLimit::main = 650000000; $RemoveFilesFlag::main = 0; $SkipCreateImage::main = -1; $MkisofsBin::main = 'mkisofs'; $VerifyFilesFlag::main = 0; $PmHSMVersion::main = (split /[$ ]+/, '$Revision: 0.6 $')[2]; $NotAccessedFor::main = 0; # Collect some statistics: %Summary::main = ( 'scan_skipage' => 0, 'scan_skipsym' => 0, 'scan_skipother' => 0, 'scan_skipdir' => 0, 'scan_fileadd' => 0, 'scan_diradd' => 0, ); # Globale Variable für Signale: $SigPipe::main = 0; $SigTerm::main = 0; $SigInt::main = 0; $SigCount::main = 0; # # Establish signal handlers. # $SIG{PIPE} = \&catch_pipe; $SIG{TERM} = \&catch_term; $SIG{INT} = \&catch_int; # Now process the arguments: GetOptions('mountpoint=s', \$MountPoint, 'imagefile=s', \$ImageFilename::main, 'help', \$HelpFlag, 'verbose!', \$VerboseFlag::main, 'tempdir=s', \$TargetDir::main, 'limit=i', \$TotalLimit::main, 'skipimage!', \$SkipCreateImage::main, 'removefiles!', \$RemoveFilesFlag::main, 'verify!', \$VerifyFilesFlag::main, 'mode=s', \$opmode::main, 'blank=s', \$BlankMethod, 'burnit!', \$BurnIt, 'Version', \$VersionFlag::main, 'notaccessedfor=i', \$NotAccessedFor::main, '<>', \&CollectDirnames) || die "Error processing options.\n"; $opmode::main = uc($opmode::main); $BlankMethod = lc($BlankMethod); # Verifying includes skipping image creation. if ($SkipCreateImage::main < 0) { $SkipCreateImage::main = ($VerifyFilesFlag::main) ? 1 : 0; } if ($HelpFlag) { print "Usage: PmHSM --mountpoint= [--imagefile=] [--mode=HSM|ARCHIVE] [--limit=] [--tempdir=] [--notaccessedfor=] [--[no]skipimage] [--[no]removefiles] [--[no]verify] [--blank=] [--[no]burnit] [--help] [--Version] [dir1 [dir2 [...]]]\n"; print "\tFor more information see: perldoc PmHSM.pl\n"; exit 1; } if ($VersionFlag::main) { print "PmHSM $PmHSMVersion::main\n"; exit 1; } if ($VerboseFlag::main) { print "Current parameters are:\n"; print "Mount point=$MountPoint\n"; print "Image file name=$ImageFilename::main\n"; print "Temporary root=$TargetDir::main\n"; print "Skip image creation=$SkipCreateImage::main\n"; print "Remove Files=$RemoveFilesFlag::main\n"; print "Mode of operation=$opmode::main\n"; print "Blank method=$BlankMethod\n"; print "Burn image=$BurnIt\n"; print "Not accessed for=$NotAccessedFor::main\n"; print "Source directories:\n"; foreach my $dirname (@SrcDirs::main) { print " $dirname\n"; } } die "Error missing mount point.\n" if (length($MountPoint) == 0); die "Error missing image file name.\n" if (length($ImageFilename::main) == 0); die "Error missing dirctory file nam.\n" if (length($TargetDir::main) == 0); die "Error missing source directories.\n" if (@SrcDirs::main == 0); die "Unknown mode of operation.\n" if ($opmode::main !~ m/(HSM|ARCHIVE)/o); die "Unknown blank method '$BlankMethod'.\n" if ($BlankMethod and !$BlankMethod =~ m/(?:all|disc|disk|fast|minimal)/o); # # Make sure we have access to mkisofs and cdrecord # print "Checking whether needed binaries can be found and executed...\n" if ($VerboseFlag::main); CheckBinaries($MkisofsBin::main, $CdrecordBin::main, $BurnIt, $BlankMethod); # # Check, if given directories are valid. We only accept directory paths which # point to a subdirectory, not the opposite direction. # Directories which contain '..' are refused as directories which are # a sub path of the working directory. # print "Checking of given directories...\n" if ($VerboseFlag::main); CheckSourceDirs(\@SrcDirs::main) or die "Error: invalid source directories given.\n"; # # Scan directories, collect file and directory data. # print "Step: Scan source directories...\n"; $nRecursion::main = 0; foreach my $path (@SrcDirs::main) { if ($SigCount::main > 0) { die "Canceled ...\n"; } Scan(\@Files::main, \@Directories::main, $path, $MountPoint, $NotAccessedFor::main, 1); } print " directories added: $Summary::main{'scan_diradd'}\n"; print " directories skipped (access rights): $Summary::main{'scan_skipdir'}\n"; print " files added: $Summary::main{'scan_fileadd'}\n"; print " files skipped (cause age): $Summary::main{'scan_skipage'}\n"; print " files skipped (cause symlink): $Summary::main{'scan_skipsym'}\n"; print " skipped (cause other): $Summary::main{'scan_skipother'}\n"; if ($opmode::main =~ m/ARCHIVE/o and $Summary::main{'scan_skipdir'} > 0) { # Abort archiving. print "\taborted, there are directories we can't remove later.\n"; exit 1; } print "Step: Calculate total size...\n"; $TotalSize::main = 0; foreach my $entry (@Files::main) { if ($SigCount::main > 0) { die "Canceled ...\n"; } $TotalSize::main += $entry->{'stat'}[7]; } print " Total file size is $TotalSize::main bytes.\n"; if ($TotalSize::main > $TotalLimit::main) { print "Total image size ($TotalLimit::main) will be exceeded ($TotalSize::main).\n"; exit 1; } # # Create the base target directory: # print "Step: Create shadow directory tree...\n"; # # Now build a directory hierarchy which mirrors all files via symbolic links. # This new build tree is the source for building the archive file system. # if (CreateShadowTree(\@Directories::main, \@Files::main)) { CleanUp($BurnIt); die "Failed to create shadow tree.\n"; } # # Now generate the isofs with mkisofs # if ($SigCount::main == 0 and $SkipCreateImage::main == 0) { print "Step: Create image file $ImageFilename::main...\n"; $rc = GenIsoFile($ImageFilename::main, $TargetDir::main); print "Image creation finished with $rc.\n" if ($VerboseFlag::main || $rc > 0); if ($rc > 0) { # In case of error, remove the image file, it's invalid! unlink($ImageFilename::main); print " Removed invalid image file $ImageFilename::main.\n"; } # Image generation succeded, so we can burn the image, # first (optional) blank the media: if ($rc == 0 and $BlankMethod) { $rc = BlankMedia($BlankMethod); if ($rc > 0) { print "Error blanking media using method $BlankMethod\n"; } } if ($rc == 0 and $BurnIt) { $rc = BurnImage($ImageFilename::main); if ($rc > 0) { print "Error burning image: $rc\n"; } } } if ($SigCount::main == 0 and $VerifyFilesFlag::main) { # # Compare files with (mounted) files. Do it only with files # which are (still) not symbolically linked (new or modified) files. # print "Step: Verify files...\n"; foreach my $entry (@Files::main) { if ($SigCount::main > 0) { CleanUp($BurnIt); die "Canceled ...\n"; } if (not -r $entry->{target}) { print "Warning: Skip file $entry->{target} (missing and/or not readable)\n"; next; } if (not -l $entry->{source}) { # compare both files. if (compare($entry->{source}, $entry->{target}) == 0) { print "$entry->{source} ok.\n" if ($VerboseFlag::main); } else { $CompareErrors++; print "Error: $entry->{source} and $entry->{target} differ.\n"; } } } } if ($SigCount::main == 0 and $RemoveFilesFlag::main == 1 and $CompareErrors == 0 and $rc == 0) { my %Removed = ( Files => 0, Errors => 0, ); # # Everything seems to be ok. # if ($opmode::main =~ m/HSM/o) { # # This is the HSM way: # # We can replace the files with a symbolic # link to the archieved file. # print "Step: Replace local files with symbolic link...\n"; foreach my $entry (@Files::main) { if ($SigCount::main > 0) { CleanUp($BurnIt); die "Canceled ... Sorry removed $Removed{Files} files so far!\n"; } # Files which are already symbolic links are skipped. This # preserves the symbolic link's file creation date as the # archieving date. if (-f $entry->{source} and not -l $entry->{source}) { my $tempname = $entry->{source} . '_'; # First rename the original file, then create the symlink and # if everything is ok remove the original file. if (rename($entry->{source}, $tempname) == 1 and symlink($entry->{target}, $entry->{source}) == 1 and unlink($tempname) == 1) { $Removed{Files}++; print "Replaced: $entry->{source}\n" if ($VerboseFlag::main); next; } else { # Something has gone wrong! Leave the loop! $Removed{Errors}++; print STDERR "Fatal error \'$!\' unlinking $entry->{source}.\n"; last; } } else { print "Skipped: $entry->{source}\n" if ($VerboseFlag::main); } } print " replaced $Removed{Files} with symbolic link."; } if ($opmode::main =~ m/ARCHIVE/o) { # # This is the ARCHIVE way: # print "Step: remove local files and replace base directory with symbolic link...\n"; $Removed{Files} = rmtree(\@SrcDirs::main, $VerboseFlag::main, undef); if ($SigCount::main > 0) { CleanUp($BurnIt); die "Canceled ... removed $Removed{Files} files so far!\n"; } # # Now create symbolic links instead of base directories. # foreach my $rd (@Directories::main) { if ($SigCount::main > 0) { CleanUp($BurnIt); die "Canceled ... removed $Removed{Files} files so far!\n"; } if (${$rd}{level} == 1) { if (symlink(${$rd}{target}, ${$rd}{source}) == 1) { $Removed{Dirs}++; print " Replaced source dir '${$rd}{source}' with link to '${$rd}{target}'\n" if ($VerboseFlag::main); next; } else { $Removed{Errors}++; print STDERR "Failed to create symbolic link ${$rd}{source}, reason '$!'\n"; last; } } } if ($Removed{Errors} == 0) { print "Successfully replaced $Removed{Dirs} directories with link to external media.\n" if ($VerboseFlag::main); } } } CleanUp($BurnIt); print "Image file $ImageFilename::main available for further processing.\n" if (-f $ImageFilename::main); exit 0; __END__ X X X X X X =head1 NAME PmHSM - PmHSM is Poor Man's Hierarchical Storage Management =head1 SYNOPSIS PmHSM.pl [--mountpoint=] [--imagefile=] [--[no]verbose] [--tempdir=] [--notaccessedfor=] [--[no]skipimage] [--[no]removefiles] [--[no]verify] [--mode=] [--blank=] [--[no]burnit] [] [... ] =head1 DESCRIPTION Generate an ISO image from directories, burn them on CD/DVD and replace the source with symbolic links to your external media. CD/DVD images are generated using mkisofs(1). Generated image files may violate several standards by omitting version numbers, not doing deep directory relocation and allowing full 31 character file names. In addition the images use Rockridge and Joliet extensions. =head1 EXAMPLES B Mount point for external media: /cdrom directory tree to be exported: ~/download =head2 Example 1 -- Write image and remove files in one step C will generate an image containing all files of directory ~/download, including all subdirectories. This image will immediately be written on media using cdrecord(1). Then all files will be removed and replaced with symbolic links pointing to their correspoding location under mountpoint /cdrom. It's assumed that your media is already (or still) blank. If not, add option blank, see below. This one step operation can only be used on the first time generating an external media or in a two drive configuration. =head2 Example 2 -- Write image, verify files, remove files in separate steps As the previous example, it's assumed you use a blank media. Otherwise use option --blank=ImethodE>, see below, to blank media before writing the image to it. C then take your media and mount it under /cdrom, verify your media with C after successful verification, remove files: C For testing purposes it's ok to skip burning the image, using a loop device to mount the image instead of the media and do verification against the image. =head2 Example 3 -- Archive a directory tree on CD-ROM, do it in one step This is comparable with Example 1, with the difference, that mode ARCHIVE is choosen, which means that the complete tree is removed and only the base directory is replaced with a symbolic link. C will generate an image containing all files of directory project_723 -- it's a subdirectory of the current directory -- including it's subdirectories. This image is immediately written on media using cdrecord(1). Then the complete directory tree of project_723 will be removed and replaced with a symbolic link named project_723 pointing to the mount point. It's assumed that your media is already blank. If not, add option blank, see below. =head1 OVERVIEW There exists lots of files which consume disk space and need not to be in permanent access. Copying them to some external media and deleting them afterwards is not handy, because afterwards files are not seen anymore. Instead of removing them, replacing them with a symbolic link is much more elegant. PmHSM generates the image of files, replaces exported files with symbolic link. Updating files is very easy. Remove them and they will be removed from the external media on the next run. Replace them with a new local copy and the new file version replaces the exported version. It's self documenting. Creation date of the symbolic link is date of last media generation. =head1 OPTIONS =over 4 =item mountpoint B<--mountpoint=>IdirectoryE> Directory name under which the image will be mounted if you want to have transparent access. =item imagefile B<--imagefile=>IfileE> Full path name of the image file. Defaults to process id with file name extension .iso in the systems temporary directory. =item help B<--help> Print a short help message and terminate. =item Version B<--Version> Print Version number and terminate. =item tempdir B<--tempdir=>IdirectoryE> Specify directory under which the shadow tree will be build. Defaults to process id with extension '.pmhsm' as base directory. =item notaccessedfor B<--notaccessedfor=>Iage in daysE> Specify age of files since last access in days to select file for export. Default is 0. Keep in mind, that backuping up files may affect the access time of files. =item limit B<--limit=>Imax image sizeE> Estimate size of file system image. Continue generating file system image only if size is less or equal. Defaults to 650,000,000. =item skipimage B<--[no]skipimage> Skip generating image. Do only building shadow directory tree. Default is off, so PmHSM will generate the file system image by default. =item removefiles B<--[no]removefiles> After generating image, remove source files. Default is off, that means: B. I recommend to use this option after applying verify option. =item verify B<--[no]verify> Having a mounted media, compare it's content with source tree. Default is off. Setting Verify includes setting SkipCreateImage in case this option isn't used. Verifying files ignores symbolic links, which would be a noop. =item mode B<--mode=>ImodeE> Mode of operation. Implemented are HSM (default) and ARCHIVE. Mode HSM replaces single files with a symbolic link to it's external location, mode ARCHIVE removes the complete tree and replaces only it's base directory with a symbolic link. =item blank B<--blank=>Iblank methodE> If the image shall be burned by PmHSM and target is a CD-RW media, then use this specified method. cdrecord(1) gets this argument. Actually supported methods are: C, C, C, C, C =item burnit B<--[no]burnit> Burn the image immediately. Default is off. Actually this can only be done, if your installation doesn't share a device for accessing your previous media generation and for writing a new image. PmHSM doesn't know how to umount media before inserting a new one. =back =head1 ENVIRONMENT =over 4 =item TEMP If variable TEMP is defined, it is used as default target directory for image files and/or the temporary directory hierarchy. Otherwise the default is /tmp. =item PATH PATH is used to find mkisofs and/or cdrecord. =back =head1 FILES =head1 BUGS It's an early version, so there will be lot's of bugs. Until now, it's tested on a Linux box. There is some support for the DOS/Windows envrionment, but it's untested. You cannot use another image generator than mkisofs(1). PmHSM uses cdrecord(1) to do automatic burning of file system images. cdrecord(1) must be configured with it's cdrecord file, see L =head1 SEE ALSO mkisofs(1), cdrecord(1) =head1 AUTHOR Martin Jacobs, 100.179370@germanynet.de =head1 HISTORY =cut # vim:nowrap