File:  [LON-CAPA] / loncom / cgi / archive.pl
Revision 1.1: download - view: text, annotated - select for diffs
Mon May 13 13:55:51 2024 UTC (3 weeks, 6 days ago) by raeburn
Branches: MAIN
CVS tags: HEAD
- Bug 6990. Author can export specified files (with/without recursion into
  subdirectories from current directory in Authoring Space to archive file.

#!/usr/bin/perl
#
# $Id: archive.pl,v 1.1 2024/05/13 13:55:51 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA 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.
#
# LON-CAPA 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 LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/cgi-bin/archive.pl
#
# http://www.lon-capa.org/
#
# The LearningOnline Network with CAPA
#
# A CGI script which creates a compressed archive file of the current
# directory in Authoring Space, with optional (a) recursion into
# sub-directories, (b) filtering by filetype and (c) encryption.
# Supported formats are: tar.gz, tar.bz2, tar.xz and zip.
####
use strict;
use lib '/home/httpd/lib/perl';
use File::Find;
use Apache::lonnet;
use Apache::loncommon;
use Apache::lonlocal;
use LONCAPA::loncgi;
use Cwd;
use HTML::Entities;

$|++;

my $lock;

our %excluded = (
                   bak => 1,
                   save => 1,
                   log => 1,
                );

our $maxdepth = 0;
our %included = ();
our $alltypes = '';
our $recurse = '';
our $includeother = '';
our $prefix = '';
our $totalfiles = 0;
our $totalsize = 0;
our $totalsubdirs = 0;
our %subdirs = ();
our $fh;

if (!&LONCAPA::loncgi::check_cookie_and_load_env()) {
    &Apache::lonlocal::get_language_handle();
    print(&LONCAPA::loncgi::missing_cookie_msg());
} else {
    &Apache::lonlocal::get_language_handle();
    my %lt = &Apache::lonlocal::texthash (
                                            indi => 'Invalid directory name',
                                            outo => 'Output of command:',
                                            comp => 'Archive creation complete.',
                                            erro => 'An error occurred.',
                                            cctf  => 'Cannot create tar file',
                                            dtf  => 'Download tar file',
                                         );
# Get the identifier and set a lock
    my %perlvar=%{&LONCAPA::Configuration::read_conf('loncapa.conf')};
    my $londocroot = $Apache::lonnet::perlvar{'lonDocRoot'};
    &Apache::lonlocal::get_language_handle();
    &Apache::loncommon::content_type(undef,'text/html');
    my $identifier = $ENV{'QUERY_STRING'};
    my ($hashref,$dir,$dirurl,$jsdirurl,$auname,$audom,$allowed,$error,$encrypt,$enckey,$format,$compress);
    my @posstypes = qw(problem library sty sequence page task rights meta xml html xhtml htm xhtm css js tex txt gif jpg jpeg png svg other);
    if (($identifier) && (exists($env{'cgi.'.$identifier.'.archive'}))) {
        $hashref = &Apache::lonnet::thaw_unescape($env{'cgi.'.$identifier.'.archive'});
        if (ref($hashref) eq 'HASH') {
            $dir = $hashref->{'dir'};
            # check for traversal
            if (-d $dir) {
                $dirurl = $dir;
                ($auname,$audom) = &Apache::lonnet::constructaccess($dir);
                if (($auname ne '') && ($audom ne '')) {
                    $dirurl =~ s/^\Q$londocroot\E//;
                    $prefix = $londocroot.$dirurl;
                    $maxdepth = $prefix =~ tr{/}{};
                    $jsdirurl = &js_escape($dirurl);
                    if (($auname eq $env{'user.name'}) && ($audom eq $env{'user.domain'}) &&
                        ($env{'environment.authorarchive'})) {
                        $allowed = 1;
                        if ($hashref->{'recurse'}) {
                            $recurse = 1;
                        } else {
                            $recurse = 0;
                        }
                        if ($hashref->{'types'} eq 'all') {
                            $alltypes = 1;
                        } else {
                            $alltypes = 0;
                            my %possincluded;
                            map { $possincluded{$_} = 1; } split(/,/,$hashref->{'types'});
                            $includeother = 0;
                            foreach my $type (@posstypes) {
                                if ($type eq 'other') {
                                    if ($possincluded{$type}) {
                                        $includeother = 1;
                                    } else {
                                        $includeother = 0;
                                    }
                                } else {
                                    if ($possincluded{$type}) {
                                        $included{$type} = 1;
                                    } else {
                                        $excluded{$type} = 1;
                                    }
                                }
                            }
                        }
                        if ((exists($hashref->{'encrypt'}) && $hashref->{'encrypt'} ne '')) { 
                            $encrypt = 1;
                            $enckey = $hashref->{'encrypt'};
                        }
                        if ((exists($hashref->{'format'}) && $hashref->{'format'} =~ /^zip$/i)) {
                            $format = lc($hashref->{'format'});
                        } else {
                            $format = 'tar';
                        }
                        unless ($format eq 'zip') { 
                            if ((exists($hashref->{'compress'})) && ($hashref->{'compress'} =~ /^(xz|bzip2)$/i)) {
                                $compress = lc($hashref->{'compress'});
                            } else {
                                $compress = 'gzip';
                            }
                        }
                    }
                }
            } else {
                $error = 'indi';
            }
        } else {
            $error = 'nohash';
        }
# delete cgi.$identifier.archive from %env
        &Apache::lonnet::delenv('cgi.'.$identifier.'.archive');
    } else {
        $error = 'noid';
    }
    $env{'request.noversionuri'} = '/cgi-bin/archive.pl';
    my ($brcrum,$title); 
    if ($error) {
        $brcrum = [{'href' => '',
                    'text' => 'Missing information'}];
    } elsif (!$allowed) {
        $brcrum = [{'href' => '',
                    'text' => 'Access denied'}];
    } else {
# Breadcrumbs
        $title = 'Creating archive file';
        $brcrum = [{'href' => $dirurl,
                    'text' => 'Authoring Space'},
                   {'href' => "javascript:gocstr('/adm/cfile?action=archive','$jsdirurl');",
                    'text' => 'File Operation'},
                   {'href' => '',
                    'text' => $title}];
    }
    my $js;
    print &Apache::loncommon::start_page($title,
                                         $js,
                                         {'bread_crumbs' => $brcrum,})."\n".
          '<form name="constspace" method="post" action="">'."\n".
          '<input type="hidden" name="filename" value="" />'."\n";
    if ($error) {
        print "&mt('Cannot create archive file -- \n";
    } elsif ($allowed) {
        my (%location_of,@tocheck);
        if ($format ne '') {
            push(@tocheck,$format);
        }
        if ($compress ne '') {
            push(@tocheck,$compress);
        } 
        foreach my $program (@tocheck) {
            foreach my $dir ('/bin/','/usr/bin/','/usr/local/bin/','/sbin/',
                             '/usr/sbin/') {
                if (-x $dir.$program) {
                    $location_of{$program} = $dir.$program;
                    last;
                }
            }
        }
        if (exists($location_of{$format})) {
            my $suffix;
            if ($format eq 'zip') {
                $suffix = 'zip';
            } else {
                $suffix = 'tar';
                if (exists($location_of{$compress})) {
                    if ($compress eq 'bzip2') {
                        $suffix .= '.bz2'; 
                    } elsif ($compress eq 'gzip') {
                        $suffix .= '.gz';
                    } elsif ($compress eq 'xz') {
                        $suffix .= '.xz';
                    }
                }
            }
            my $namesdest = $perlvar{'lonPrtDir'}.'/'.$env{'user.name'}.'_'.$env{'user.domain'}.'_archive_'.$identifier.'.txt';
            my $filesdest = $perlvar{'lonPrtDir'}.'/'.$env{'user.name'}.'_'.$env{'user.domain'}.'_archive_'.$identifier.'.'.$suffix;
            my $filesurl = '/prtspool/'.$env{'user.name'}.'_'.$env{'user.domain'}.'_archive_'.$identifier.'.'.$suffix;
            unless ($lock) { $lock=&Apache::lonnet::set_lock(&mt('Archiving [_1]',$dirurl)); }
            if (open($fh,'>',$namesdest)) {
                find(
                     {preprocess => \&filter_files,
                      wanted     => \&store_names,
                      no_chdir   => 1,
                     },$dir);
                close($fh);
                if (($totalfiles) || ($totalsubdirs)) {
                    print '<p>'.
                          &mt('Archiving: [quant,_1,file,files] with total size: [_2] bytes in [quant,_3,subdirectory,subdirectories] ...',
                              $totalfiles,$totalsize,$totalsubdirs).
                          '</p>';
                    my ($cwd,@args);
                    if ($format eq 'zip') {
                        $cwd = &Cwd::getcwd(); 
                        @args = ('zip',$filesdest,'-v','-r','.','-i@'.$namesdest);
                        chdir $prefix;
                    } else {
                        @args = ('tar',"--create","--verbose");
                        if (($compress ne '') && (exists($location_of{$compress}))) {
                            push(@args,"--$compress");
                        }
                        push(@args,("--file=$filesdest","--directory=$prefix","--files-from=$namesdest"));
                    }
                    if (open(my $pipe,'-|',@args)) {
                        my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin('',$totalfiles); 
                        while (<$pipe>) {
                            &Apache::lonhtmlcommon::Increment_PrgWin('',\%prog_state,'last file');
                        }
                        &Apache::lonhtmlcommon::Close_PrgWin('',\%prog_state);
                        close($pipe);
                        if (!-e $filesdest) {
                            print '<p>'.&mt('No archive file available for download').'</p>'."\n"; 
                        }
                    } else {
                        print '<p>'.&mt('Could not call [_1] command',$format).'</p>'."\n";
                    }
                    if (($format eq 'zip') && ($cwd ne '')) {
                        chdir $cwd;
                    }
                } else {
                    print '<p>'.&mt('No files match the requested types so no archive file was created.').'</p>'."\n";
                }
                unlink($namesdest);
            } else {
                print '<p>'.&mt('Could not store list of files to archive').'</p>'."\n";
            }
            if ($lock) { &Apache::lonnet::remove_lock($lock); }
        } else {
            print '<p>'.&mt('Could not find location of [_1] command',$format).'</p>'."\n";
        }
    }
    if ($dirurl) {
        print '<br /><br />'.
              &Apache::lonhtmlcommon::actionbox(['<a href="'.&HTML::Entities::encode($dirurl,'\'"&<>').'">'.
                                                 &mt('Return to Directory').'</a>']);
    }
    print '</form>'.&Apache::loncommon::end_page();

# Code to delete archive file after successful download
    %included = ();
    $alltypes = '';
    $recurse = '';
    $includeother = '';
    $prefix = '';
    $totalfiles = 0;
    $totalsize = 0;
    $totalsubdirs = 0;
    %excluded = (
                   bak => 1,
                   save => 1,
                   log => 1,
                );
}

sub filter_files {
    my @PossibleFiles = @_;
    my @ChosenFiles;
    foreach my $file (@PossibleFiles) {
        if (-d $File::Find::dir."/".$file) {
            if (!$recurse) {
                my $depth = $File::Find::dir =~ tr[/][];
                next unless ($depth < $maxdepth-1);
            }
            push(@ChosenFiles,$file);
        } else {
            my ($extension) = ($file =~ /\.([^.]+)$/);
            if ((!$excluded{$extension}) && ($alltypes || $includeother || $included{$extension})) {
                push(@ChosenFiles,$file);
            }
        }
    }
    return @ChosenFiles;  
}

sub store_names {
    my $filename = $File::Find::name;
    if (-d $filename) {
        unless ("$filename/" eq $prefix) {
            if ($recurse) {
                $subdirs{$filename} = 1;
                $totalsubdirs ++;
            }
        }
        next;
    }
    $totalfiles ++;
    $totalsize += -s $filename;
    $filename =~ s{^$prefix}{}; 
    print $fh "$filename\n";
}

sub js {
    my $output = <<'END';
const xhrButtonSuccess = document.querySelector(".xhr.success");
const xhrButtonError = document.querySelector(".xhr.error");
const xhrButtonAbort = document.querySelector(".xhr.abort");
const log = document.querySelector(".event-log");

function handleEvent(e) {
  log.textContent = `${log.textContent}${e.type}: ${e.loaded} bytes transferred\n`;
}

function addListeners(xhr) {
  xhr.addEventListener("loadstart", handleEvent);
  xhr.addEventListener("load", handleEvent);
  xhr.addEventListener("loadend", handleEvent);
  xhr.addEventListener("progress", handleEvent);
  xhr.addEventListener("error", handleEvent);
  xhr.addEventListener("abort", handleEvent);
}

function runXHR(url) {
  log.textContent = "";

  const xhr = new XMLHttpRequest();
  addListeners(xhr);
  xhr.open("GET", url);
  xhr.send();
  return xhr;
}

xhrButtonSuccess.addEventListener("click", () => {
  runXHR(
    "https://somewhere",
  );
});

xhrButtonError.addEventListener("click", () => {
  runXHR("http://i-dont-exist");
});

xhrButtonAbort.addEventListener("click", () => {
  runXHR(
    "https://somewhere",
  ).abort();
});

END

}



FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>