Current File : //var/wcp4/demo1812/public_html/file/private/lib/GT/Tar.pm
# ==================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
#   GT::Tar
#   Author: Scott Beck
#   $Id: Tar.pm,v 1.46 2002/04/07 03:35:35 jagerman Exp $
#
# Copyright (c) 2002 Gossamer Threads Inc.  All Rights Reserved.
# ==================================================================
#
# Description: A general purpose taring and untaring module.
#

package GT::Tar;
# ==================================================================
# Pragmas
    use vars qw/$DEBUG $ERRORS $FAKE_GETPWUID $HAVE_GZIP $FAKE_GETGRGID $FH/;
    use strict;

# System modules
    use Fcntl;

# Contants
    use constant BLOCK    => 4096;
    use constant FILE     => 0;
    use constant HARDLINK => 1;
    use constant SYMLINK  => 2;
    use constant CHARDEV  => 3;
    use constant BLOCKDEV => 4;
    use constant DIR      => 5;
    use constant FIFO     => 6;
    use constant SOCKET   => 8;
    use constant UNKNOWN  => 9;

# Internal modules
    use GT::Base;

# Globals
    $DEBUG = 0;
    @GT::Tar::ISA = qw{GT::Base};

    $ERRORS = {
        OPEN        => "Could not open %s. Reason: %s",
        READ        => "There was an error reading from %s. Expected to read %s bytes, but only got %s.",
        BINMODE     => "Could not binmode %s. Reason: %s",
        BADARGS     => "Bad arguments passed to %s. Reason: %s",
        CHECKSUM    => "Checksum Error parsing tar file. Most likely this is a corrupt tar.\nHeader: %s\nChecksum: %s\nFile: %s\n",
        NOBODY      => "File '%s' does not have a body!",
        CANTFIND    => "Unable to find a file named: '%s' in tar archive.",
        CHMOD       => "Could not chmod %s, Reason: %s",
        DIRFILE     => "'%s' exists and is a file. Cannot create directory",
        MKDIR       => "Could not mkdir %s, Reason: %s",
        RENAME      => "Unable to rename temp file: '%s' to tar file '%s'. Reason: %s",
        NOGZIP      => "Compress::Zlib module is required to work with .tar.gz files."
    };

    $FAKE_GETPWUID = "unknown" if ($^O eq 'MSWin32');
    $FAKE_GETGRGID = "unknown" if ($^O eq 'MSWin32');
    $HAVE_GZIP     = eval { local $SIG{__DIE__}; require Compress::Zlib; 1; } ? 1 : 0;
    $FH            = 0;

sub new {
# ------------------------------------------------------------------------------
# GT::Tar->new('/path/to/new/tar.tar');
# --------------------------------------
#   Constructor for GT::Tar. Call this method to create a new archive.
#   To do anything with an existing archive call GT::Tar->open.
#
    my $this  = shift;
    my $class = ref $this || $this;
    my $self  = bless {}, $class;

    my $opt = {};
    if (@_ == 1) { $opt->{io} = shift }
    else {
        $opt = $self->common_param(@_);
    }

    $self->{_debug} = exists $opt->{debug} ? $opt->{debug} : $DEBUG;
    $opt->{io} or return $self->error("BADARGS", "FATAL", "new()", "No output archive passed in");

    $opt->{io} =~ /^(.+)$/;
    my $file = $1;

# If it's a gz file, store the name in gz_file, and work off a temp file.
    if ($file =~ /\.t?gz$/) {
        $HAVE_GZIP or return $self->error('NOGZIP', 'WARN');
        require GT::TempFile;
        my $tmpfile = new GT::TempFile;
        $self->{file}     = $$tmpfile;     # Filename of ungzipped tar file.
        $self->{gz_file}  = $file;         # Filename of gzipped file.
        $self->{tmp_file} = $tmpfile;      # Don't unlink it till the object is destroyed.
    }
    else {
        $self->{file} = $file;
    }
    $self->{io} = _gen_fh();
    sysopen $self->{io}, $self->{file}, O_CREAT|O_TRUNC|O_RDWR or return $self->error("OPEN", "FATAL", $self->{file}, "($!)");
    binmode $self->{io} or return $self->error("BINMODE", "FATAL", $self->{file}, "($!)");
    select((select($self->{io}), $| = 1)[0]);

    $self->{parsed}    = 0;
    $self->{new_tar}   = 1;
    return $self;
}

sub open {
# ------------------------------------------------------------------------------
# GT::Tar->open('/path/to/tar.tar');
# -----------------------------------
#   Opens the tar specified by the first argument for reading and calls
#   $obj->parse to parse the contents.
#   Returns a new GT::Tar object.
#
    my $this  = shift;
    my $class = ref $this || $this;
    my $self  = bless {}, $class;

    my $opt = {};
    if (@_ == 1) { $opt->{io} = shift }
    else {
        $opt = $self->common_param(@_);
    }

    $self->{_debug} = exists $opt->{debug} ? $opt->{debug} : $DEBUG;
    $opt->{io} or return $self->error("BADARGS", "FATAL", "open()", "No input archive passed in");

    $opt->{io} =~ /^(.+)$/;
    my $file = $1;

# If it's a gz file, uncompress it to a temp file and work off that.
    if ($file =~ /\.t?gz$/) {
        $HAVE_GZIP or return $self->error('NOGZIP', 'WARN');
        require GT::TempFile;
        my $tmpfile = new GT::TempFile;
        $self->debug("Decompressing gz file to temp file: $$tmpfile") if ($self->{_debug});
        open(FH, "> $$tmpfile") or return $self->error('OPEN', 'WARN', $$tmpfile, "$!");
        binmode FH;
        my $gz = Compress::Zlib::gzopen($file, 'rb') or return $self->error('OPEN', 'WARN', $file, $Compress::Zlib::gzerrno);
        my $line;
        while ($gz->gzreadline($line)) {
            print FH $line;
        }
        close FH;

        $gz->gzclose;
        $self->{file}      = $$tmpfile;     # Filename of open ungzipped tar file.
        $self->{gz_file}   = $file;         # Filename of original gzipped file.
        $self->{tmp_file}  = $tmpfile;      # Don't unlink it till the object is destroyed.
    }
    else {
        $self->{file} = $file;
    }
    $self->{io} = _gen_fh();
    $self->debug("Opening $file") if ($self->{_debug});
    sysopen $self->{io}, $self->{file}, O_RDONLY or return $self->error("OPEN", "WARN", $self->{file}, "($!)");
    binmode $self->{io} or return $self->error("BINMODE", "WARN", "($!)");
    select((select($self->{io}), $| = 1)[0]);

    my $parts = $self->parse;
    defined $parts or return;
    $self->{new_tar} = 0;
    return $self;
}

sub close_tar {
# ------------------------------------------------------------------------------
# Closes the tar file.
#
    my $self = shift;
    $self->{parsed} = 0;
    close $self->{io} if ($self->{io} and fileno($self->{io}));
}
sub DESTROY { my $self = shift; $self->close_tar; }

sub parse {
# ------------------------------------------------------------------------------
# Modified from code in Archive::Tar
# Untar a file, specified by first argument to directories, specified in third
# argument, and set the path to perl, specified in second argument, to all .pl
# and .cgi files
#
    my $self = shift;
    $self->{parts} = [];
    my ($head, $msg);
    my $tar = $self->{io}
        or return $self->error("BADARGS", "FATAL", "parse", "An IO must be defined to parse");

    seek($tar, 0, 0);
    read($tar, $head, 512);

    READLOOP: while (length($head) == 512) {
# End of archive
        last READLOOP if $head eq "\0" x 512;

# Apparently this should really be two blocks of 512 zeroes, but GNU tar
# sometimes gets it wrong. See comment in the source code (tar.c) to GNU cpio.

        my $file = GT::Tar::Parts->format_read($head);

        $self->debug("Looking at $file->{name}") if ($self->{_debug});

        substr($head, 148, 8) = "        ";
        if (unpack("%16C*", $head) != $file->{chksum}) {
            return $self->error('CHECKSUM', 'WARN', $head, $file->{chksum}, $file->{name});
        }

        if ($file->{type} == FILE) {
# Find the start and the end positions in the tar file for the body of the tar
# part
            my $start = tell $tar;
            seek($tar,  $file->{size}, 1);
            $file->body([$tar, $start]);

# Seek off trailing garbage.
            my $block = $file->{size} & 0x01ff ? ($file->{size} & ~0x01ff) + 512 : $file->{size};
            my $to_read = $block - $file->{size};
            if ($to_read) { seek($tar, $to_read, 1) }
        }

# Guard against tarfiles with garbage at the end
        last READLOOP if $file->{name} eq '';

        push(@{$self->{parts}}, $file);

    }
    continue { read($tar, $head, 512) }
    $self->{parsed} = 1;
    seek($tar, 0, 0);
    return wantarray ? @{$self->{parts}} : $self->{parts};
}

sub untar {
# -----------------------------------------------------------------------------
# $obj->untar(\&code);
# ---------------------
#   Untars tar file specified in $obj->open and runs callback for each entry in
#   the tar file. Passed a parts object to that callback.
#
# $obj->untar;
# ------------
#   Same a above but no callback.
#
# GT::Tar->untar('/path/to/tar.tar', \&code);
# --------------------------------------------
#   Untars file specified by the first argument and runs callback in second
#   argument.
#
# GT::Tar->untar('/path/to/tar.tar');
# ------------------------------------
#   Untars tar file specified by first argument.
#
    my $self = (ref $_[0] eq __PACKAGE__) ? shift : shift()->open( shift() );

    my $callback = pop;
    if ($callback) {
        (ref $callback eq 'CODE')
            or return $self->error("BADARGS", "FATAL", "untar", "Callback that was passed in was not a code ref");
    }

    if (!$self->{parsed}) {
        $self->debug("Parsing tar file") if ($self->{_debug});
        $self->parse or return;
    }
    else {
        $self->debug("Already parsed") if ($self->{_debug});
    }

    for (@{$self->{parts}}) {
        if ($callback) {
            $callback->($_);
        }
        else {
            $_->write;
        }
    }
    return $self;
}

sub tar {
# ------------------------------------------------------------------------------
# $obj->tar;
# ----------
#   Creates tar file that was specified in $obj->new with files that were added
#   using $obj->add.
#
# GT::Tar->tar('/path/to/tar.tar', @files);
# ------------------------------------------
#   Creates tar file specified by the first argument with the files specified
#   by the remaining arguments.
#
    my $self;
    if (ref $_[0] eq __PACKAGE__) {
        $self = shift;
    }
    else {
        my $class = shift;
        $self  = $class->new( io => shift );
        $self->add(@_) if (@_);
    }
    $self->write;
}

sub write {
# ------------------------------------------------------------------------------
# $obj->write;
# ------------
#   Creates all the files that are internally in the parts objects.  You add
#   files to parts by calling $obj->add -or- by calling $obj->open on an
#   existing tar file. This is similar to untar.
#
    my $self = shift;
    my ($out, $rename, $filename);

# Working off an existing tar file.
    if (! $self->{new_tar}) {
        if (@_) {
            $filename = shift;

# If we have a new .tar.gz file, we need to write it to a tmp .tar first.
            if ($filename =~ /\.t?gz$/) {
                $HAVE_GZIP or return $self->error('NOGZIP', 'WARN');
                $self->{gz_file} = $filename;
                undef $filename;
            }
        }
        if (! $filename) {
            require GT::TempFile;
            my $tmp = new GT::TempFile;
            $filename = $$tmp;
            $rename   = $self->{file};
        }
        $out = _gen_fh();
        sysopen $out, $filename, O_CREAT|O_TRUNC|O_RDWR or return $self->error("OPEN", "WARN", $filename, "($!)");
        binmode $out or return $self->error('BINMODE', 'FATAL', $filename, "($!)");
    }
# Working off a new tar file.
    else {
        $out = $self->{io};
        seek($out, 0, 0);
    }

# Unbuffer output
    select((select($out), $| = 1)[0]);
    foreach my $entry (@{$self->{parts}}) {
        my $head = $entry->format_write;
        print $out $head;
        my $save = tell $out;
        if ($entry->type == FILE) {
            my $bh;
            my $body = $entry->body or return $self->error('NOBODY', 'WARN', $entry->name);
            my $ref  = ref $body;
            if ($ref eq 'GLOB' and fileno $body) {
                my $fh = $body;
                my $pos  = tell $fh;
                binmode $fh;
                while (read $fh, $_, BLOCK) {
                    print $out $_;
                }
                seek($fh, $pos, 0);
            }
            elsif ($ref eq 'ARRAY') {
                my ($reads, $rem, $data, $pos);
                my ($fh, $start) = @{$body};
                $pos = tell $fh;
                seek($fh, $start, 0);
                binmode $fh;
                $reads = int($entry->{size} / BLOCK);
                $rem   = $entry->{size} % BLOCK;
                for (1 .. $reads) {
                    my $read = read($fh, $data, BLOCK);
                    ($read == BLOCK)
                        or return $self->error("READ", "WARN", join(',' => @{$body}), BLOCK, $read);
                    print $out $data;
                }
                if ($rem) {
                    my $read = read($fh, $data, $rem);
                    ($read == $rem)
                        or return $self->error("READ", "WARN", join(',' => @{$body}), $rem, $read);
                    print $out $data;
                }
                seek($fh, $pos, 0);
            }
            elsif ($ref eq 'SCALAR') {
                CORE::open F, ${$body} or return $self->error('READOPEN', 'WARN', ${$body}, "($!)");
                binmode F;
                while (read F, $_, BLOCK) {
                    print $out $_;
                }
                close F;
            }
            else {
                print $out $body;
            }
            my $size = $entry->{size} & 511;
            if ($size) {
                print $out ("\0" x (512 - $size));
            }
            $entry->body( [ $out, $save ] );
        }
    }
    print $out ("\0" x 1024);

# Copy the temp file over to the original file (can't rename across filesystems).
    if ($rename and !$self->{gz_file}) {
        seek($out, 0, 0);
        $self->{io} = _gen_fh();
        sysopen($self->{io}, $rename, O_CREAT|O_TRUNC|O_RDWR) or return $self->error("OPEN", "WARN", $rename, "($!)");
        binmode $self->{io};
        while (read($out, my $buffer, BLOCK)) {
            print {$self->{io}} $buffer;
        }
        seek($self->{io}, 0, 0);

# Need to set the parts to the new file handle.
        foreach my $entry (@{$self->{parts}}) {
            if ($entry->type == FILE) {
                $entry->{body}->[0] = $self->{io};
            }
        }
        close $out;
        $out = $self->{io};
        $self->{file} = $rename;
        unlink $filename or return $self->error('UNLINK', 'WARN', $filename, "($!)");
    }

# Recompress if it was a .gz file.
    if ($self->{gz_file}) {
        $HAVE_GZIP or return $self->error('NOGZIP', 'WARN');
        seek($out, 0, 0);
        my $gz = Compress::Zlib::gzopen($self->{gz_file}, 'wb') or return $self->error('OPEN', 'WARN', $self->{gz_file}, $Compress::Zlib::gzerrno);
        while (read($out, my $buffer, BLOCK)) {
            $gz->gzwrite($buffer);
        }
        $gz->gzclose();
        seek($out, 0, 0);
    }
    return 1;
}

sub extract {
# ------------------------------------------------------------------------------
# $obj->extract(@list);
# ----------------------
# $obj->extract(\@list);
# -----------------------
#   Extracts only the files specified in @list from the working tar file. No
#   files are extracted if none are in memory.
#
    my $self  = shift;
    my %files = map { $_ => 1 } ref($_[0]) eq 'ARRAY' ? @{$_[0]} : @_;
    my $num = '0E0';
    foreach my $entry (@{$self->{parts}}) {
        next unless (exists $files{$entry->{name}});
        $entry->write;
        $num++;
    }
    return $num;
}

sub add_file {
# ------------------------------------------------------------------------------
# $obj->add_file(@list);
# ------------------
# $obj->add_file(\@list);
# -------------------
#   Adds the files specified in @list to the in-memory archive.
#
    my $self  = shift;
    my @files = ref $_[0] eq 'ARRAY' ? @{$_[0]} : @_;

    while (my $file = shift @files or @files) {
        next if not defined $file;
        my ($mode, $nlnk, $uid, $gid, $rdev, $size, $mtime, $type, $linkname);

        $self->debug("Looking at $file") if ($self->{_debug});
        if (($mode, $nlnk, $uid, $gid, $rdev, $size, $mtime) = (lstat $file)[2 .. 7, 9]) {
            $linkname = "";
            $type = filetype($file);

            $linkname = readlink $file if ($type == SYMLINK);
            if ($type == DIR) {
                my $dir = _gen_fh();
                opendir $dir, $file or return $self->error("OPEN", "WARN", "Can't add directory '$file'", "($!)");
                push(@files, map { $file . '/' . $_ } grep !/^\.\.?$/, readdir $dir);
                closedir $dir;
            }

            my $part = GT::Tar::Parts->new(
                {
                    name     => $file,
                    mode     => $mode,
                    uid      => $uid,
                    gid      => $gid,
                    size     => $size,
                    mtime    => ($mtime | 0),
                    chksum   => "      ",
                    magic    => "ustar",
                    version  => "",
                    type     => $type,
                    linkname => $linkname,
                    devmajor => 0, # We don't handle this yet
                    devminor => 0, # We don't handle this yet
                    uname    => ($FAKE_GETPWUID || scalar getpwuid($uid)),
                    gname    => ($FAKE_GETGRGID || scalar getgrgid($gid)),
                    prefix   => "",
                }
            );
            if ($type == FILE) {
                $self->debug("Adding $file to as body") if ($self->{_debug});
                $part->body(\$file);
            }
            push(@{$self->{parts}}, $part);

        }
        else {
            $self->debug("Could not stat file '$file'");
        }
    }
    return wantarray ? @{$self->{parts}} : $self->{parts};
}

sub remove_file {
# -------------------------------------------------------------------
# Takes a string and removes the file from the tar.
#
    my ($self, $filename) = @_;
    return unless (defined $filename);
    @{$self->{parts}} = grep { $_->{name} ne $filename } @{$self->{parts}};
}

sub get_file {
# -------------------------------------------------------------------
# Returns the file object of a given file name.
#
    my ($self, $filename) = @_;
    return unless (defined $filename);
    my @files = grep { $_->{name} eq $filename } @{$self->{parts}};
    if (! @files) {
        return $self->error('CANTFIND', 'WARN', $filename);
    }
    return wantarray ? @files : shift @files;
}

sub add_data {
# -------------------------------------------------------------------
# $obj->add_newfile( { ... } );
# ------------------------------
#   Adds a file from a hash ref of part attributes.
#
    my $self = shift;
    my $part = @_ > 1 ? {@_} : shift;
    ref $part eq 'HASH' or return $self->error('BADARGS', 'FATAL', "Usage: \$obj->add_newfile( part options )");

    defined $part->{name} or return $self->error('BADARGS', 'FATAL', "You must supply a file name.");
    defined $part->{body} or return $self->error('BADARGS', 'FATAL', "You must supply a body for the file.");

    if (ref $part->{body}) {
        if (fileno $part->{body}) {
            local $/;
            my $fh = $part->{body};
            $part->{body} = <$fh>;
        }
        else {
            return $self->error('BADARGS', 'FATAL', "You must supply either a scalar or a file handle to body");
        }
    }
    my $file = GT::Tar::Parts->new({
        name     => $part->{name},
        mode     => defined $part->{mode}  ? $part->{mode} : 0666 & (0777 - umask),
        uid      => defined $part->{uid}   ? $part->{uid}  : $>,
        gid      => defined $part->{gid}   ? $part->{gid}  : (split(/ /,$)))[0],
        size     => length $part->{body},
        mtime    => defined $part->{mtime} ? $part->{mtime} : time,
        chksum   => "      ",
        magic    => "ustar",
        version  => "00",
        type     => FILE,
        linkname => '',
        devmajor => 0, # We don't handle this yet
        devminor => 0, # We don't handle this yet
        uname    => ($FAKE_GETPWUID || scalar getpwuid(defined $part->{uid} ? int($part->{uid}) : $>)),
        gname    => ($FAKE_GETGRGID || scalar getgrgid(defined $part->{gid} ? int($part->{gid}) : (split(/ /,$)))[0])),
        prefix   => ""
    });
    $file->body($part->{body});
    push(@{$self->{parts}}, $file);
    return $file;
}

sub files {
# ------------------------------------------------------------------------------
# my @files = $obj->files;
# ------------------------
#   Returns a list of the part objects that are in the in-memory archive.
#   Returns an array ref in scalar context.
#
    my @parts = defined $_[0]->{parts} ? @{$_[0]->{parts}} : ();
    return wantarray ? @parts : \@parts;
}

sub filetype {
# ------------------------------------------------------------------------------
# Internal method. filetype -- Determine the type value for a given file
#
    my $file = shift;

    return SYMLINK  if (-l $file);  # Symlink
    return FILE     if (-f _);      # Plain file
    return DIR      if (-d _);      # Directory
    return FIFO     if (-p _);      # Named pipe
    return SOCKET   if (-S _);      # Socket
    return BLOCKDEV if (-b _);      # Block special
    return CHARDEV  if (-c _);      # Character special
    return UNKNOWN; # Something else (like what?)
}

sub _gen_fh {
# -------------------------------------------------------------------
# Return a file handle symbol.
#
    no strict 'refs';
    return *{"FH" . $FH++};
}

package GT::Tar::Parts;
# ==================================================================
# Pragmas
    use vars qw/$DEBUG $ERRORS $ATTRIBS $ERROR_MESSAGE/;
    use strict;

# System modules
    use Fcntl;

# Globals
    $DEBUG = 0;
    @GT::Tar::Parts::ISA = qw{GT::Base};
    $ATTRIBS = {
         name      => '',
         mode      => '',
         uid       => '',
         gid       => '',
         size      => '',
         mtime     => '',
         chksum    => "      ",
         type      => '',
         linkname  => '',
         magic     => "ustar",
         version   => "00",
         uname     => 'unknown',
         gname     => 'unknown',
         devmajor  => 0, # We don't handle this yet
         devminor  => 0, # We don't handle this yet
         prefix    => "",
         body      => undef,
         set_owner => 1,
         set_perms => 1,
         set_time  => 1,
    };
    $ERROR_MESSAGE = 'GT::Tar';

sub format_read {
# ------------------------------------------------------------------------------
# my $obj = GT::Tar::Parts->format_read($heading);
# -------------------------------------------------
#   Unpacks the string that is passed in. The string need to be a valid header
#   from a single entry in a tar file. Return a new object for the Tar part.
#   You will need to set the body yourself after calling this.
#
    my $head_tainted = pop;
    my ($head) = $head_tainted =~ /(.+)/;
    my $tar_unpack_header = 'A100 A8 A8 A8 A12 A12 A8 A1 A100 A6 A2 A32 A32 A8 A8 A155';
    my $file = {};
    (
        $file->{name},     $file->{mode},
        $file->{uid},      $file->{gid},
        $file->{size},     $file->{mtime},
        $file->{chksum},   $file->{type},
        $file->{linkname}, $file->{magic},
        $file->{version},  $file->{uname},
        $file->{gname},    $file->{devmajor},
        $file->{devminor}, $file->{prefix}
    ) = unpack($tar_unpack_header, $head);

    $file->{uid}      = oct $file->{uid};
    $file->{gid}      = oct $file->{gid};
    $file->{mode}     = oct $file->{mode};
    $file->{size}     = oct $file->{size};
    $file->{mtime}    = oct $file->{mtime};
    $file->{chksum}   = oct $file->{chksum};
    $file->{devmajor} = oct $file->{devmajor};
    $file->{devminor} = oct $file->{devminor};
    $file->{name}     = $file->{prefix} . "/" . $file->{name} if $file->{prefix};
    $file->{prefix}   = "";

    $file->{type} = GT::Tar::DIR
        if $file->{name} =~ m|/$| and $file->{type} == GT::Tar::FILE;
    return GT::Tar::Parts->new($file);
}

sub format_write {
# ------------------------------------------------------------------------------
# $obj->format_write;
# -------------------
#   Formats the current objects header for writting to a tar file.
#   Returns the formatted string.
#
    my $self = shift;
    my ($tmp, $file, $prefix, $pos);

    $file = $self->{name};
    if (length($file) > 99) {
        $pos = index $file, "/", (length($file) - 100);
        next if $pos == -1;  # Filename longer than 100 chars!

        $prefix = substr $file, 0, $pos;
        $file   = substr $file, $pos+1;
        substr($prefix, 0, -155) = "" if length($prefix) > 154;
    }
    else {
        $prefix = "";
    }
    if ($self->{type} == GT::Tar::DIR and $file !~ m,/$,) {
        $file .= '/';
    }
    $tmp = pack(
        'a100 a8 a8 a8 a12 a12 A8 a1 a100 a5 a3 a32 a32 a8 a8 a155 x12',
        $file,
        sprintf("%07o",$self->{mode}),
        sprintf("%07o",$self->{uid}),
        sprintf("%07o",$self->{gid}),
        sprintf("%011o", $self->{type} == GT::Tar::DIR ? 0 : $self->{size}),
        sprintf("%011o",$self->{mtime}),
        "",        #checksum field - space padded by pack("A8")
        $self->{type},
        $self->{linkname},
        $self->{magic},
        $self->{version} || '  ',
        $self->{uname},
        $self->{gname},
        '', # sprintf("%6o ",$self->{devmajor}),
        '', # sprintf("%6o ",$self->{devminor}),
        $prefix
    );
    substr($tmp, 148, 7) = sprintf("%06o\0", unpack("%16C*", $tmp));
    return $tmp;
}

sub body {
# ------------------------------------------------------------------------------
# my $path = $obj->body;
# ----------------------
# $obj->body(\'/path/to/body');
# $obj->body("My body text.");
# -----------------------------
#   Sets or gets the path to the body of this tar part. If a scalar ref is
#   passed in it is considered a path to a file otherwize it is considered a
#   string to write to the body when write is called.
#
    my ($self, $io) = @_;
    !$io and return $self->{body};
    $self->{body} = $io;
    my $ref = ref $io;
    if ($ref eq 'GLOB' and fileno $io) {
        $self->{size} = (lstat(${$self->{body}}))[7];
    }
    elsif ($ref eq 'SCALAR') {
        $self->{size} = -s ${$self->{body}};
    }
    elsif (not $ref) {
        $self->{size} = length $self->{body};
    }

    return $self->{body};
}

sub body_as_string {
# ------------------------------------------------------------------------------
# my $data = $obj->body_as_string;
# --------------------------------
#   Returns the body of the file as a string.
#
    my $self = shift;
    my $data = '';
    my $ref  = ref $self->{body};
    if ($ref eq 'GLOB' and fileno $self->{body}) {
        my $fh = $self->{body};
        my $pos = tell $fh;
        seek($fh, 0, 0);
        binmode $fh;
        local $/;
        $data = <$fh>;
        seek($fh, $pos, 0);
    }
    elsif ($ref eq 'ARRAY') {
        my ($fh, $start) = @{$self->{body}};
        my $pos = tell $fh;
        binmode $fh;
        seek($fh, $start, 0);
        read($fh, $data, $self->{size});
        seek($fh, $pos, 0);
    }
    elsif ($ref eq 'SCALAR') {
        my $fh = _gen_fh();
        open $fh, ${$self->{body}} or return $self->error('READOPEN', 'WARN', ${$self->{body}}, "($!)");
        binmode $fh;
        read($fh, $data, -s $fh);
        close $fh;
    }
    else {
        $data = $self->{body};
    }
    return $data;
}

sub write {
# ------------------------------------------------------------------------------
# $obj->write;
# ------------
#   Writes this part to disk using the path that is in $obj->body. This function
#   will recursivlty make the directories needed to create the structure of this
#   part.
#
    my $self = shift;

# For the moment, we assume that all paths in tarfiles are given according to
# Unix standards, which they *are*, according to the tar format spec!
    $self->_write_dir or return;
    if ($self->{type} == GT::Tar::FILE) {
        my $out = GT::Tar::_gen_fh();
        $self->{name} =~ /^(.+)$/;
        my $name = $1;
        open $out, ">$self->{name}" or return $self->error("OPEN", "WARN", $self->{name}, "($!)");
        binmode $out or return $self->error("BINMODE", "WARN", "($!)");
        my $ref  = ref $self->{body};
        if ($ref eq 'GLOB' and fileno $self->{body}) {
            my $fh = $self->{body};
            my $pos = tell $fh;
            binmode $fh;
            while (read $fh, $_, GT::Tar::BLOCK) {
                print $out $_;
            }
            seek($fh, $pos, 0);
        }
        elsif ($ref eq 'ARRAY') {
            my ($reads, $rem, $data, $pos);
            my ($fh, $start) = @{$self->{body}};
            $pos = tell $fh;
            seek($fh, $start, 0);
            binmode $fh;
            $reads = int($self->{size} / GT::Tar::BLOCK);
            $rem   = $self->{size} % GT::Tar::BLOCK;
            for (1 .. $reads) {
                my $read = read($fh, $data, GT::Tar::BLOCK);
                ($read == GT::Tar::BLOCK)
                    or return $self->error("READ", "WARN", join(',' => @{$self->{body}}), GT::Tar::BLOCK, $read);
                print $out $data;
            }
            if ($rem) {
                my $read = read($fh, $data, $rem);
                ($read == $rem)
                    or return $self->error("READ", "WARN", join(',' => @{$self->{body}}), $rem, $read);
                print $out $data;
            }
            seek($fh, $pos, 0);
        }
        elsif ($ref eq 'SCALAR') {
            my $fh = GT::Tar::_gen_sym();
            open $fh, ${$self->{body}} or return $self->error('READOPEN', 'WARN', ${$self->{body}}, "($!)");
            binmode $fh;
            while (read $fh, $_, GT::Tar::BLOCK) {
                print $out $_;
            }
            close $fh;
        }
        else {
            print $out $self->{body};
        }
        close $out;
        $self->debug("Created $self->{name} size $self->{size}") if ($self->{_debug});
    }
    $self->_file_sets;

    return 1;
}

sub _recurse_mkdir {
# ---------------------------------------------------------------------
# Internal method to recursivly make a directory.
#
    my ($self) = @_;
    my $dir    = $self->{name};
    my @path   = split m|/|, $dir;
    ($dir      =~ m,/$,) or pop(@path);
    my $go     = '';
    foreach my $path (@path) {
        next if $path =~ /^\s*$/;
        $go .= $path;
        $go .= '/' unless $go =~ m,/$,;
        ($go = '/' . $go) if ($dir =~ m,^/, and $go !~ m,^/,);
        (my $next = $go) =~ s,/$,,;
        ((-e $next) and (not -d $next)) and return $self->error("DIRFILE", "FATAL", $self->{name});
        unless (-d $next) {
            mkdir($next, 0777) or return $self->error("MKDIR", "WARN", $next, "($!)");
            $self->debug("mkdir $next") if ($DEBUG);
        }
    }
    return 1;
}

sub _write_dir {
# ------------------------------------------------------------------------------
# Internal method used to create a directory for a file, or just create a
# directory if this is a directory part and the directory does not exist.
    my $self = shift;

    if ($self->{type} == GT::Tar::DIR) {
        ((-e $self->{name}) and (not -d $self->{name}))
            and return $self->error("DIRFILE", "FATAL", $self->{name});
        unless (-d $self->{name}) {
            $self->_recurse_mkdir or return;
        }
    }
    else {
        $self->_recurse_mkdir or return;
    }
    return 1;
}

sub _file_sets {
# ------------------------------------------------------------------------------
# Internal method to set the file or directory permissions and or onership of
# this part.
#
    my $self = shift;

# Set the file creation time.
    if ($self->{set_time}) {
        utime time, $self->{mtime}, $self->{name};
    }

# Set the file owner.
    if ($self->{set_owner}) {
        $self->debug("chown ($self->{uid},$self->{gid}) $self->{name}") if ($self->{_debug});
        chown($self->{uid}, $self->{gid}, $self->{name})
            if ($> == 0 and $^O ne "MacOS" and $^O ne "MSWin32");
    }

# Set the permissions (done last in case it makes file readonly)
    if ($self->{set_perms}) {
        my ($mode) = sprintf("%lo", $self->{mode}) =~ /(\d{3})$/;
        $self->debug("chmod $mode, $self->{name}") if ($self->{_debug});
        chmod $self->{mode}, $self->{name} or return $self->error("CHMOD", "WARN", $self->{name}, "($!)");
    }

    return 1;
}

1;

__END__

=head1 NAME

GT::Tar - Perl module to manipulate tar files.

=head1 SYNOPSIS

    use GT::Tar;
    my $tar = GT::Tar->open('foo.tar');
    $tar->add_file( '/path/to/file' );
    $tar->write;

=head1 DESCRIPTION

GT::Tar provides an OO intefrace to a tar file. It allows you to create or edit
tar files, and if you have Compress::Zlib installed, it allows you to work with
.tar.gz files as well!

=head2 Creating a tar file

To create a tar file, you simply call:

    my $tar = new GT::Tar;

and then to save it:

    $tar->write('filename.tar');

will save the tar file and any files you have added.

=head2 Opening an existing tar file

To open a tar file you call:

    my $tar = GT::Tar->open('/path/to/file.tar')
        or die "Can't open: $GT::Tar::error";

Note: the tar object keeps an open filehandle to the file, so if you are on
windows, you may not be able to manipulate it until you call $tar->close_tar, or
the tar object goes out of scope.

=head2 Untarring a tar file

To untar a tar file, you can simply call:

    $tar->untar( \&code_ref );

or as a class method

    GT::Tar->untar('/path/to/tar.tar', \&code_ref );

The code ref is optional. If provided, you will get passed in the a
GT::Tar::Part object before the file is extracted. This lets you change the
path, or alter any attributes of the file before it is saved to disk.

=head2 Adding files to a tar file

To add a file:

    $tar->add_file( '/path/to/file' );

Note, if you add a directory, the tar module will recurse and add all files in
that directory.

To add a file that isn't saved:

    $tar->add_data( name => 'Filename', body => 'File body' );

You can pass in either a scalar for the body, or an opened file handle.

=head2 Getting a list of files in a tar

To get a list of files in a tar:

    my $files = $tar->files;

This returns an array ref of GT::Tar::Part objects. See below for how to access
information from a part.

Note: if you change a part, it will update the tar file if you save it.

=head2 Getting an individual file from a tar

If you know the name of the file you want:

    my $file = $tar->get_file('Filename');

will return a single GT::Tar::Part object.

=head2 Removing a file from a tar

To remove a file, you need to know the name of it:

    $tar->remove_file('Filename');
    $tar->write;

and you need to save it before the change will take affect.

=head2 GT::Tar::Part

Each file is a separate part object. The part object has the following
attributes:

    name    file name
    mode    file permissions
    uid     user id
    gid     group id
    size    file size
    mtime   last modified time
    type    file type
    body    file body

You can access or set any of these attributes by just using the attribute name
as the method (as it inherits from L<GT::Base>).

You can also call:

    $file->write;

and the file will be created with the given attributes. Basically untar just
foreach's through each of the objects and calls write() on it.

=head1 EXAMPLES

To create a new tar and add two directories to it, and save it in
'/tmp/foo.tar';

    my $tar = new GT::Tar;
    $tar->add_file( '/home/httpd/html' );
    $tar->add_file( '/home/backup' );
    $tar->write('/tmp/foo.tar');

To open an existing tar file and save all the .pl files in /home/alex.

    my $tar = GT::Tar->open('files.tar');
    my $files = $tar->files;
    foreach my $file (@$files) {
        my $name = $file->name;
        if ($name =~ m,([^/]*\.pl$),) {
            $file->name( "/home/alex/$1" );
            $file->write;
        }
    }

=head1 COPYRIGHT

Copyright (c) 2002 Gossamer Threads Inc.  All Rights Reserved.
http://www.gossamer-threads.com/

=head1 VERSION

Revision: $Id: Tar.pm,v 1.46 2002/04/07 03:35:35 jagerman Exp $

=cut