RSS Git Download  Clone
Raw Blame History
package LIMS::Controller::Roles::DataFile;

use Moose::Role;

use File::Copy;
use File::Path qw(make_path);
use LWP::UserAgent;
use File::Basename;
use IO::Uncompress::Unzip qw(unzip $UnzipError) ;
use IO::All; # rename() gives cryptic error "Can't locate IO/All/Rename.pm in @INC"
             # if target file doesn't exist
use Data::Dumper; # leave on - used by File::Path error handler

#-------------------------------------------------------------------------------
sub data_file_from_url {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    # limit upload to 20MB:
    $CGI::POST_MAX = 20_000 * 1_024; # working but not setting cgi_error ??

    my $url = $self->query->param('url') || return $self->forward('data_file');

    my $rtn_url = $self->query->param('_return') # optional - from referral_sources lookup page
    || $self->query->url . '/resources/data_file'; # warn $rtn_url;

    # needs http(s) prefix:
    unless ($url =~ m!\Ahttp(s?)://!) {
        my $msg  = $self->messages('admin')->{data_file}->{prefix_error};
        my $html = $self->forward( 'data_file', $msg );
        return $self->fill_form($html);
    }

    # expect one of following suffixes:
    my ($suffix) = $url =~ /(zip|csv)\Z/;

    # return to source if no / incorrect suffix:
    if (! $suffix) {
        my $msg  = $self->messages('admin')->{data_file}->{url_error};
        my $html = $self->forward( 'data_file', $msg );
        return $self->fill_form($html);
    }

    # filename for temp file:
    my $temp_file = $self->_path_to_data_files . '/temp.' . $suffix;

    # io($url) > io($temp_file); # IO::All wrapper round LWP::UserAgent - gives error

    my $ua = LWP::UserAgent->new;

    my $response;

    # get the file or give up after 15secs:
	eval {
		local $SIG{ALRM} = sub {
			die 'LWP::UserAgent timeout' . "\n"; # NB \n required
		};
		alarm 15;
		$response = $ua->get( $url, ':content_file' => $temp_file );
		alarm 0;
	};

    unless ($response->is_success) {
        my $html = $self->forward( 'data_file', $response->status_line );
        return $self->fill_form($html);
    }
    # TODO: rest of block almost identical to data_file_upload()
    my $filename;

    # unzip & get filename:
    if ($suffix eq 'zip') {
        $filename = $self->_unzip_file($temp_file); # returns empty on error (sets flash)
    }
    # must be .csv:
    else {
        $filename = $self->_get_filename_from_url($url);
    }

    # delete temp file now:
    io($temp_file)->unlink;

    if (! $filename) { # redirect (flash message set earlier):
        return $self->redirect( $rtn_url );
    }
    # update data file (returns true on success):
    elsif ( $self->_update_file($filename) ) {
        $self->flash( info => $self->messages('admin')->{data_file}->{update_success} );
    }
    else {
        $self->flash( error => $self->messages('admin')->{data_file}->{update_failed} );
    }

    return $self->redirect( $rtn_url );
}

#-------------------------------------------------------------------------------
sub data_file_from_upload {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    $self->query->param('data_file') || return $self->forward('data_file');

    my $file = $self->query->upload('data_file'); # CGI method

    # expect one of following suffixes:
    my ($suffix) = $file =~ /(zip|csv)\Z/;

    # return to source if no / incorrect suffix:
    if (! $suffix) {
        my $msg  = $self->messages('admin')->{data_file}->{url_error};
        my $html = $self->forward( 'data_file', $msg );
        return $self->fill_form($html);
    }

    my $temp_file = $self->_path_to_data_files . '/temp.' . $suffix;

    { # write to file:
        # Write contents to temp file:
        my $buffer;
        while ( read( $file, $buffer, 16_384 ) ) {
            io($temp_file)->append($buffer);
        }
    }
    # TODO: rest of block almost identical to data_file_url()
    my $filename;

    if ( $suffix eq 'zip' ) {
        $filename = $self->_unzip_file($temp_file); # returns empty on error (sets flash)
    }
    else {
        $filename = $self->_get_filename_from_url($file);
    }

    # delete temp file now:
    io($temp_file)->unlink;

    if (! $filename) { # redirect (flash message set earlier):
        return $self->redirect( $self->query->url . '/resources/data_file' );
    }
    # update data file (returns true on success):
    elsif ( $self->_update_file($filename) ) {
        $self->flash( info => $self->messages('admin')->{data_file}->{update_success} );
    }
    else {
        $self->flash( error => $self->messages('admin')->{data_file}->{update_failed} );
    }

    return $self->redirect( $self->query->url . '/resources/data_file' );
}

#-------------------------------------------------------------------------------
sub load_image_files {
	my ($self, $request) = @_;

    my $dir = $self->_get_destination_dir($request); # warn $dir;

    return unless -d $dir;

    my $contents;

    foreach my $item ( io($dir)->all ) { # warn $item->type;
        next unless $item->type eq 'file';

        # separate images & other files:
        if ( $item->filename =~ /\.(png|jp(e?)g|gif)\Z/i ) {
            push @{ $contents->{images} }, $item->filename;
        }
        else {
            push @{ $contents->{files} }, $item->filename;
        }
    } # warn Dumper $contents;

    return $contents;
}

#-------------------------------------------------------------------------------
sub check_image_file {
	my ($self, $request_id) = @_;

    # get request:
    my $request = $self->model('Request')->get_request($request_id);

	# get the uploaded file from the web page
	my $fh = $self->query->upload('image_file'); # CGI method
	$self->stash( fh => $fh ); # warn $fh;

    my $destination_dir = $self->_get_destination_dir($request->as_tree);

	# get the filename from the full path for naming the image correctly
    if ( $ENV{'HTTP_USER_AGENT'} =~ /MSIE/ ) { # or can't split filename from path
        File::Basename::fileparse_set_fstype('MSWin32');
    }

	my $filename = File::Basename::basename($fh); # warn $filename;
    { # remove any chars that generate malformed url's when loading for file deletion:
        my $re = qr(\+);
        if ( $filename =~ /$re/ ) {
            $filename =~ s/$re/_/g; # warn $filename;
            my $msg = $self->messages('file_upload')->{reserved_chars};
            $self->flash( warning =>  $msg );
        }
    }

    my $file = join '/', $destination_dir, $filename; # warn $file;

	# stash file for later use:
	$self->stash( filename => $file ); # warn $file;

	# returns true if file already exist:
	return ( -e $file ); # if ( -e $file ) { warn 'EXISTS' }
}

#-------------------------------------------------------------------------------
sub upload_image {
	my $self = shift;

    my $destination_dir = $self->stash->{destination_dir};

	unless (-d $destination_dir) {
        # mode/mask will NOT set 'w' mode on 'group' & 'other'; umask setting
        # overrides this; "umask 002" should allow dirs to be group-writable but doesn't
        make_path($destination_dir, { mode => 0776, error => \my $err });
        return Dumper $err if @$err;
	}

	my $fh = $self->stash->{fh};

    my $file = $self->stash->{filename};

	# write to file:
	if ($^O =~ /MSWin32/) { # doesn't work with read() method
		open(OUT,">$file") or return "cannot open output file $!";
		binmode(OUT);
		while(<$fh>) {
			print OUT $_;
		}
		close(OUT);
	}
   else {
        my $buffer;
        while ( read( $fh, $buffer, 16_384 ) ) {
            io($file)->append($buffer);
        }
    }

	# caller expects any return value to be error:
	return 0;
}

#-------------------------------------------------------------------------------
sub delete_image {
	my ($self, $args) = @_; # warn Dumper $args;

	my $request_data = $args->{request_data};
	my $filename     = $args->{filename};

    my $destination_dir = $self->_get_destination_dir($request_data);

	my $file = $destination_dir . '/' . $filename; # warn $file; return;
	eval { # getting: Can't find a class for method 'unlink'
        io($file)->unlink if -e $file; # warn $file;
    };
    return $@ if $@;
}

#-------------------------------------------------------------------------------
sub slurp_file_contents {
    my ($self, $request, $data_file) = @_;

    my $destination_dir = $self->_get_destination_dir($request);

    my @file_contents = io($destination_dir . '/' . $data_file)->slurp;

    return \@file_contents;
}

#-------------------------------------------------------------------------------
sub process_file_upload {
	my $self = shift;
	my $args = shift; # warn Dumper $args;

	my $target_dir = $args->{target_dir};
	my $filename   = $args->{filename};
	my $src_file   = $args->{src_file};

	use constant BUFFER_SIZE => 16_384;

	my $new_file = join '/', $target_dir, $filename; # warn Dumper $tmp_file;
	$new_file =~ s/\s/_/g; # substitute spaces

    # delete previous file if exists from failed previous invocation or we'll die at $fh stage:
    if (-e $new_file) {
        unlink $new_file || die "Couldn't delete previous $new_file file - $!";
    }

    # Write contents to output file
    my $buffer = undef;
    while ( read( $src_file, $buffer, BUFFER_SIZE ) ) {
		io($new_file)->append($buffer);
    }

	return 1;
}

#-------------------------------------------------------------------------------
sub get_file_by_filetype {
	my ($self, $args) = @_; # warn Dumper $args;

    my $request_id = $args->{request_id};
    my $file_type  = $args->{file_type}; # PB_CMP.pdf

    my $request = $self->model('Request')->get_request($request_id);

    my $dir = $self->_get_destination_dir($request); # warn $dir;

    my $file = sprintf '%.02d_%.05d_%s',
        $request->year - 2000, $request->request_number, $file_type; # warn $file;

    if ( -e $dir . '/' . $file ) { # if file exists, return components:
        my @parts = ( $request->year, $self->stash->{mini_dir},
            $request->request_number, $file );
        return \@parts;
    }
    return 0;
}

#-------------------------------------------------------------------------------
sub _get_destination_dir {
    my ($self, $request) = @_; # warn Dumper $request;

    my $path_to_app_root = $self->cfg('path_to_app_root');
	my $centre = $self->cfg('settings')->{_centre}; # warn $centre;

    # 1-99 = 0, 100-199 = 1, 200-299 = 2, etc
    my $i = int $request->{request_number} / 100; # warn $i;

    my $mini_dir = sprintf '%s-%s',
        100 * $i || 1, # default to 1 if 0; 1, 100, 200, 300, etc
        100 * $i + 99; # 99, 199, 299, etc

	my @parts = ( $path_to_app_root, 'static', 'files', $centre, $request->{year},
		$mini_dir, $request->{request_number} ); # warn [ $mini_dir, $destination_dir ];

    my $destination_dir = join '/', @parts; # warn $destination_dir;

    # stash for later:
    $self->stash(
        destination_dir => $destination_dir,
        mini_dir        => $mini_dir,
    );

    return $destination_dir;
}

#-------------------------------------------------------------------------------
sub _unzip_file {
    my $self = shift;
    my $file = shift; # warn $file;

    my $u = new IO::Uncompress::Unzip $file
        or die "Cannot open $file: $UnzipError";

    my ($status, $filename);
    # get filename of content:
    for ( $status = 1; ! $u->eof(); $status = $u->nextStream() ) {
        $filename = $u->getHeaderInfo()->{Name}; # warn $filename;
        last if $filename =~ /\.csv\Z/; # HSCIC now using csv + pdf in zip
    } # warn $filename;
    die "error processing $file: $!\n" if $status < 0 ;

    # check filename ends in .csv file - returns true if so:
    $self->_check_csv($filename) || return 0;

    # split $filename:
    my ($prefix, $suffix) = split '\.', $filename; # warn $prefix; warn $suffix;

    # check it's a permitted filename - returns true if so:
    $self->_check_permitted_filename($prefix) || return 0;

    # unzip file to temp.csv (to match filename if csv uploaded):
    unzip $file => $self->_path_to_data_files . '/temp.csv',
    # HSCIC now using csv + pdf in zip so need 'Name' => $filename (already checked .csv):
    Name => $filename || die "unzip failed: $UnzipError\n";

    return $prefix; # success
}

#-------------------------------------------------------------------------------
sub _get_filename_from_url {
    my $self = shift;
    my $url  = shift;

    my ($prefix) = $url =~ m!(\w+)\.csv\Z!; # warn $filename;

    # check it's a permitted filename - returns true if so:
    $self->_check_permitted_filename($prefix) || return 0;

    return $prefix;
}

#-------------------------------------------------------------------------------
sub _check_permitted_filename {
    my ($self, $filename) = @_;

    my @allowed = ();
    { # get list of permitted .csv files:
        my @contents = io($self->_path_to_data_files)->all; # get contents of dir
        for (@contents) { # warn $_->filename;
            next unless /csv\Z/;
            my ($prefix, $suffix) = split '\.', $_->filename;
            push @allowed, $prefix;
        } # warn Dumper \@allowed;
    }

    # check filename is one of permitted files:
    unless (grep lc $filename eq $_, @allowed) {
        my $msg = $self->messages('admin')->{data_file}->{unrecognised};
        $self->flash( error => sprintf $msg, $filename );
        return 0;
    }
    return 1; # OK
}

#-------------------------------------------------------------------------------
sub _update_file {
    my ($self, $filename) = @_; # warn $filename;

    # filename (without suffix) of destination file (ensure it's lc):
    my $outfile = $self->_path_to_data_files . '/' . lc $filename . '.csv'; # ODS files usually uc

    # move existing file out of the way:
    io($outfile)->rename($outfile . '.old') || return 0;

    # rename temp file to working filename:
    io($self->_path_to_data_files . '/temp.csv')->rename($outfile) || return 0;

    return 1; # success
}

#-------------------------------------------------------------------------------
sub _check_csv {
    my ($self, $str) = @_; # warn Dumper $str;

    # check it's a csv filename:
    unless ($str && $str =~ /\.csv\Z/) {
        $self->flash( error => $self->messages('admin')->{data_file}->{not_csv} );
        return 0;
    }
    return 1;
}

#-------------------------------------------------------------------------------
sub _path_to_data_files {
    my $self = shift;

    return $self->cfg->{path_to_app_root} . '/src/data';
}

1;