Koha-clms/Koha/File/Transport/SFTP.pm

332 lines
7.8 KiB
Perl

package Koha::File::Transport::SFTP;
# This file is part of Koha.
#
# Koha 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 3 of the License, or
# (at your option) any later version.
#
# Koha 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 Koha; if not, see <https://www.gnu.org/licenses>.
use Modern::Perl;
use Koha::Logger;
use File::Spec;
use IO::File;
use Net::SFTP::Foreign;
use Try::Tiny;
use base qw(Koha::File::Transport);
=head1 NAME
Koha::File::Transport::SFTP - SFTP implementation of file transport
=head2 Class methods
=head3 connect
my $success = $self->connect;
Start the SFTP transport connect, returns true on success or undefined on failure.
=cut
sub connect {
my ($self) = @_;
my $operation = "connection";
# String to capture STDERR output
$self->{stderr_capture} = '';
open my $stderr_fh, '>', \$self->{stderr_capture} or die "Can't open scalar as filehandle: $!";
$self->{connection} = Net::SFTP::Foreign->new(
host => $self->host,
port => $self->port,
user => $self->user_name,
password => $self->plain_text_password,
$self->_locate_key_file ? ( key_path => $self->_locate_key_file ) : (),
timeout => $self->DEFAULT_TIMEOUT,
stderr_fh => $stderr_fh,
more => [qw( -v -o StrictHostKeyChecking=no)],
);
$self->{stderr_fh} = $stderr_fh;
return $self->_abort_operation($operation) if ( $self->{connection}->error );
$self->add_message(
{
message => $operation,
type => 'success',
payload => {
status => $self->{connection}->status,
error => $self->{connection}->error,
path => $self->{connection}->cwd
}
}
);
return 1;
}
=head3 upload_file
my $success = $transport->upload_file($fh);
Passed a filehandle, this will upload the file to the current directory of the server connection.
Returns true on success or undefined on failure.
=cut
sub upload_file {
my ( $self, $local_file, $remote_file ) = @_;
my $operation = "upload";
$self->{connection}->put( $local_file, $remote_file ) or return $self->_abort_operation($operation);
$self->add_message(
{
message => $operation,
type => 'success',
payload => {
status => $self->{connection}->status,
error => $self->{connection}->error,
path => $self->{connection}->cwd
}
}
);
return 1;
}
=head3 download_file
my $success = $transport->download_file($filename);
Passed a filename, this will download the file from the current directory of the server connection.
Returns true on success or undefined on failure.
=cut
sub download_file {
my ( $self, $remote_file, $local_file ) = @_;
my $operation = 'download';
$self->{connection}->get( $remote_file, $local_file ) or return $self->_abort_operation($operation);
$self->add_message(
{
message => $operation,
type => 'success',
payload => {
status => $self->{connection}->status,
error => $self->{connection}->error,
path => $self->{connection}->cwd
}
}
);
return 1;
}
=head3 change_directory
my $success = $server->change_directory($directory);
Passed a directory name, this will change the current directory of the server connection.
Returns true on success or undefined on failure.
=cut
sub change_directory {
my ( $self, $remote_directory ) = @_;
my $operation = 'change_directory';
$self->{connection}->setcwd($remote_directory) or return $self->_abort_operation( $operation, $remote_directory );
$self->add_message(
{
message => $operation,
type => 'success',
payload => {
status => $self->{connection}->status,
error => $self->{connection}->error,
path => $self->{connection}->cwd
}
}
);
return 1;
}
=head3 list_files
my @files = $server->list_files;
Returns an array of filenames found in the current directory of the server connection.
=cut
sub list_files {
my ($self) = @_;
my $operation = "list";
my $file_list = $self->{connection}->ls or return $self->_abort_operation($operation);
$self->add_message(
{
message => $operation,
type => 'success',
payload => {
status => $self->{connection}->status,
error => $self->{connection}->error,
path => $self->{connection}->cwd
}
}
);
return $file_list;
}
=head2 Internal methods
=head3 _post_store_trigger
$server->post_store_trigger;
Local trigger run by the parent store method after storage.
Ensures key_file also gets written to the filesystem.
=cut
sub _post_store_trigger {
my ($self) = @_;
$self->_write_key_file;
return $self;
}
=head3 _write_key_file
my $success = $server->_write_key_file;
Writes the keyfile from the db into a file.
Returns 1 on success, undef on failure.
=cut
sub _write_key_file {
my ($self) = @_;
return unless $self->plain_text_key;
my $upload_path = C4::Context->config('upload_path') or return;
my $logger = Koha::Logger->get;
my $key_path = File::Spec->catdir( $upload_path, 'ssh_keys' );
my $key_file = File::Spec->catfile( $key_path, 'id_ssh_' . $self->id );
mkdir $key_path unless -d $key_path;
unlink $key_file if -f $key_file;
my $fh = IO::File->new( $key_file, 'w' ) or return;
try {
chmod 0600, $key_file if -f $key_file;
print $fh $self->plain_text_key;
close $fh or $logger->warn("Failed to close key file: $!");
return 1;
} catch {
$logger->warn("Error writing key file: $_");
close $fh;
return;
};
}
=head3 _locate_key_file
my $path = $server->_locate_key_file;
Returns the keyfile's path if it exists, undef otherwise.
=cut
sub _locate_key_file {
my ($self) = @_;
my $upload_path = C4::Context->config('upload_path') or return;
my $key_file = File::Spec->catfile(
$upload_path,
'ssh_keys',
'id_ssh_' . $self->id
);
return ( -f $key_file ) ? $key_file : undef;
}
=head3 _abort_operation
Helper method to abort the current operation and return.
=cut
sub _abort_operation {
my ( $self, $operation, $path ) = @_;
my $stderr = $self->{stderr_capture};
$self->{stderr_capture} = '';
my $payload = {
status => $self->{connection}->status,
error => $self->{connection}->error,
path => $path ? $path : $self->{connection}->cwd,
error_raw => $stderr
};
$self->add_message(
{
message => $operation,
type => 'error',
payload => $payload
}
);
if ( $self->{connection} ) {
$self->{connection}->abort;
}
my $status = {
status => 'errors',
operations => [ { code => $operation, status => 'error', detail => $payload } ]
};
$self->set( { status => encode_json($status) } )->store();
return;
}
=head3 DESTROY
Ensure proper cleanup of open filehandles
=cut
sub DESTROY {
my ($self) = @_;
# Ensure the filehandle is closed properly
if ( $self->{stderr_fh} ) {
close $self->{stderr_fh} or warn "Failed to close STDERR filehandle: $!";
}
}
1;