#!/usr/bin/perl ### snap ################################################### # # This is the package management script for snap. It's # quite a hack, and a bit messy currently. This will be # cleaned up in time, but the initial goal is just to # create something that works. It's not really too big # to clean up later. # # Things to do! # - Clean up $target, make sure it is correct for all subs # - add 'usher' support (script executed from package): # * built into ar archive # * has preinst, postinst, prerm, postrm args # ############################################################ use strict; use warnings; use IPC::Open3; use IO::Select; use IO::Socket; use Compress::Zlib; use Digest::SHA qw( sha256_hex ); use Data::Dumper; my $snapver; my $conffile = '/etc/snap.conf'; my $conf = readconf( $conffile ); my @opts = ( 'info', 'list', 'hash', 'install', 'installed', 'reinstall', 'refresh', 'remove', 'search' ); open( FILE, "; close( FILE ); chomp( $snapver ); sub readconf { my $file = shift; my $section = ''; my %data; open( FILE, "<$file" ) || die( "open: $file: $!\n" ); while ( ) { chomp( $_ ); if ( $_ =~ /\[(\S+)\]/ ) { $section = $1; next; } elsif ( $_ =~ /(\S+)\s*=\s*(.*)$/ ) { $data{$section}{$1} = $2; } } close( FILE ); if ( ! $data{'general'}{'snapdir'} ) { die( "readconf: $file: snapdir is required under [general]\n" ); } elsif ( ! $data{'general'}{'pkgfile'} ) { die( "readconf: $file: pkgfile is required under [general]\n" ); } elsif ( ! $data{'sources'} ) { die( "readconf: $file: no sources defined\n" ); } return( \%data ); } sub runcmd{ my $cmd = shift; my %runcmd = ( sel => IO::Select->new(), pid => 0, stat => 0, fh_out => '', fh_err => '' ); $runcmd{'pid'} = open3( \*CHLD_STDIN, \*CHLD_STDOUT, \*CHLD_STDERR, $cmd ); close( CHLD_STDIN ); $runcmd{'sel'}->add( *CHLD_STDOUT, *CHLD_STDERR ); $runcmd{'fh_out'} = *CHLD_STDOUT; $runcmd{'fh_err'} = *CHLD_STDERR; return( %runcmd ); } sub info{ my $pkgfile = shift; my $target = shift || ''; my $pkgdir = "$target/$conf->{'general'}{'snapdir'}/$pkgfile"; my $cmd = "ar p $pkgfile snapinfo"; my %result = ( status => 0, stdout => '', stderr => '', info => { package => '', version => '', depends => '', arch => '', bytes => 0, url => '', description => '' } ); my $regex = '^(' . 'package' . '|version' . '|depends' . '|arch' . '|bytes' . '|url' . '|description' . '):\s+(.*)'; my %runcmd; my $el; if ( ! -f $pkgfile && -f "$pkgdir/snapinfo" ){ my $snapinfo = "$pkgdir/snapinfo"; open( SNAPINFO, "<$snapinfo" ); while ( my $line = ){ if ( $line =~ /$regex/ ){ $el = $1; $result{'info'}{$el} = $2; } elsif ( $el ){ $result{'info'}{$el} .= $line; } } return( \%result ); } elsif ( ! -f $pkgfile ){ $result{'stderr'} = "No such package ($pkgfile) found"; $result{'status'} = -1; return( \%result ); } %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ foreach my $fh ( @fhs ){ if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); next; } if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ while ( my $line = <$fh> ){ $result{'stdout'} .= $line; chomp( $line ); if ( $line =~ /$regex/ ){ $el = $1; $result{'info'}{$el} = $2; } elsif ( $el ){ $result{'info'}{$el} .= $line; } } } elsif ( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ $result{'stderr'} .= <$fh>; } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); return( \%result ); } sub list{ my $pkgfile = shift; my $target = shift || ''; my $pkgdir = "$target/$conf->{'general'}{'snapdir'}/$pkgfile"; my $cmd = "ar p $pkgfile manifest"; my %result = ( status => 0, stdout => '', stderr => '', list => [] ); my %runcmd; if ( ! -f $pkgfile && -f "$pkgdir/manifest" ){ my $manifest = "$pkgdir/manifest"; open( MANIFEST, "<$manifest" ); while ( my $line = ){ my ( $sha256, $perms, $file ) = split( /\s+/, $line ); my $type = substr( $perms, 0, 1 ); if ( $file =~ /^\.{1,2}$/ ){ next; } push( @{$result{'list'}}, { sha256 => $sha256, perms => $perms, file => $file, type => $type } ); } close( MANIFEST ); return( \%result ); } %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ foreach my $fh ( @fhs ){ if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); next; } if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ while ( my $line = <$fh> ){ my $sha256; my $perms; my $file; my $type; chomp( $line ); ( $sha256, $perms, $file ) = split( /\s/, $line ); $type = substr( $perms, 0, 1 ); push( @{$result{'list'}}, { sha256 => $sha256, perms => $perms, file => $file, type => $type } ); } } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ $result{'stderr'} .= <$fh>; } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); return( \%result ); } sub installed{ my $target = shift || ''; my $pkgsdir = "$target/$conf->{'general'}{'snapdir'}"; my %result; opendir( DIR, $pkgsdir ) || die( "Error: $!" ); while ( my $file = readdir( DIR ) ){ if ( $file =~ /^\.{1,2}$/ || ! -d "$pkgsdir/$file" ){ next; } if ( -f "$pkgsdir/$file/snapinfo" ){ open( SNAPINFO, "<$pkgsdir/$file/snapinfo" ) || die( "Error: $!" ); while ( my $line = ){ if ( $line =~ /^package:\s*(\S+)$/ ){ $result{$file}{'package'} = $1; } elsif ( $line =~ /^version:\s*(\S+)$/ ){ $result{$file}{'version'} = $1; } elsif ( $line =~ /^bytes:\s*(\S+)$/ ){ $result{$file}{'bytes'} = $1; } elsif ( $line =~ /^description:\s*(.*)$/ ){ $result{$file}{'description'} = $1; } } close( SNAPINFO ); } } close( DIR ); return( \%result ); } sub repo { my %packages; foreach my $source ( keys( %{$conf->{'sources'}} ) ) { my $file = "/$conf->{'general'}{'snapdir'}/$source-packages.gz"; my $data = gzopen( $file, 'r' ); my $buffer; my $lastpkg; while ( $data->gzreadline( $buffer ) > 0 ) { if ( $buffer =~ /^package:\s+(.*)$/ ) { $lastpkg = $1; } elsif ( $buffer =~ /^version:\s+(.*)$/ ) { $packages{$lastpkg}{'version'} = $1; } elsif ( $buffer =~ /^depends:\s+(.*)$/ ) { $packages{$lastpkg}{'depends'} = $1; } elsif ( $buffer =~ /^bytes:\s+(.*)$/ ) { $packages{$lastpkg}{'bytes'} = $1; } elsif ( $buffer =~ /^description:\s+(.*)$/ ) { $packages{$lastpkg}{'description'} = $1; } elsif ( $buffer =~ /^path:\s+(.*)$/ ) { $packages{$lastpkg}{'path'} = $1; } elsif ( $buffer =~ /^sha256:\s+(.*)$/ ) { $packages{$lastpkg}{'sha256'} = $1; } } $data->gzclose(); } return( \%packages ); } sub hash{ my $pkgfile = shift; my $result = { status => 0, stdout => '', stderr => '', hash => '' }; if ( ! -f $pkgfile ){ $result->{'status'} = 1; $result->{'stderr'} = "'$pkgfile' not a valid package file"; return( $result ); } open( PKG, "$pkgfile" ); $result->{'hash'} = sha256_hex( ); close( PKG ); return( $result ); } sub human{ my $B = shift; my $human; if ( $B > 1099511627776 ){ $human = sprintf( '%.02f', $B / ( 1024 ** 4 ) ) . 'TB'; } elsif ( $B > 1073741824 ){ $human = sprintf( '%.02f', $B / ( 1024 ** 3 ) ) . 'GB'; } elsif ( $B > 1048576 ){ $human = sprintf( '%.02f', $B / ( 1024 ** 2 ) ) . 'MB'; } else{ $human = sprintf( '%.02f', $B / 1024 ) . 'KB'; } return( $human ); } sub conflicts{ my $pkgfile = shift; my $target = shift || ''; my $infodata = info( $pkgfile, $target ); my $listdata = list( $pkgfile ); my $snapdir = "$target/$conf->{'general'}{'snapdir'}"; my @conflicts; print "Checking for conflicts...\n"; opendir( DIR, $snapdir ) || die( $! ); #################################################### # # Need to re-write this a little more efficiently.. # Should we design a SQLite DB to hold this data?? # # It would be preferable to stick with flat files # and directories for simplicity... # #################################################### while ( my $pkgdir = readdir( DIR ) ){ if ( ! -d "$snapdir/$pkgdir" || $pkgdir =~ /^\.{1,2}$/ || $infodata->{'info'}{'package'} eq $pkgdir ){ next; } if ( ! -f "$snapdir/$pkgdir/manifest" ) { next; } open( MANIFEST, "<$snapdir/$pkgdir/manifest" ) || die( $! ); while ( my $line = ){ ( my $file = $line ) =~ s/.*\t//g; chomp( $file ); foreach my $data ( @{$listdata->{'list'}} ){ if ( $data->{'file'} eq $file && $data->{'type'} ne 'd' ){ push( @conflicts, "$pkgdir: $file" ); last; } } } } if ( @conflicts ){ return( \@conflicts ); } else{ return 0; } } sub depends { my $packages = shift; my $package = shift; my $depends = shift; if ( $packages->{$package} && $packages->{$package}{'depends'} ) { foreach my $depend ( split( ',', $packages->{$package}{'depends'} ) ) { depends( $packages, $depend, $depends ); push( @$depends, $depend ); } } } sub mkinfo{ my $target = shift || ''; my $infodir = "$target/usr/share/info"; opendir( INFODIR, "<$infodir" ); while ( my $file = readdir( INFODIR ) ){ if ( ! -f $file ){ next; } system( "cd $infodir && install-info $file dir" ) || return( 1 ); } } sub chkempty{ my $dir = shift; my $empty = 1; if ( ! -d $dir ) { return; } opendir( DIR, $dir ) || die( $! ); while ( my $file = readdir( DIR ) ){ if ( $file =~ /^\.{1,2}$/ ){ next; } else{ $empty = 0; last; } } return( $empty ); } sub usher{ my $pkgfile = shift; my $target = shift || ''; my $stage = shift; my $snapdir; my $pkgdir; my $infodata; my $usher; my %runcmd; my $cmd = "ar -t $pkgfile"; my %result = ( status => 0, stdout => '', stderr => '' ); $snapdir = "$target/$conf->{'general'}{'snapdir'}"; $infodata = info( $pkgfile, $target ); $pkgdir = "$snapdir/$infodata->{'info'}{'package'}"; %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ local $| = 1; foreach my $fh ( @fhs ){ if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ while ( my $line = <$fh> ){ chomp( $line ); if ( $line eq 'usher' ){ $usher = "$pkgdir/usher"; } } } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ if ( ! $result{'stderr'} ){ $result{'stderr'} = <$fh>; } else{ $result{'stderr'} .= <$fh>; } } if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; if ( $result{'stdout'} ){ chomp( $result{'stdout'} ); } if ( $result{'stderr'} ){ chomp( $result{'stderr'} ); } if ( $result{'status'} || ! $usher ){ return( \%result ); } if ( ! -d $pkgdir ){ mkdir( $pkgdir ); } undef( $result{'stdout'} ); undef( $result{'stderr'} ); $cmd = "ar -p $pkgfile usher > $usher"; %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ local $| = 1; foreach my $fh ( @fhs ){ if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ if ( ! $result{'stdout'} ){ $result{'stdout'} = <$fh>; } else{ $result{'stdout'} .= <$fh>; } } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ if ( ! $result{'stderr'} ){ $result{'stderr'} = <$fh>; } else{ $result{'stderr'} .= <$fh>; } } if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; if ( $result{'stdout'} ){ chomp( $result{'stdout'} ); } if ( $result{'stderr'} ){ chomp( $result{'stderr'} ); } if ( $result{'status'} ){ return( \%result ); } undef( $result{'stdout'} ); undef( $result{'stderr'} ); chmod( 0700, $usher ) || do{ $result{'status'} = 1; $result{'stderr'} = $!; return( \%result ); }; if ( $target ) { $cmd = "chroot $target && $usher $stage"; } else { $cmd = "$usher $stage"; } %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ local $| = 1; foreach my $fh ( @fhs ){ if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ my $line = <$fh>; if ( $line ){ print STDOUT $line; } } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ my $line = <$fh>; if ( $line ){ print STDERR $line; } } if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; if ( $result{'stdout'} ){ chomp( $result{'stdout'} ); } if ( $result{'stderr'} ){ chomp( $result{'stderr'} ); } return( \%result ); } sub httpget { my $url = shift; my $dest = shift; my $mode = shift; ( my $host = $url ) =~ s/^https?:\/\/|\/.*//g; ( my $file = $url ) =~ s/.*$host//; my %httpget = ( 'status' => '', 'length' => 0, 'type' => '', 'data' => '', 'dflag' => 0 ); my $sock = IO::Socket::INET->new( PeerAddr => $host, PeerPort => 'http(80)', Proto => 'tcp' ) || die( $! ); my %result = ( status => 0, stdout => '', stderr => '' ); $sock->send("GET $file HTTP/1.0\r\n"); $sock->send("Host: $host\r\n"); $sock->send("\r\n"); if ( $dest && $mode ) { open( DEST, ">$dest" ) || do{ $result{'status'} = 1; $result{'stderr'} = "open: $dest: $!"; return( \%result ); }; chmod( $mode, $dest ) || do{ $result{'status'} = 1; $result{'stderr'} = "chmod: $dest: $!"; return( \%result ); } } elsif ( $dest && ! $mode ) { die( "httpget: Provided dest ($dest) without mode!\n" ); } while ( <$sock> ) { if ( ! $httpget{'dflag'} ) { if ( ! $httpget{'status'} && $_ =~ /^HTTP\S+\s(\d+)/ ) { $httpget{'status'} = $1; } elsif ( ! $httpget{'date'} && $_ =~ /^Date:\s+(.*)/ ) { $httpget{'date'} = $1; } elsif ( ! $httpget{'server'} && $_ =~ /^Server:\s+(.*)/ ) { $httpget{'server'} = $1; } elsif ( ! $httpget{'lastmod'} && $_ =~ /^Last-Modified:\s+(.*)/ ) { $httpget{'lastmod'} = $1; } elsif ( ! $httpget{'etag'} && $_ =~ /^ETag:\s+(.*)/ ) { $httpget{'etag'} = $1; } elsif ( ! $httpget{'length'} && $_ =~ /Content-Length:\s+(\d+)/ ) { $httpget{'length'} = $1; } elsif ( ! $httpget{'type'} && $_ =~ /Content-Type:\s+(\S+)/ ) { $httpget{'type'} = $1; } elsif( $_ eq "\r\n" ) { $httpget{'dflag'}++; } next; } if ( ! $dest ) { $httpget{'data'} .= $_; } else { print DEST $_; } } close( $sock ); return( \%httpget ); } sub install{ my $pkgfile = shift; my $target = shift || ''; my $snapdir = "$target/$conf->{'general'}{'snapdir'}"; my $infodata; my $confirm; my $cmd; my %runcmd; my $install; my $package; my $version; my $snapinfo; my $manifest; my $filenum = 0; my $type = 0; my $mkinfo = 0; my $usher; my %result = ( status => 0, stdout => '', stderr => '' ); if ( ! -d $snapdir ) { mkdirp( $snapdir, 0755 ) || die( $! ); } $infodata = info( $pkgfile ); if ( $infodata->{'status'} ) { $result{'status'} = $infodata->{'status'}; $result{'stdout'} = $infodata->{'stdout'}; $result{'stderr'} = $infodata->{'stderr'}; return( \%result ); } $package = $infodata->{'info'}->{'package'}; $version = $infodata->{'info'}->{'version'}; $snapinfo = "$snapdir/$package/snapinfo"; $manifest = "$snapdir/$package/manifest"; print "Preparing to install $package-$version...\n"; if ( my $conflicts = conflicts( $pkgfile, $target ) ){ $result{'status'} = 1; $result{'stderr'} = "Package $package conflicts" . " with the following packages/files:\n" . join( "\n", @$conflicts ); return( \%result ); } #################################################### # # If the package is already installed check the # version and return if it is already installed # #################################################### if ( -f $snapinfo ){ open( SNAPINFO, "<$snapinfo" ) || die( $! ); while( my $line = ){ if ( $line =~ /^version: (\S+)/ ){ my @sorted = sort( { vercmp( $a, $b ) } ( $1, $version ) ); if ( $sorted[0] eq $sorted[1] ){ $result{'status'} = -1; $result{'stderr'} = "$package $version" . " already installed"; return( \%result ); } elsif ( $version eq $sorted[0] ){ $type = -1; } elsif ( $version eq $sorted[1] ){ $type = 1; } last; } } close( SNAPINFO ); } if ( ! $confirm ){ my $size = human( $infodata->{'info'}->{'bytes'} ); print "Package: $infodata->{'info'}->{'package'}\n" . "Version: $infodata->{'info'}->{'version'}\n" . "Size: $size\n"; if ( $type == -1 ){ print "Downgrade $package to $version on $target?" . "(y/n): "; } elsif ( $type == 1 ){ print "Upgrade $package to $version on $target?" . "(y/n): "; } else{ print "Install $package on $target? (y/n): "; } } while( ! $confirm ){ $confirm = ; chomp( $confirm ); if ( lc( $confirm ) eq 'n' ){ print STDERR "Aborting installation\n"; exit 1; } elsif ( lc( $confirm ne 'y' ) ){ print "Answer 'y' or 'n': "; undef( $confirm ); } } #################################################### # # Here we copy the old manifest (if present) to a # temp file to compare after installation and clean # up any files from the old package that don't exist # in the new one. # #################################################### if ( -f $manifest ){ open( MANIFEST, "<$manifest" ) || die( $! ); open( TMPMANIFEST, ">$manifest.tmp" ) || die( $! ); while ( my $line = ){ print TMPMANIFEST $line || die( $! ); } close( MANIFEST ); close( TMPMANIFEST ); } $usher = usher( $pkgfile, $target, 'preinst' ); if ( $usher->{'status'} ) { $result{'status'} = $usher->{'status'}; $result{'stdout'} = $usher->{'stdout'}; $result{'stderr'} = $usher->{'stderr'}; return( \%result ); } print "Extracting files for $infodata->{'info'}->{'package'}\n\n"; if ( $target ) { $cmd = "ar p $pkgfile files.tar.gz|tar -hzvxf - -C $target"; } else { $cmd = "ar p $pkgfile files.tar.gz|tar -hzvxf - -C /"; } %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ local $| = 1; foreach my $fh ( @fhs ){ if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ my $line = <$fh>; ( my $file = $line ) =~ s/.*\/|\n$//; chomp( $file ); if ( $file ){ $filenum++; print STDOUT "\e[?16;0;200c\033[K" . "$file\r"; } if ( $line && $line =~ /^\/usr\/share\/info\// ){ $mkinfo = 1; } } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ print STDERR <$fh>; } if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); if ( $mkinfo && ! $result{'status'} ){ if ( mkinfo( $target ) ){ $result{'status'} = 1; $result{'stderr'} = 'Failed to update info db'; } } print STDOUT "\e[K$filenum files extracted\n"; usher( $pkgfile, $target, 'postinst' ); return( \%result ); } sub reinstall{ my @args = @_; my $pkgfile; my $target; my $infodata; my $confirm; my $cmd; my %runcmd; my $install; my $snapdir; my $package; my $version; my $snapinfo; my $manifest; my $listdata; my $filenum = 0; my $type = 0; my $mkinfo = 0; my %result = ( status => 0, stdout => '', stderr => '' ); for ( my $i = $#args; $i >= 0; $i-- ){ if ( $args[$i] eq '-y' ){ splice( @args, $i, 1 ); $confirm = 'y'; } elsif ( $args[$i] eq 'reinstall' ){ splice( @args, $i, 1 ); } } ( $pkgfile, $target ) = @args; if ( $target ){ $target =~ s/\/$//; } else{ $target = '/'; } $snapdir = "$target/var/snap"; $infodata = info( $pkgfile, $target ); $package = $infodata->{'info'}->{'package'}; $version = $infodata->{'info'}->{'version'}; $snapinfo = "$snapdir/$package/snapinfo"; $manifest = "$snapdir/$package/manifest"; print "Preparing to re-install $package-$version...\n"; if ( my $conflicts = conflicts( $pkgfile, $target ) ){ $result{'status'} = 1; $result{'stderr'} = "Package $package conflicts" . " with the following packages/files:\n" . join( "\n", @$conflicts ); return( \%result ); } if ( -f $snapinfo ){ open( SNAPINFO, "<$snapinfo" ) || die( $! ); while( my $line = ){ if ( $line =~ /^version: (\S+)/ ){ my @sorted = sort( { vercmp( $a, $b ) } ( $1, $version ) ); if ( $sorted[0] eq $sorted[1] ){ last; } else{ close( SNAPINFO ); $result{'status'} = -1; $result{'stderr'} = "$package $version" . " not already installed"; return( \%result ); } } } close( SNAPINFO ); } if ( ! $confirm ){ my $size = human( $infodata->{'info'}->{'bytes'} ); print "Package: $infodata->{'info'}->{'package'}\n" . "Version: $infodata->{'info'}->{'version'}\n" . "Size: $size\n"; print "Re-install $package on $target? (y/n): "; } while( ! $confirm ){ $confirm = ; chomp( $confirm ); if ( lc( $confirm ) eq 'n' ){ print STDERR "Aborting installation\n"; exit 1; } elsif ( lc( $confirm ne 'y' ) ){ print "Answer 'y' or 'n': "; undef( $confirm ); } } #################################################### # # Here we copy the old manifest (if present) to a # temp file to compare after installation and clean # up any files from the old package that done exist # in the new one. # #################################################### open( MANIFEST, "<$manifest" ) || die( $! ); open( TMPMANIFEST, ">$manifest.tmp" ) || die( $! ); while ( my $line = ){ print TMPMANIFEST $line || die( $! ); } close( MANIFEST ); close( TMPMANIFEST ); print "Extracting files for $infodata->{'info'}->{'package'}\n\n"; $cmd = "ar p $pkgfile files.tar.gz|tar -hzvxf - -C $target"; %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ local $| = 1; foreach my $fh ( @fhs ){ if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ my $line = <$fh>; ( my $file = $line ) =~ s/.*\/|\n$//; chomp( $file ); if ( $file ){ $filenum++; print STDOUT "\e[?16;0;200c\033[K" . "$file\r"; } if ( $line && $line =~ /^\/usr\/share\/info\// ){ $mkinfo = 1; } } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ print STDERR <$fh>; } if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); if ( $result{'status'} ){ return( \%result ); } if ( $mkinfo && ! $result{'status'} ){ if ( mkinfo( $target ) ){ $result{'status'} = 1; $result{'stderr'} = 'Failed to update info db'; return( \%result ); } } print STDOUT "\e[K$filenum files extracted\n"; return( \%result ); } sub remove { my @args = @_; my $package; my $target; my $snapdir; my $infodata; my $listdata; my $confirm; my %result = ( status => 0, stdout => '', stderr => '' ); for ( my $i = $#args; $i >= 0; $i-- ){ if ( $args[$i] eq '-y' ){ splice( @args, $i, 1 ); $confirm = 'y'; } elsif ( $args[$i] eq 'remove' ){ splice( @args, $i, 1 ); } } $package = $args[0]; ( $target = $args[1] || '' ) =~ s/\/$//; $snapdir = "$target/$conf->{'general'}{'snapdir'}"; if ( ! $package ){ $result{'status'} = -1; $result{'stderr'} = 'You must supply a package name'; return( \%result ); } if ( -d "$snapdir/$package" ){ $infodata = info( $package, $target ); $listdata = list( $package, $target ); } else{ $result{'stderr'} = "Package '$package' is not installed"; $result{'status'} = -1; return( \%result ); } if ( ! $confirm ){ my $size = human( $infodata->{'info'}->{'bytes'} ); print "Package: $infodata->{'info'}->{'package'}\n" . "Version: $infodata->{'info'}->{'version'}\n" . "Size: $size\n" . "Remove $package from $target? (y/n): "; } while( ! $confirm ){ $confirm = ; chomp( $confirm ); if ( lc( $confirm ) eq 'n' ){ print STDERR "Aborting removal\n"; exit 1; } elsif ( lc( $confirm ne 'y' ) ){ print "Answer 'y' or 'n': "; undef( $confirm ); } } foreach my $data ( @{$listdata->{'list'}} ){ if ( $data->{'type'} ne 'd' ){ unlink( "$target/$data->{'file'}" ); } } foreach my $data ( @{$listdata->{'list'}} ){ if ( $data->{'type'} eq 'd' ){ if ( chkempty( "$target/$data->{'file'}" ) ){ rmdir( "$target/$data->{'file'}" ); } } } opendir( DIR, "$snapdir/$package" ); while ( my $file = readdir( DIR ) ){ if ( $file !~ /^\.{1,2}$/ ){ unlink( "$snapdir/$package/$file" ) || die( $! ); } } rmdir( "$snapdir/$package" ) || die( $! ); return( \%result ); } sub manifest{ my @args = @_; my $pkgfile; my $target; my $infodata; my $listdata; my $snapdir; my $cmd; my %runcmd; my %result = ( status => 0, stdout => '', stderr => '' ); for ( my $i = $#args; $i >= 0; $i-- ){ if ( $args[$i] eq '-y' ){ splice( @args, $i, 1 ); } elsif ( $args[$i] =~ /(re)*install/ ){ splice( @args, $i, 1 ); } } ( $pkgfile, $target ) = @args; if ( $target ){ $target =~ s/\/$//; } else{ $target = ''; } $infodata = info( $pkgfile, $target ); $listdata = list( $pkgfile ); $snapdir = "$target/var/snap/$infodata->{'info'}->{'package'}"; mkdir( "$snapdir" ); $cmd = "ar p $pkgfile manifest > $snapdir/manifest"; %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ foreach my $fh ( @fhs ){ if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); next; } if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ $result{'stdout'} .= <$fh>; } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ $result{'stderr'} .= <$fh>; } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); return( \%result ); } sub mkdirp{ ( my $dir = shift ) =~ s/\/^//; my $mode = shift; ( my $parent = $dir ) =~ s/\/[^\/]+$//; if ( -d $dir ){ return; } mkdirp( $parent, $mode ); mkdir( $dir ) || return( $! ); } sub snapinfo{ my @args = @_; my $pkgfile; my $target; my $infodata; my $snapdir; my $cmd; my %runcmd; my %result = ( status => 0, stdout => '', stderr => '' ); for ( my $i = $#args; $i >= 0; $i-- ){ if ( $args[$i] eq '-y' ){ splice( @args, $i, 1 ); } elsif ( $args[$i] =~ /(re)*install/ ){ splice( @args, $i, 1 ); } } ( $pkgfile, $target ) = @args; if ( $target ){ $target =~ s/\/$//; } else{ $target = ''; } $infodata = info( $pkgfile, $target ); $snapdir = "$target/var/snap/$infodata->{'info'}->{'package'}"; mkdir( "$snapdir" ); $cmd = "ar p $pkgfile snapinfo > $snapdir/snapinfo"; %runcmd = runcmd( $cmd ); while ( my @fhs = $runcmd{'sel'}->can_read ){ foreach my $fh ( @fhs ){ if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); next; } if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ $result{'stdout'} .= <$fh>; } elsif( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ $result{'stderr'} .= <$fh>; } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); return( \%result ); } sub search { my $searchstring = shift; foreach my $source ( keys( %{$conf->{'sources'}} ) ) { my $file = "/$conf->{'general'}{'snapdir'}/$source-packages.gz"; my $data = gzopen( $file, 'r' ); my $buffer; my @packages; while ( $data->gzreadline( $buffer ) > 0 ) { if ( $buffer =~ /^package:\s+(.*)$/ ) { print "$1"; } elsif ( $buffer =~ /^version:\s+(.*)$/ ) { print "-$1"; } elsif ( $buffer =~ /^description:\s+(.*)$/ ) { print " - $1\n"; } } $data->gzclose(); } } sub sttysize { my %size = ( width => 0, height => 0 ); my $cmd = 'stty size'; my %runcmd = runcmd( $cmd ); my %result = ( status => 0, stdout => '', stderr => '' ); while ( my @fhs = $runcmd{'sel'}->can_read ){ foreach my $fh ( @fhs ){ if ( eof( $fh ) ){ $runcmd{'sel'}->remove( $fh ); next; } if ( fileno( $fh ) == fileno( $runcmd{'fh_out'} ) ){ while ( my $line = <$fh> ){ $result{'stdout'} .= $line; } } elsif ( fileno( $fh ) == fileno( $runcmd{'fh_err'} ) ){ $result{'stderr'} .= <$fh>; } } } close( $runcmd{'fh_out'} ); close( $runcmd{'fh_err'} ); waitpid( $runcmd{'pid'}, 0 ); $result{'status'} = $? >> 8; chomp( $result{'stdout'} ); chomp( $result{'stderr'} ); return( \%result ); } ### vercmp() ############################################### # # This subroutine was basically copied verbatim from the # Sort::Versions module. It was modified slightly so that # it more closely matched the aesthetics of the rest of # the snap code. The following credits were provided within # the documentation of that module: # # Ed Avis and Matt Johnson # for recent releases; the original # author is Kenneth J. Albanowski . # Thanks to Hack Kampbjørn and Slaven Rezic for patches # and bug reports. # # Copyright (c) 1996, Kenneth J. Albanowski. All rights # reserved. This program is free software; you can # redistribute it and/or modify it under the same terms as # Perl itself. # ############################################################ sub vercmp{ my @A = ( $_[0] =~ /([-.]|\d+|[^-.\d]+)/g ); my @B = ( $_[1] =~ /([-.]|\d+|[^-.\d]+)/g ); my ( $A, $B ); while ( @A and @B ){ $A = shift @A; $B = shift @B; if ( $A eq '-' and $B eq '-' ){ next; } elsif ( $A eq '-' ){ return -1; } elsif ( $B eq '-' ){ return 1; } elsif ( $A eq '.' and $B eq '.' ){ next; } elsif ( $A eq '.' ){ return -1; } elsif ( $B eq '.' ){ return 1; } elsif ( $A =~ /^\d+$/ and $B =~ /^\d+$/ ){ if ( $A =~ /^0/ || $B =~ /^0/ ){ return $A cmp $B if $A cmp $B; } else{ return $A <=> $B if $A <=> $B; } } else{ $A = uc $A; $B = uc $B; return $A cmp $B if $A cmp $B; } } @A <=> @B; } if ( $ARGV[0] eq 'info' ){ my $result = info( $ARGV[1], $ARGV[2] || '' ); my $info = $result->{'info'}; if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } print "package: $info->{'package'}\nversion: $info->{'version'}\n"; print "depends: $info->{'depends'}\n"; print "bytes: $info->{'bytes'}\nurl: $info->{'url'}\n"; print "description: $info->{'description'}\n"; exit 0; } elsif ( $ARGV[0] eq 'list' && $ARGV[1] ){ my $result = list( $ARGV[1], $ARGV[2] || '' ); my $list = $result->{'list'}; if ( $result->{'status'} ){ print "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } foreach my $row ( @$list ){ if ( $row->{'type'} eq 'd' ){ next; } print "$row->{'sha256'}\t$row->{'file'}\n"; } } elsif ( $ARGV[0] eq 'installed' ){ my $result = installed( $ARGV[1] || '' ); foreach my $key ( sort( keys( %{$result} ) ) ){ if ( $result->{$key}{'package'} && -t STDOUT ){ printf( '%-10.10s ', $result->{$key}{'package'} ); } elsif ( $result->{$key}{'package'} ){ printf( '%-30.30s', $result->{$key}{'package'} ); } else{ printf( '%-10.10s ', 'UNKNOWN' ); } if ( $result->{$key}{'version'} && -t STDOUT ){ printf( '%-10.10s ', $result->{$key}{'version'} ); } elsif ( $result->{$key}{'version'} ){ printf( '%-20.20s', $result->{$key}{'version'} ); } else{ printf( '%-10.10s ', 'UNKNOWN' ); } if ( $result->{$key}{'description'} && -t STDOUT ){ printf( '%.58s', $result->{$key}{'description'} ); } elsif ( $result->{$key}{'description'} ){ print "$result->{$key}{'description'}"; } else{ printf( '%.58s', ' ' ); } print "\n"; } } elsif ( $ARGV[0] eq 'hash' ){ my $result = hash( $ARGV[1] ); my $hash = $result->{'hash'}; if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } print "$hash\n"; } elsif ( $ARGV[0] eq 'install' ){ my $packages = repo(); my $installed = installed(); my $result; # = install( @ARGV ); my $infodata; my $listdata; my $manifest; my $package; my @depends; my @depfails; my $size = 0; my $yes = 0; my $target; for ( my $i = 0; $i <= $#ARGV; $i++ ){ if ( $ARGV[$i] eq 'install' ){ next; } elsif ( $ARGV[$i] eq '-y' ){ $yes++; next; } if ( ! $package ){ $package = $ARGV[$i]; } else{ $target = $ARGV[$i]; } } if ( ! -f $package ) { if ( ! $packages->{$package} ) { print STDERR "$package is not a valid snap file" . " and not available in any repos\n"; exit 1; } } else { my $info = info( $package ); if ( $installed->{$info->{'info'}{'package'}} && $packages->{$package}{'version'} eq $installed->{$info->{'info'}{'package'}} ) { print STDERR "$package $info->{'info'}{'version'}" . " is already installed\n"; exit 1; } $packages->{$package} = $info->{'info'}; } depends( $packages, $package, \@depends ); for ( my $i = $#depends; $i >= 0; $i-- ) { if ( $installed->{$depends[$i]} ) { splice( @depends, $i, 1 ); } } foreach my $pkg ( ( @depends, $package ) ) { if ( ! $packages->{$pkg} ) { push( @depfails, $pkg ); } else { $size += $packages->{$pkg}{'bytes'}; } } if ( @depfails ) { print STDERR "The following dependencies are not" . " available in any repos:\n"; foreach my $depfail ( sort( @depfails ) ) { print STDERR " $depfail\n"; } exit 1; } if ( ! $yes ){ if ( @depends ) { print "The following dependencies will need to be" . " installed:\n" . join( ' ', @depends ) . "\n"; } print "Total size on disk: " . human( $size ) . " ($size bytes)\n"; if ( $installed->{$package} ) { print "Would you like to upgrade $package" . " ($installed->{$package}{'version'}" . " to $packages->{$package}{'version'})" . "? (y/n): "; } # if ( $type == -1 ){ # print "Downgrade $package to $version on $target?" # . "(y/n): "; # } # elsif ( $type == 1 ){ # print "Upgrade $package to $version on $target?" # . "(y/n): "; # } # else{ # print "Install $package on $target? (y/n): "; # } } while( ! $yes ){ $yes = ; chomp( $yes ); if ( lc( $yes ) eq 'n' ){ print STDERR "Aborting installation\n"; exit 1; } elsif ( lc( $yes ne 'y' ) ){ print "Answer 'y' or 'n': "; undef( $yes ); } } print "SIZE: $size\n"; exit; if ( ! $target ){ $target = '/'; } $manifest .= "$target/var/snap/$infodata->{'info'}{'package'}/manifest"; if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } else{ ############################################ # # Here we use manifest() and snapinfo() # to write the manifest and snapinfo # files in /var/snap/packagename # ############################################ $result = manifest( @ARGV ); if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } $result = snapinfo( @ARGV ); if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } } ############################################ # # In this section we check to see if # $manifest.tmp exists (which means an # already installed package is being # replaced (reinstall, upgrade, etc). # This keeps us from being left with # orphaned files. # ############################################ if ( -f "$manifest.tmp" ){ open( TMPMANIFEST, "<$manifest.tmp" ) || die( $! ); $listdata = list( $package ); while ( my $line = ){ my ( $sha256, $perms, $file ) = split( /\s/, $line ); my $match = 0; foreach my $data ( @{$listdata->{'list'}} ){ if ( $file eq $data->{'file'} ){ $match = 1; last; } } if ( ! $match ){ if ( -d "$target/$file" && chkempty( "$target/$file" ) ){ rmdir( "$target/$file" ) || die( $! ); } elsif ( -f "$target/$file" ){ unlink( "$target/$file" ) || die( $! ); } } } close( TMPMANIFEST ); unlink( "$manifest.tmp" ) || die( $! ); } print "Package successfully installed\n\n"; } elsif ( $ARGV[0] eq 'reinstall' ){ my $result = reinstall( @ARGV ); my $infodata; my $listdata; my $manifest; my $pkgfile; my $target; for ( my $i = $#ARGV; $i >= 0; $i-- ){ if ( $ARGV[$i] eq '-y' ){ next; } elsif ( $ARGV[$i] eq 'reinstall' ){ next; } if ( ! $pkgfile ){ $pkgfile = $ARGV[$i]; } else{ $target = $ARGV[$i]; } } $infodata = info( $pkgfile, $target ); if ( ! $target ){ $target = '/'; } $manifest .= "$target/var/snap/$infodata->{'info'}{'package'}/manifest"; if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } else{ ############################################ # # Here we use manifest() and snapinfo() # to write the manifest and snapinfo # files in /var/snap/packagename # ############################################ $result = manifest( @ARGV ); if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } $result = snapinfo( @ARGV ); if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } ############################################ # # In this section we look through the files # in the old package and remove them if they # aren't in the new package. This # functionality should probably be made # into a sub at some point. # ############################################ open( TMPMANIFEST, "<$manifest.tmp" ) || die( $! ); $listdata = list( $pkgfile ); while ( my $line = ){ my ( $sha256, $perms, $file ) = split( /\s/, $line ); my $match = 0; foreach my $data ( @{$listdata->{'list'}} ){ if ( $file eq $data->{'file'} ){ $match = 1; last; } } if ( ! $match ){ if ( -d "$target/$file" && chkempty( "$target/$file" ) ){ rmdir( "$target/$file" ) || die( $! ); } elsif ( -f "$target/$file" ){ unlink( "$target/$file" ) || die( $! ); } } } close( TMPMANIFEST ); unlink( "$manifest.tmp" ) || die( $! ); print "Package successfully re-installed\n\n"; } } elsif ( $ARGV[0] eq 'refresh' ) { foreach my $source ( keys( %{$conf->{'sources'}} ) ) { ( my $src = "$conf->{'sources'}{$source}" ) =~ s/\/$//; my $dest = "/$conf->{'general'}{'snapdir'}/$source-packages.gz"; my $mode = 0644; my $packages; print "Retrieving packages for source '$source'... "; $packages = httpget( "$src/$snapver/packages.gz", $dest, $mode ); if ( $packages->{'status'} == 200 ) { print "Success\n"; } else { print "Failed!\nhttpget: $packages->{'stderr'}\n"; } } } elsif ( $ARGV[0] eq 'remove' ){ my $result = remove( @ARGV ); if ( $result->{'status'} ){ print STDERR "snap Error: $result->{'stderr'}\n"; exit $result->{'status'}; } else{ print "\nPackage successfully removed\n\n"; } } elsif ( $ARGV[0] eq 'repo' ) { my $packages = repo(); foreach my $package ( sort( keys( %{$packages} ) ) ) { print "$package - $packages->{$package}{'description'}\n"; } } elsif ( $ARGV[0] eq 'search' ) { my $packages = repo(); foreach my $package ( sort( keys( %{$packages} ) ) ) { if ( index( $package, $ARGV[1] ) != -1 || index( $packages->{$package}{'description'}, $ARGV[1] ) != -1) { print "$package - " . "$packages->{$package}{'description'}\n"; } } } else{ print STDERR "snap Error: $ARGV[0] is not a valid argument\n"; exit 1; }