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 (4 weeks, 1 day 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.

    1: #!/usr/bin/perl
    2: #
    3: # $Id: archive.pl,v 1.1 2024/05/13 13:55:51 raeburn Exp $
    4: #
    5: # Copyright Michigan State University Board of Trustees
    6: #
    7: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    8: #
    9: # LON-CAPA is free software; you can redistribute it and/or modify
   10: # it under the terms of the GNU General Public License as published by
   11: # the Free Software Foundation; either version 2 of the License, or
   12: # (at your option) any later version.
   13: #
   14: # LON-CAPA is distributed in the hope that it will be useful,
   15: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   17: # GNU General Public License for more details.
   18: #
   19: # You should have received a copy of the GNU General Public License
   20: # along with LON-CAPA; if not, write to the Free Software
   21: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   22: #
   23: # /home/httpd/cgi-bin/archive.pl
   24: #
   25: # http://www.lon-capa.org/
   26: #
   27: # The LearningOnline Network with CAPA
   28: #
   29: # A CGI script which creates a compressed archive file of the current
   30: # directory in Authoring Space, with optional (a) recursion into
   31: # sub-directories, (b) filtering by filetype and (c) encryption.
   32: # Supported formats are: tar.gz, tar.bz2, tar.xz and zip.
   33: ####
   34: use strict;
   35: use lib '/home/httpd/lib/perl';
   36: use File::Find;
   37: use Apache::lonnet;
   38: use Apache::loncommon;
   39: use Apache::lonlocal;
   40: use LONCAPA::loncgi;
   41: use Cwd;
   42: use HTML::Entities;
   43: 
   44: $|++;
   45: 
   46: my $lock;
   47: 
   48: our %excluded = (
   49:                    bak => 1,
   50:                    save => 1,
   51:                    log => 1,
   52:                 );
   53: 
   54: our $maxdepth = 0;
   55: our %included = ();
   56: our $alltypes = '';
   57: our $recurse = '';
   58: our $includeother = '';
   59: our $prefix = '';
   60: our $totalfiles = 0;
   61: our $totalsize = 0;
   62: our $totalsubdirs = 0;
   63: our %subdirs = ();
   64: our $fh;
   65: 
   66: if (!&LONCAPA::loncgi::check_cookie_and_load_env()) {
   67:     &Apache::lonlocal::get_language_handle();
   68:     print(&LONCAPA::loncgi::missing_cookie_msg());
   69: } else {
   70:     &Apache::lonlocal::get_language_handle();
   71:     my %lt = &Apache::lonlocal::texthash (
   72:                                             indi => 'Invalid directory name',
   73:                                             outo => 'Output of command:',
   74:                                             comp => 'Archive creation complete.',
   75:                                             erro => 'An error occurred.',
   76:                                             cctf  => 'Cannot create tar file',
   77:                                             dtf  => 'Download tar file',
   78:                                          );
   79: # Get the identifier and set a lock
   80:     my %perlvar=%{&LONCAPA::Configuration::read_conf('loncapa.conf')};
   81:     my $londocroot = $Apache::lonnet::perlvar{'lonDocRoot'};
   82:     &Apache::lonlocal::get_language_handle();
   83:     &Apache::loncommon::content_type(undef,'text/html');
   84:     my $identifier = $ENV{'QUERY_STRING'};
   85:     my ($hashref,$dir,$dirurl,$jsdirurl,$auname,$audom,$allowed,$error,$encrypt,$enckey,$format,$compress);
   86:     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);
   87:     if (($identifier) && (exists($env{'cgi.'.$identifier.'.archive'}))) {
   88:         $hashref = &Apache::lonnet::thaw_unescape($env{'cgi.'.$identifier.'.archive'});
   89:         if (ref($hashref) eq 'HASH') {
   90:             $dir = $hashref->{'dir'};
   91:             # check for traversal
   92:             if (-d $dir) {
   93:                 $dirurl = $dir;
   94:                 ($auname,$audom) = &Apache::lonnet::constructaccess($dir);
   95:                 if (($auname ne '') && ($audom ne '')) {
   96:                     $dirurl =~ s/^\Q$londocroot\E//;
   97:                     $prefix = $londocroot.$dirurl;
   98:                     $maxdepth = $prefix =~ tr{/}{};
   99:                     $jsdirurl = &js_escape($dirurl);
  100:                     if (($auname eq $env{'user.name'}) && ($audom eq $env{'user.domain'}) &&
  101:                         ($env{'environment.authorarchive'})) {
  102:                         $allowed = 1;
  103:                         if ($hashref->{'recurse'}) {
  104:                             $recurse = 1;
  105:                         } else {
  106:                             $recurse = 0;
  107:                         }
  108:                         if ($hashref->{'types'} eq 'all') {
  109:                             $alltypes = 1;
  110:                         } else {
  111:                             $alltypes = 0;
  112:                             my %possincluded;
  113:                             map { $possincluded{$_} = 1; } split(/,/,$hashref->{'types'});
  114:                             $includeother = 0;
  115:                             foreach my $type (@posstypes) {
  116:                                 if ($type eq 'other') {
  117:                                     if ($possincluded{$type}) {
  118:                                         $includeother = 1;
  119:                                     } else {
  120:                                         $includeother = 0;
  121:                                     }
  122:                                 } else {
  123:                                     if ($possincluded{$type}) {
  124:                                         $included{$type} = 1;
  125:                                     } else {
  126:                                         $excluded{$type} = 1;
  127:                                     }
  128:                                 }
  129:                             }
  130:                         }
  131:                         if ((exists($hashref->{'encrypt'}) && $hashref->{'encrypt'} ne '')) { 
  132:                             $encrypt = 1;
  133:                             $enckey = $hashref->{'encrypt'};
  134:                         }
  135:                         if ((exists($hashref->{'format'}) && $hashref->{'format'} =~ /^zip$/i)) {
  136:                             $format = lc($hashref->{'format'});
  137:                         } else {
  138:                             $format = 'tar';
  139:                         }
  140:                         unless ($format eq 'zip') { 
  141:                             if ((exists($hashref->{'compress'})) && ($hashref->{'compress'} =~ /^(xz|bzip2)$/i)) {
  142:                                 $compress = lc($hashref->{'compress'});
  143:                             } else {
  144:                                 $compress = 'gzip';
  145:                             }
  146:                         }
  147:                     }
  148:                 }
  149:             } else {
  150:                 $error = 'indi';
  151:             }
  152:         } else {
  153:             $error = 'nohash';
  154:         }
  155: # delete cgi.$identifier.archive from %env
  156:         &Apache::lonnet::delenv('cgi.'.$identifier.'.archive');
  157:     } else {
  158:         $error = 'noid';
  159:     }
  160:     $env{'request.noversionuri'} = '/cgi-bin/archive.pl';
  161:     my ($brcrum,$title); 
  162:     if ($error) {
  163:         $brcrum = [{'href' => '',
  164:                     'text' => 'Missing information'}];
  165:     } elsif (!$allowed) {
  166:         $brcrum = [{'href' => '',
  167:                     'text' => 'Access denied'}];
  168:     } else {
  169: # Breadcrumbs
  170:         $title = 'Creating archive file';
  171:         $brcrum = [{'href' => $dirurl,
  172:                     'text' => 'Authoring Space'},
  173:                    {'href' => "javascript:gocstr('/adm/cfile?action=archive','$jsdirurl');",
  174:                     'text' => 'File Operation'},
  175:                    {'href' => '',
  176:                     'text' => $title}];
  177:     }
  178:     my $js;
  179:     print &Apache::loncommon::start_page($title,
  180:                                          $js,
  181:                                          {'bread_crumbs' => $brcrum,})."\n".
  182:           '<form name="constspace" method="post" action="">'."\n".
  183:           '<input type="hidden" name="filename" value="" />'."\n";
  184:     if ($error) {
  185:         print "&mt('Cannot create archive file -- \n";
  186:     } elsif ($allowed) {
  187:         my (%location_of,@tocheck);
  188:         if ($format ne '') {
  189:             push(@tocheck,$format);
  190:         }
  191:         if ($compress ne '') {
  192:             push(@tocheck,$compress);
  193:         } 
  194:         foreach my $program (@tocheck) {
  195:             foreach my $dir ('/bin/','/usr/bin/','/usr/local/bin/','/sbin/',
  196:                              '/usr/sbin/') {
  197:                 if (-x $dir.$program) {
  198:                     $location_of{$program} = $dir.$program;
  199:                     last;
  200:                 }
  201:             }
  202:         }
  203:         if (exists($location_of{$format})) {
  204:             my $suffix;
  205:             if ($format eq 'zip') {
  206:                 $suffix = 'zip';
  207:             } else {
  208:                 $suffix = 'tar';
  209:                 if (exists($location_of{$compress})) {
  210:                     if ($compress eq 'bzip2') {
  211:                         $suffix .= '.bz2'; 
  212:                     } elsif ($compress eq 'gzip') {
  213:                         $suffix .= '.gz';
  214:                     } elsif ($compress eq 'xz') {
  215:                         $suffix .= '.xz';
  216:                     }
  217:                 }
  218:             }
  219:             my $namesdest = $perlvar{'lonPrtDir'}.'/'.$env{'user.name'}.'_'.$env{'user.domain'}.'_archive_'.$identifier.'.txt';
  220:             my $filesdest = $perlvar{'lonPrtDir'}.'/'.$env{'user.name'}.'_'.$env{'user.domain'}.'_archive_'.$identifier.'.'.$suffix;
  221:             my $filesurl = '/prtspool/'.$env{'user.name'}.'_'.$env{'user.domain'}.'_archive_'.$identifier.'.'.$suffix;
  222:             unless ($lock) { $lock=&Apache::lonnet::set_lock(&mt('Archiving [_1]',$dirurl)); }
  223:             if (open($fh,'>',$namesdest)) {
  224:                 find(
  225:                      {preprocess => \&filter_files,
  226:                       wanted     => \&store_names,
  227:                       no_chdir   => 1,
  228:                      },$dir);
  229:                 close($fh);
  230:                 if (($totalfiles) || ($totalsubdirs)) {
  231:                     print '<p>'.
  232:                           &mt('Archiving: [quant,_1,file,files] with total size: [_2] bytes in [quant,_3,subdirectory,subdirectories] ...',
  233:                               $totalfiles,$totalsize,$totalsubdirs).
  234:                           '</p>';
  235:                     my ($cwd,@args);
  236:                     if ($format eq 'zip') {
  237:                         $cwd = &Cwd::getcwd(); 
  238:                         @args = ('zip',$filesdest,'-v','-r','.','-i@'.$namesdest);
  239:                         chdir $prefix;
  240:                     } else {
  241:                         @args = ('tar',"--create","--verbose");
  242:                         if (($compress ne '') && (exists($location_of{$compress}))) {
  243:                             push(@args,"--$compress");
  244:                         }
  245:                         push(@args,("--file=$filesdest","--directory=$prefix","--files-from=$namesdest"));
  246:                     }
  247:                     if (open(my $pipe,'-|',@args)) {
  248:                         my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin('',$totalfiles); 
  249:                         while (<$pipe>) {
  250:                             &Apache::lonhtmlcommon::Increment_PrgWin('',\%prog_state,'last file');
  251:                         }
  252:                         &Apache::lonhtmlcommon::Close_PrgWin('',\%prog_state);
  253:                         close($pipe);
  254:                         if (!-e $filesdest) {
  255:                             print '<p>'.&mt('No archive file available for download').'</p>'."\n"; 
  256:                         }
  257:                     } else {
  258:                         print '<p>'.&mt('Could not call [_1] command',$format).'</p>'."\n";
  259:                     }
  260:                     if (($format eq 'zip') && ($cwd ne '')) {
  261:                         chdir $cwd;
  262:                     }
  263:                 } else {
  264:                     print '<p>'.&mt('No files match the requested types so no archive file was created.').'</p>'."\n";
  265:                 }
  266:                 unlink($namesdest);
  267:             } else {
  268:                 print '<p>'.&mt('Could not store list of files to archive').'</p>'."\n";
  269:             }
  270:             if ($lock) { &Apache::lonnet::remove_lock($lock); }
  271:         } else {
  272:             print '<p>'.&mt('Could not find location of [_1] command',$format).'</p>'."\n";
  273:         }
  274:     }
  275:     if ($dirurl) {
  276:         print '<br /><br />'.
  277:               &Apache::lonhtmlcommon::actionbox(['<a href="'.&HTML::Entities::encode($dirurl,'\'"&<>').'">'.
  278:                                                  &mt('Return to Directory').'</a>']);
  279:     }
  280:     print '</form>'.&Apache::loncommon::end_page();
  281: 
  282: # Code to delete archive file after successful download
  283:     %included = ();
  284:     $alltypes = '';
  285:     $recurse = '';
  286:     $includeother = '';
  287:     $prefix = '';
  288:     $totalfiles = 0;
  289:     $totalsize = 0;
  290:     $totalsubdirs = 0;
  291:     %excluded = (
  292:                    bak => 1,
  293:                    save => 1,
  294:                    log => 1,
  295:                 );
  296: }
  297: 
  298: sub filter_files {
  299:     my @PossibleFiles = @_;
  300:     my @ChosenFiles;
  301:     foreach my $file (@PossibleFiles) {
  302:         if (-d $File::Find::dir."/".$file) {
  303:             if (!$recurse) {
  304:                 my $depth = $File::Find::dir =~ tr[/][];
  305:                 next unless ($depth < $maxdepth-1);
  306:             }
  307:             push(@ChosenFiles,$file);
  308:         } else {
  309:             my ($extension) = ($file =~ /\.([^.]+)$/);
  310:             if ((!$excluded{$extension}) && ($alltypes || $includeother || $included{$extension})) {
  311:                 push(@ChosenFiles,$file);
  312:             }
  313:         }
  314:     }
  315:     return @ChosenFiles;  
  316: }
  317: 
  318: sub store_names {
  319:     my $filename = $File::Find::name;
  320:     if (-d $filename) {
  321:         unless ("$filename/" eq $prefix) {
  322:             if ($recurse) {
  323:                 $subdirs{$filename} = 1;
  324:                 $totalsubdirs ++;
  325:             }
  326:         }
  327:         next;
  328:     }
  329:     $totalfiles ++;
  330:     $totalsize += -s $filename;
  331:     $filename =~ s{^$prefix}{}; 
  332:     print $fh "$filename\n";
  333: }
  334: 
  335: sub js {
  336:     my $output = <<'END';
  337: const xhrButtonSuccess = document.querySelector(".xhr.success");
  338: const xhrButtonError = document.querySelector(".xhr.error");
  339: const xhrButtonAbort = document.querySelector(".xhr.abort");
  340: const log = document.querySelector(".event-log");
  341: 
  342: function handleEvent(e) {
  343:   log.textContent = `${log.textContent}${e.type}: ${e.loaded} bytes transferred\n`;
  344: }
  345: 
  346: function addListeners(xhr) {
  347:   xhr.addEventListener("loadstart", handleEvent);
  348:   xhr.addEventListener("load", handleEvent);
  349:   xhr.addEventListener("loadend", handleEvent);
  350:   xhr.addEventListener("progress", handleEvent);
  351:   xhr.addEventListener("error", handleEvent);
  352:   xhr.addEventListener("abort", handleEvent);
  353: }
  354: 
  355: function runXHR(url) {
  356:   log.textContent = "";
  357: 
  358:   const xhr = new XMLHttpRequest();
  359:   addListeners(xhr);
  360:   xhr.open("GET", url);
  361:   xhr.send();
  362:   return xhr;
  363: }
  364: 
  365: xhrButtonSuccess.addEventListener("click", () => {
  366:   runXHR(
  367:     "https://somewhere",
  368:   );
  369: });
  370: 
  371: xhrButtonError.addEventListener("click", () => {
  372:   runXHR("http://i-dont-exist");
  373: });
  374: 
  375: xhrButtonAbort.addEventListener("click", () => {
  376:   runXHR(
  377:     "https://somewhere",
  378:   ).abort();
  379: });
  380: 
  381: END
  382: 
  383: }
  384: 
  385: 

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