332 lines
7.8 KiB
Perl
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;
|