869 lines
24 KiB
Perl
869 lines
24 KiB
Perl
package Koha::File::Transport;
|
|
|
|
# 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 constant {
|
|
DEFAULT_TIMEOUT => 10,
|
|
TEST_FILE_NAME => '.koha_test_file',
|
|
TEST_FILE_CONTENT => "Hello, world!\n",
|
|
};
|
|
use JSON qw( decode_json encode_json );
|
|
use List::MoreUtils qw( any );
|
|
|
|
use Koha::Database;
|
|
use Koha::Exceptions::Object;
|
|
use Koha::Encryption;
|
|
|
|
use base qw(Koha::Object);
|
|
|
|
=head1 NAME
|
|
|
|
Koha::File::Transport - Base class for file transport handling
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use Koha::File::Transports;
|
|
|
|
# Create/retrieve a transport (polymorphic - returns SFTP/FTP/Local subclass)
|
|
my $transport = Koha::File::Transports->find($id);
|
|
|
|
# SIMPLIFIED API (recommended) - connection and directory management are automatic
|
|
# Connections are established on-demand and directories are managed automatically
|
|
|
|
# Example 1: Upload to custom path (connection happens automatically)
|
|
$transport->upload_file('/local/file.txt', 'remote.txt', { path => '/custom/dir/' });
|
|
|
|
# Example 2: Download using configured download_directory (auto-managed)
|
|
$transport->download_file('remote.txt', '/local/file.txt');
|
|
|
|
# Example 3: List files in custom directory (one-shot operation)
|
|
my $files = $transport->list_files({ path => '/some/directory/' });
|
|
|
|
# TRADITIONAL API - manual connection and directory management
|
|
# Useful when you need fine-grained control or want to perform multiple
|
|
# operations in the same directory without repeating the path
|
|
|
|
$transport->connect(); # Optional - will auto-connect if omitted
|
|
$transport->change_directory('/work/dir/'); # Sets working directory
|
|
$transport->upload_file('/local/1.txt', '1.txt');
|
|
$transport->upload_file('/local/2.txt', '2.txt'); # Uses same directory
|
|
my $files = $transport->list_files(); # Lists /work/dir/
|
|
$transport->rename_file('1.txt', '1_old.txt');
|
|
$transport->download_file('2.txt', '/local/2.txt');
|
|
$transport->disconnect(); # Optional - cleaned up automatically
|
|
|
|
# HYBRID APPROACH - mixing both APIs
|
|
# Once you explicitly set a directory, auto-management is disabled
|
|
|
|
$transport->change_directory('/work/dir/'); # Explicit directory change
|
|
$transport->upload_file('/local/file.txt', 'file.txt'); # Uses /work/dir/
|
|
$transport->list_files(); # Still uses /work/dir/
|
|
# The configured upload_directory/download_directory won't be used anymore
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Base class providing common functionality for FTP/SFTP/Local file transport.
|
|
|
|
This class supports two distinct usage patterns:
|
|
|
|
=head2 Simplified API (Auto-Managing)
|
|
|
|
The simplified API automatically manages connections and directories:
|
|
|
|
=over 4
|
|
|
|
=item * B<Automatic Connection Management>
|
|
|
|
You never need to call connect() or disconnect(). Connections are established
|
|
on-demand when you call file operation methods and are automatically cleaned
|
|
up when the object is destroyed.
|
|
|
|
=item * B<Flexible Directory Management>
|
|
|
|
Each file operation (upload_file, download_file, list_files) accepts an optional
|
|
options hashref with a 'path' key to specify a custom directory for that operation:
|
|
|
|
$transport->upload_file($local, $remote, { path => '/custom/dir/' });
|
|
|
|
If no path is provided, the transport uses its configured upload_directory
|
|
(for uploads) or download_directory (for downloads/listings).
|
|
|
|
=item * B<No State Maintained>
|
|
|
|
Each operation is independent. The transport doesn't remember which directory
|
|
you used in previous operations, making the API stateless and safe for
|
|
concurrent usage patterns.
|
|
|
|
=back
|
|
|
|
=head2 Traditional API (Explicit Control)
|
|
|
|
The traditional API provides manual control over connection and directory state:
|
|
|
|
=over 4
|
|
|
|
=item * B<Explicit Connection Control>
|
|
|
|
While connect() and disconnect() are still available and can be called explicitly,
|
|
they are entirely optional. The simplified API manages connections automatically.
|
|
|
|
=item * B<Stateful Directory Management>
|
|
|
|
Once you call change_directory() explicitly, the transport switches to "manual mode"
|
|
and remembers your working directory. Subsequent operations will use this directory
|
|
and automatic directory management is disabled:
|
|
|
|
$transport->change_directory('/work/');
|
|
$transport->upload_file($local, $remote); # Uses /work/, not upload_directory
|
|
$transport->list_files(); # Lists /work/, not download_directory
|
|
|
|
=item * B<Session-Based Operations>
|
|
|
|
This is useful when you need to perform multiple operations in the same directory
|
|
without repeating the path parameter each time.
|
|
|
|
=back
|
|
|
|
=head2 How It Works
|
|
|
|
The implementation uses a C<_user_set_directory> flag to track which mode is active:
|
|
|
|
=over 4
|
|
|
|
=item * When you call change_directory() explicitly, the flag is set to true
|
|
|
|
=item * When the flag is true, auto-directory management is disabled
|
|
|
|
=item * When the flag is false (default), operations use their path option or fall back to configured directories
|
|
|
|
=item * The flag is reset to false when a new connection is established
|
|
|
|
=back
|
|
|
|
This design allows you to mix both approaches in the same codebase, choosing
|
|
the right pattern for each use case.
|
|
|
|
=cut
|
|
|
|
=head1 API
|
|
|
|
=head2 Class methods
|
|
|
|
=head3 store
|
|
|
|
$server->store;
|
|
|
|
Overloaded store method that ensures directory paths end with a forward slash.
|
|
|
|
=cut
|
|
|
|
sub store {
|
|
my ($self) = @_;
|
|
|
|
# Encrypt sensitive data if changed
|
|
$self->_encrypt_sensitive_data();
|
|
|
|
# Normalize directory paths
|
|
for my $dir_field (qw(download_directory upload_directory)) {
|
|
my $dir = $self->$dir_field;
|
|
next unless $dir && $dir ne '';
|
|
$self->$dir_field( $dir . '/' ) unless substr( $dir, -1 ) eq '/';
|
|
}
|
|
|
|
my @config_fields = (qw(host port user_name password key_file upload_directory download_directory));
|
|
my $changed_config = ( !$self->in_storage || any { $self->_result->is_column_changed($_) } @config_fields ) ? 1 : 0;
|
|
|
|
# Store
|
|
$self->SUPER::store;
|
|
|
|
# Subclass triggers
|
|
my $subclass_map = {
|
|
'sftp' => 'Koha::File::Transport::SFTP',
|
|
'ftp' => 'Koha::File::Transport::FTP',
|
|
'local' => 'Koha::File::Transport::Local',
|
|
};
|
|
my $subclass = $subclass_map->{ $self->transport } || 'Koha::File::Transport';
|
|
$self = $subclass->_new_from_dbic( $self->_result );
|
|
$self->_post_store_trigger;
|
|
|
|
# Enqueue a connection test
|
|
if ($changed_config) {
|
|
require Koha::BackgroundJob::TestTransport;
|
|
Koha::BackgroundJob::TestTransport->new->enqueue( { transport_id => $self->id } );
|
|
}
|
|
|
|
# Return the updated object including the encrypt_sensitive_data
|
|
return $self;
|
|
}
|
|
|
|
=head3 _ensure_connected
|
|
|
|
$transport->_ensure_connected();
|
|
|
|
Internal method that ensures a connection exists, connecting if needed.
|
|
Returns true if connected, false if connection failed.
|
|
|
|
=cut
|
|
|
|
sub _ensure_connected {
|
|
my ($self) = @_;
|
|
|
|
# Check if already connected (transport-specific)
|
|
return 1 if $self->_is_connected();
|
|
|
|
# Attempt to connect (which will reset directory state)
|
|
return $self->connect();
|
|
}
|
|
|
|
=head3 _is_connected
|
|
|
|
my $connected = $transport->_is_connected();
|
|
|
|
Internal method to check if transport is currently connected.
|
|
Must be implemented by subclasses.
|
|
|
|
=cut
|
|
|
|
sub _is_connected {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _is_connected";
|
|
}
|
|
|
|
=head3 current_directory
|
|
|
|
my $path = $transport->current_directory();
|
|
|
|
Returns the transport's current remote working directory as a plain string,
|
|
without side effects. Automatically establishes a connection if needed.
|
|
|
|
B<Note:> This is a read-only query. It must never navigate the connection -
|
|
unlike some underlying libraries' own C<cwd()>/C<pwd()> methods, which differ
|
|
in meaning between backends (see subclass implementations).
|
|
|
|
B<Returns:> The current directory path on success, undef on failure. Check
|
|
object_messages() for details.
|
|
|
|
=cut
|
|
|
|
sub current_directory {
|
|
my ($self) = @_;
|
|
|
|
return unless $self->_ensure_connected();
|
|
|
|
return $self->_current_directory();
|
|
}
|
|
|
|
=head3 _current_directory
|
|
|
|
my $path = $transport->_current_directory();
|
|
|
|
Internal method that returns the protocol-specific current working
|
|
directory. Must be implemented by subclasses. Called by current_directory()
|
|
after connection verification.
|
|
|
|
=cut
|
|
|
|
sub _current_directory {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _current_directory";
|
|
}
|
|
|
|
=head3 _auto_change_directory
|
|
|
|
$transport->_auto_change_directory($dir_type, $custom_path);
|
|
|
|
Internal method that automatically changes to the appropriate directory.
|
|
$dir_type is 'upload' or 'download', $custom_path is optional override.
|
|
|
|
=cut
|
|
|
|
sub _auto_change_directory {
|
|
my ( $self, $dir_type, $custom_path ) = @_;
|
|
|
|
my $target_dir;
|
|
if ($custom_path) {
|
|
$target_dir = $custom_path;
|
|
} elsif ( $dir_type eq 'upload' ) {
|
|
$target_dir = $self->upload_directory;
|
|
} elsif ( $dir_type eq 'download' ) {
|
|
$target_dir = $self->download_directory;
|
|
}
|
|
|
|
return 1 unless $target_dir; # No directory to change to
|
|
|
|
# Call the internal _change_directory directly to avoid setting the flag
|
|
# Only explicit user calls to change_directory() should set the flag
|
|
return $self->_change_directory($target_dir);
|
|
}
|
|
|
|
=head3 plain_text_password
|
|
|
|
my $password = $server->plain_text_password;
|
|
|
|
Returns the decrypted plaintext password.
|
|
|
|
=cut
|
|
|
|
sub plain_text_password {
|
|
my ($self) = @_;
|
|
return unless $self->password;
|
|
return Koha::Encryption->new->decrypt_hex( $self->password );
|
|
}
|
|
|
|
=head3 plain_text_key
|
|
|
|
my $key = $server->plain_text_key;
|
|
|
|
Returns the decrypted plaintext key file.
|
|
|
|
=cut
|
|
|
|
sub plain_text_key {
|
|
my ($self) = @_;
|
|
return unless $self->key_file;
|
|
return Koha::Encryption->new->decrypt_hex( $self->key_file ) . "\n";
|
|
}
|
|
|
|
=head3 to_api
|
|
|
|
my $json = $transport->to_api;
|
|
|
|
Returns a JSON representation of the object suitable for API output,
|
|
excluding sensitive data.
|
|
|
|
=cut
|
|
|
|
sub to_api {
|
|
my ( $self, $params ) = @_;
|
|
|
|
my $json = $self->SUPER::to_api($params) or return;
|
|
delete @{$json}{qw(password key_file)}; # Remove sensitive data
|
|
$json->{status} = $self->status ? decode_json( $self->status ) : undef; # Decode json status
|
|
|
|
return $json;
|
|
}
|
|
|
|
=head3 to_api_mapping
|
|
|
|
This method returns the mapping for representing a Koha::File::Transport object
|
|
on the API.
|
|
|
|
=cut
|
|
|
|
sub to_api_mapping {
|
|
return { id => 'file_transport_id' };
|
|
}
|
|
|
|
=head3 test_connection
|
|
|
|
$transport->test_connection
|
|
|
|
Method to test the connection for the configuration of the current file server
|
|
|
|
=cut
|
|
|
|
sub test_connection {
|
|
my ($self) = @_;
|
|
|
|
$self->connect or return;
|
|
|
|
for my $dir_type (qw(download upload)) {
|
|
my $field = "${dir_type}_directory";
|
|
my $dir = $self->$field;
|
|
$dir ||= undef;
|
|
|
|
$self->change_directory(undef) or next;
|
|
$self->change_directory($dir) or next;
|
|
$self->list_files() or next;
|
|
}
|
|
|
|
my $return = 1;
|
|
my $messages = $self->object_messages;
|
|
for my $message ( @{$messages} ) {
|
|
$return = 0 if $message->type eq 'error';
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
=head2 Subclass methods
|
|
|
|
Interface methods that must be implemented by subclasses
|
|
|
|
=head3 connect
|
|
|
|
my $success = $transport->connect();
|
|
|
|
Establishes a connection to the remote server.
|
|
|
|
B<Note:> Calling this method is entirely optional. All file operations
|
|
(upload_file, download_file, list_files, etc.) automatically establish
|
|
a connection if one doesn't already exist. You only need to call this
|
|
explicitly if you want to verify connectivity before performing operations.
|
|
|
|
B<Returns:> True on success, undef on failure. Check object_messages() for details.
|
|
|
|
=cut
|
|
|
|
sub connect {
|
|
my ($self) = @_;
|
|
|
|
# Reset directory state on new connection
|
|
$self->{_user_set_directory} = 0;
|
|
|
|
return $self->_connect();
|
|
}
|
|
|
|
=head3 _connect
|
|
|
|
my $success = $transport->_connect();
|
|
|
|
Internal method that performs the protocol-specific connection operation.
|
|
Must be implemented by subclasses. Called by connect() after resetting
|
|
directory state.
|
|
|
|
=cut
|
|
|
|
sub _connect {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _connect";
|
|
}
|
|
|
|
=head3 upload_file
|
|
|
|
# Signature:
|
|
my $success = $transport->upload_file($local_file, $remote_file, \%options);
|
|
|
|
Uploads a file to the remote server. Automatically establishes a connection if needed.
|
|
|
|
B<Parameters:>
|
|
|
|
=over 4
|
|
|
|
=item * C<$local_file> - Path to local file to upload (required)
|
|
|
|
=item * C<$remote_file> - Remote filename (not a path, just the filename) (required)
|
|
|
|
=item * C<\%options> - Optional hashref with keys:
|
|
|
|
=over 4
|
|
|
|
=item * C<path> - Directory path to upload to. If provided, uses this directory
|
|
for this operation only (simplified API). If omitted, behavior depends on whether
|
|
change_directory() has been called explicitly (see DESCRIPTION).
|
|
|
|
=back
|
|
|
|
=back
|
|
|
|
B<Usage Patterns:>
|
|
|
|
# Pattern 1: Simplified API with custom path
|
|
$transport->upload_file('/tmp/data.csv', 'data.csv', { path => '/uploads/' });
|
|
|
|
# Pattern 2: Simplified API with configured upload_directory
|
|
$transport->upload_file('/tmp/data.csv', 'data.csv');
|
|
|
|
# Pattern 3: Traditional API with explicit directory
|
|
$transport->change_directory('/uploads/');
|
|
$transport->upload_file('/tmp/data.csv', 'data.csv');
|
|
|
|
B<Returns:> True on success, undef on failure. Check object_messages() for details.
|
|
|
|
=cut
|
|
|
|
sub upload_file {
|
|
my ( $self, $local_file, $remote_file, $options ) = @_;
|
|
|
|
return unless $self->_ensure_connected();
|
|
|
|
# Only auto-change directory if:
|
|
# 1. Options provided with custom path (simplified API), OR
|
|
# 2. No explicit directory set by user AND default upload_directory exists (traditional API)
|
|
if ( $options && $options->{path} ) {
|
|
|
|
# Simplified API - use custom path
|
|
return unless $self->_auto_change_directory( 'upload', $options->{path} );
|
|
} elsif ( !$self->{_user_set_directory} ) {
|
|
|
|
# Traditional API - use default directory only if user hasn't set one
|
|
return unless $self->_auto_change_directory( 'upload', undef );
|
|
}
|
|
|
|
return $self->_upload_file( $local_file, $remote_file );
|
|
}
|
|
|
|
=head3 _upload_file
|
|
|
|
$transport->_upload_file($local_file, $remote_file);
|
|
|
|
Internal method that performs the protocol-specific upload operation.
|
|
Must be implemented by subclasses. Called by upload_file after connection
|
|
and directory management.
|
|
|
|
=cut
|
|
|
|
sub _upload_file {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _upload_file";
|
|
}
|
|
|
|
=head3 download_file
|
|
|
|
# Signature:
|
|
my $success = $transport->download_file($remote_file, $local_file, \%options);
|
|
|
|
Downloads a file from the remote server. Automatically establishes a connection if needed.
|
|
|
|
B<Parameters:>
|
|
|
|
=over 4
|
|
|
|
=item * C<$remote_file> - Remote filename (not a path, just the filename) (required)
|
|
|
|
=item * C<$local_file> - Path where the downloaded file should be saved (required)
|
|
|
|
=item * C<\%options> - Optional hashref with keys:
|
|
|
|
=over 4
|
|
|
|
=item * C<path> - Directory path to download from. If provided, uses this directory
|
|
for this operation only (simplified API). If omitted, behavior depends on whether
|
|
change_directory() has been called explicitly (see DESCRIPTION).
|
|
|
|
=back
|
|
|
|
=back
|
|
|
|
B<Usage Patterns:>
|
|
|
|
# Pattern 1: Simplified API with custom path
|
|
$transport->download_file('data.csv', '/tmp/data.csv', { path => '/downloads/' });
|
|
|
|
# Pattern 2: Simplified API with configured download_directory
|
|
$transport->download_file('data.csv', '/tmp/data.csv');
|
|
|
|
# Pattern 3: Traditional API with explicit directory
|
|
$transport->change_directory('/downloads/');
|
|
$transport->download_file('data.csv', '/tmp/data.csv');
|
|
|
|
B<Returns:> True on success, undef on failure. Check object_messages() for details.
|
|
|
|
=cut
|
|
|
|
sub download_file {
|
|
my ( $self, $remote_file, $local_file, $options ) = @_;
|
|
|
|
return unless $self->_ensure_connected();
|
|
|
|
# Only auto-change directory if:
|
|
# 1. Options provided with custom path (simplified API), OR
|
|
# 2. No explicit directory set by user AND default download_directory exists (traditional API)
|
|
if ( $options && $options->{path} ) {
|
|
|
|
# Simplified API - use custom path
|
|
return unless $self->_auto_change_directory( 'download', $options->{path} );
|
|
} elsif ( !$self->{_user_set_directory} ) {
|
|
|
|
# Traditional API - use default directory only if user hasn't set one
|
|
return unless $self->_auto_change_directory( 'download', undef );
|
|
}
|
|
|
|
return $self->_download_file( $remote_file, $local_file );
|
|
}
|
|
|
|
=head3 _download_file
|
|
|
|
$transport->_download_file($remote_file, $local_file);
|
|
|
|
Internal method that performs the protocol-specific download operation.
|
|
Must be implemented by subclasses. Called by download_file after connection
|
|
and directory management.
|
|
|
|
=cut
|
|
|
|
sub _download_file {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _download_file";
|
|
}
|
|
|
|
=head3 rename_file
|
|
|
|
my $success = $transport->rename_file($old_name, $new_name);
|
|
|
|
Method for renaming a file on the current file server
|
|
|
|
=cut
|
|
|
|
sub rename_file {
|
|
my ( $self, $old_name, $new_name ) = @_;
|
|
|
|
return unless $self->_ensure_connected();
|
|
|
|
return $self->_rename_file( $old_name, $new_name );
|
|
}
|
|
|
|
=head3 _rename_file
|
|
|
|
$transport->_rename_file($old_name, $new_name);
|
|
|
|
Internal method that performs the protocol-specific file rename operation.
|
|
Must be implemented by subclasses. Called by rename_file after connection
|
|
verification.
|
|
|
|
=cut
|
|
|
|
sub _rename_file {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _rename_file";
|
|
}
|
|
|
|
=head3 list_files
|
|
|
|
# Signature:
|
|
my $files = $transport->list_files(\%options);
|
|
|
|
Lists files in a directory on the remote server. Automatically establishes a connection if needed.
|
|
|
|
B<Parameters:>
|
|
|
|
=over 4
|
|
|
|
=item * C<\%options> - Optional hashref with keys:
|
|
|
|
=over 4
|
|
|
|
=item * C<path> - Directory path to list files from. If provided, uses this directory
|
|
for this operation only (simplified API). If omitted, behavior depends on whether
|
|
change_directory() has been called explicitly (see DESCRIPTION).
|
|
|
|
=back
|
|
|
|
=back
|
|
|
|
B<Usage Patterns:>
|
|
|
|
# Pattern 1: Simplified API with custom path
|
|
my $files = $transport->list_files({ path => '/incoming/' });
|
|
|
|
# Pattern 2: Simplified API with configured download_directory
|
|
my $files = $transport->list_files();
|
|
|
|
# Pattern 3: Traditional API with explicit directory
|
|
$transport->change_directory('/incoming/');
|
|
my $files = $transport->list_files();
|
|
|
|
B<Returns:> Arrayref of hashrefs on success, undef on failure. Each hashref contains
|
|
file metadata (filename, size, permissions, etc.). The exact structure varies by
|
|
transport type but always includes a 'filename' key.
|
|
|
|
=cut
|
|
|
|
sub list_files {
|
|
my ( $self, $options ) = @_;
|
|
|
|
return unless $self->_ensure_connected();
|
|
|
|
# Only auto-change directory if:
|
|
# 1. Options provided with custom path (simplified API), OR
|
|
# 2. No explicit directory set by user AND default download_directory exists (traditional API)
|
|
if ( $options && $options->{path} ) {
|
|
|
|
# Simplified API - use custom path
|
|
return unless $self->_auto_change_directory( 'download', $options->{path} );
|
|
} elsif ( !$self->{_user_set_directory} ) {
|
|
|
|
# Traditional API - use default directory only if user hasn't set one
|
|
return unless $self->_auto_change_directory( 'download', undef );
|
|
}
|
|
|
|
return $self->_list_files();
|
|
}
|
|
|
|
=head3 _list_files
|
|
|
|
my $files = $transport->_list_files();
|
|
|
|
Internal method that performs the protocol-specific file listing operation.
|
|
Must be implemented by subclasses. Called by list_files after connection
|
|
and directory management.
|
|
|
|
=cut
|
|
|
|
sub _list_files {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _list_files";
|
|
}
|
|
|
|
=head3 disconnect
|
|
|
|
$transport->disconnect();
|
|
|
|
Closes the connection to the remote server.
|
|
|
|
B<Note:> Calling this method is entirely optional. Connections are automatically
|
|
cleaned up when the transport object is destroyed (goes out of scope). You only
|
|
need to call this explicitly if you want to free resources before the object
|
|
is destroyed, such as in long-running processes.
|
|
|
|
B<Returns:> True on success, undef on failure.
|
|
|
|
=cut
|
|
|
|
sub disconnect {
|
|
my ($self) = @_;
|
|
|
|
# Reset directory state when disconnecting
|
|
$self->{_user_set_directory} = 0;
|
|
|
|
return $self->_disconnect();
|
|
}
|
|
|
|
=head3 _disconnect
|
|
|
|
$transport->_disconnect();
|
|
|
|
Internal method that performs the protocol-specific disconnection operation.
|
|
Must be implemented by subclasses. Called by disconnect() after resetting
|
|
directory state.
|
|
|
|
=cut
|
|
|
|
sub _disconnect {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _disconnect";
|
|
}
|
|
|
|
=head3 change_directory
|
|
|
|
my $success = $transport->change_directory($path);
|
|
|
|
Changes the current working directory on the remote server.
|
|
|
|
B<Important:> Calling this method explicitly switches the transport to "manual mode"
|
|
and disables automatic directory management. After calling this method, all subsequent
|
|
file operations will use this directory (or relative paths from it) until you call
|
|
change_directory() again or create a new connection.
|
|
|
|
B<Parameters:>
|
|
|
|
=over 4
|
|
|
|
=item * C<$path> - Directory path to change to (required)
|
|
|
|
=back
|
|
|
|
B<Example:>
|
|
|
|
# After calling change_directory explicitly:
|
|
$transport->change_directory('/work/dir/');
|
|
$transport->upload_file($local, $remote); # Uses /work/dir/, not upload_directory
|
|
$transport->list_files(); # Lists /work/dir/, not download_directory
|
|
|
|
B<Returns:> True on success, undef on failure. Check object_messages() for details.
|
|
|
|
=cut
|
|
|
|
sub change_directory {
|
|
my ( $self, $path ) = @_;
|
|
|
|
# Mark that user has explicitly set a directory
|
|
# This prevents auto-directory management from interfering
|
|
$self->{_user_set_directory} = 1;
|
|
|
|
return $self->_change_directory($path);
|
|
}
|
|
|
|
=head3 _change_directory
|
|
|
|
my $success = $transport->_change_directory($path);
|
|
|
|
Internal method that performs the protocol-specific directory change operation.
|
|
Must be implemented by subclasses. Called by change_directory() after setting
|
|
the _user_set_directory flag.
|
|
|
|
=cut
|
|
|
|
sub _change_directory {
|
|
my ($self) = @_;
|
|
die "Subclass must implement _change_directory";
|
|
}
|
|
|
|
=head3 _post_store_trigger
|
|
|
|
$server->_post_store_trigger;
|
|
|
|
Method triggered by parent store to allow local additions to the store call
|
|
|
|
=cut
|
|
|
|
sub _post_store_trigger {
|
|
my ($self) = @_;
|
|
|
|
#Subclass may implement a _post_store_trigger as required
|
|
return $self;
|
|
}
|
|
|
|
=head2 Internal methods
|
|
|
|
=head3 _encrypt_sensitive_data
|
|
|
|
Handle encryption of sensitive data
|
|
|
|
=cut
|
|
|
|
sub _encrypt_sensitive_data {
|
|
my ($self) = @_;
|
|
my $encryption = Koha::Encryption->new;
|
|
|
|
# Only encrypt if the value has changed ($self->_result->is_column_changed from Koha::Object)
|
|
if ( ( !$self->in_storage || $self->_result->is_column_changed('password') ) && $self->password ) {
|
|
$self->password( $encryption->encrypt_hex( $self->password ) );
|
|
}
|
|
|
|
if ( ( !$self->in_storage || $self->_result->is_column_changed('key_file') ) && $self->key_file ) {
|
|
$self->key_file( $encryption->encrypt_hex( _dos2unix( $self->key_file ) ) );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
=head3 _dos2unix
|
|
|
|
Return a CR-free string from an input
|
|
|
|
=cut
|
|
|
|
sub _dos2unix {
|
|
my $dosStr = shift;
|
|
|
|
return $dosStr =~ s/\015\012/\012/gr;
|
|
}
|
|
|
|
=head3 _type
|
|
|
|
Return type of Object relating to Schema Result
|
|
|
|
=cut
|
|
|
|
sub _type {
|
|
return 'FileTransport';
|
|
}
|
|
|
|
1;
|