View | Details | Raw Unified | Return to bug 40811
Collapse All | Expand All

(-)a/Koha/File/Transport.pm (-29 / +517 lines)
Lines 35-43 use base qw(Koha::Object); Link Here
35
35
36
Koha::File::Transport - Base class for file transport handling
36
Koha::File::Transport - Base class for file transport handling
37
37
38
=head1 SYNOPSIS
39
40
    use Koha::File::Transports;
41
42
    # Create/retrieve a transport (polymorphic - returns SFTP/FTP/Local subclass)
43
    my $transport = Koha::File::Transports->find($id);
44
45
    # SIMPLIFIED API (recommended) - connection and directory management are automatic
46
    # Connections are established on-demand and directories are managed automatically
47
48
    # Example 1: Upload to custom path (connection happens automatically)
49
    $transport->upload_file('/local/file.txt', 'remote.txt', { path => '/custom/dir/' });
50
51
    # Example 2: Download using configured download_directory (auto-managed)
52
    $transport->download_file('remote.txt', '/local/file.txt');
53
54
    # Example 3: List files in custom directory (one-shot operation)
55
    my $files = $transport->list_files({ path => '/some/directory/' });
56
57
    # TRADITIONAL API - manual connection and directory management
58
    # Useful when you need fine-grained control or want to perform multiple
59
    # operations in the same directory without repeating the path
60
61
    $transport->connect();                          # Optional - will auto-connect if omitted
62
    $transport->change_directory('/work/dir/');     # Sets working directory
63
    $transport->upload_file('/local/1.txt', '1.txt');
64
    $transport->upload_file('/local/2.txt', '2.txt'); # Uses same directory
65
    my $files = $transport->list_files();           # Lists /work/dir/
66
    $transport->rename_file('1.txt', '1_old.txt');
67
    $transport->download_file('2.txt', '/local/2.txt');
68
    $transport->disconnect();                       # Optional - cleaned up automatically
69
70
    # HYBRID APPROACH - mixing both APIs
71
    # Once you explicitly set a directory, auto-management is disabled
72
73
    $transport->change_directory('/work/dir/');     # Explicit directory change
74
    $transport->upload_file('/local/file.txt', 'file.txt');  # Uses /work/dir/
75
    $transport->list_files();                       # Still uses /work/dir/
76
    # The configured upload_directory/download_directory won't be used anymore
77
38
=head1 DESCRIPTION
78
=head1 DESCRIPTION
39
79
40
Base class providing common functionality for FTP/SFTP file transport.
80
Base class providing common functionality for FTP/SFTP/Local file transport.
81
82
This class supports two distinct usage patterns:
83
84
=head2 Simplified API (Auto-Managing)
85
86
The simplified API automatically manages connections and directories:
87
88
=over 4
89
90
=item * B<Automatic Connection Management>
91
92
You never need to call connect() or disconnect(). Connections are established
93
on-demand when you call file operation methods and are automatically cleaned
94
up when the object is destroyed.
95
96
=item * B<Flexible Directory Management>
97
98
Each file operation (upload_file, download_file, list_files) accepts an optional
99
options hashref with a 'path' key to specify a custom directory for that operation:
100
101
    $transport->upload_file($local, $remote, { path => '/custom/dir/' });
102
103
If no path is provided, the transport uses its configured upload_directory
104
(for uploads) or download_directory (for downloads/listings).
105
106
=item * B<No State Maintained>
107
108
Each operation is independent. The transport doesn't remember which directory
109
you used in previous operations, making the API stateless and safe for
110
concurrent usage patterns.
111
112
=back
113
114
=head2 Traditional API (Explicit Control)
115
116
The traditional API provides manual control over connection and directory state:
117
118
=over 4
119
120
=item * B<Explicit Connection Control>
121
122
While connect() and disconnect() are still available and can be called explicitly,
123
they are entirely optional. The simplified API manages connections automatically.
124
125
=item * B<Stateful Directory Management>
126
127
Once you call change_directory() explicitly, the transport switches to "manual mode"
128
and remembers your working directory. Subsequent operations will use this directory
129
and automatic directory management is disabled:
130
131
    $transport->change_directory('/work/');
132
    $transport->upload_file($local, $remote);     # Uses /work/, not upload_directory
133
    $transport->list_files();                     # Lists /work/, not download_directory
134
135
=item * B<Session-Based Operations>
136
137
This is useful when you need to perform multiple operations in the same directory
138
without repeating the path parameter each time.
139
140
=back
141
142
=head2 How It Works
143
144
The implementation uses a C<_user_set_directory> flag to track which mode is active:
145
146
=over 4
147
148
=item * When you call change_directory() explicitly, the flag is set to true
149
150
=item * When the flag is true, auto-directory management is disabled
151
152
=item * When the flag is false (default), operations use their path option or fall back to configured directories
153
154
=item * The flag is reset to false when a new connection is established
155
156
=back
157
158
This design allows you to mix both approaches in the same codebase, choosing
159
the right pattern for each use case.
41
160
42
=cut
161
=cut
43
162
Lines 92-97 sub store { Link Here
92
    return $self;
211
    return $self;
93
}
212
}
94
213
214
=head3 _ensure_connected
215
216
    $transport->_ensure_connected();
217
218
Internal method that ensures a connection exists, connecting if needed.
219
Returns true if connected, false if connection failed.
220
221
=cut
222
223
sub _ensure_connected {
224
    my ($self) = @_;
225
226
    # Check if already connected (transport-specific)
227
    return 1 if $self->_is_connected();
228
229
    # Attempt to connect (which will reset directory state)
230
    return $self->connect();
231
}
232
233
=head3 _is_connected
234
235
    my $connected = $transport->_is_connected();
236
237
Internal method to check if transport is currently connected.
238
Must be implemented by subclasses.
239
240
=cut
241
242
sub _is_connected {
243
    my ($self) = @_;
244
    die "Subclass must implement _is_connected";
245
}
246
247
=head3 _auto_change_directory
248
249
    $transport->_auto_change_directory($dir_type, $custom_path);
250
251
Internal method that automatically changes to the appropriate directory.
252
$dir_type is 'upload' or 'download', $custom_path is optional override.
253
254
=cut
255
256
sub _auto_change_directory {
257
    my ( $self, $dir_type, $custom_path ) = @_;
258
259
    my $target_dir;
260
    if ($custom_path) {
261
        $target_dir = $custom_path;
262
    } elsif ( $dir_type eq 'upload' ) {
263
        $target_dir = $self->upload_directory;
264
    } elsif ( $dir_type eq 'download' ) {
265
        $target_dir = $self->download_directory;
266
    }
267
268
    return 1 unless $target_dir;    # No directory to change to
269
270
    # Call the internal _change_directory directly to avoid setting the flag
271
    # Only explicit user calls to change_directory() should set the flag
272
    return $self->_change_directory($target_dir);
273
}
274
95
=head3 plain_text_password
275
=head3 plain_text_password
96
276
97
    my $password = $server->plain_text_password;
277
    my $password = $server->plain_text_password;
Lines 188-254 Interface methods that must be implemented by subclasses Link Here
188
368
189
=head3 connect
369
=head3 connect
190
370
191
    $transport->connect();
371
    my $success = $transport->connect();
372
373
Establishes a connection to the remote server.
192
374
193
Method for connecting the current transport to the file server
375
B<Note:> Calling this method is entirely optional. All file operations
376
(upload_file, download_file, list_files, etc.) automatically establish
377
a connection if one doesn't already exist. You only need to call this
378
explicitly if you want to verify connectivity before performing operations.
379
380
B<Returns:> True on success, undef on failure. Check object_messages() for details.
194
381
195
=cut
382
=cut
196
383
197
sub connect {
384
sub connect {
198
    my ($self) = @_;
385
    my ($self) = @_;
199
    die "Subclass must implement connect";
386
387
    # Reset directory state on new connection
388
    $self->{_user_set_directory} = 0;
389
390
    return $self->_connect();
391
}
392
393
=head3 _connect
394
395
    my $success = $transport->_connect();
396
397
Internal method that performs the protocol-specific connection operation.
398
Must be implemented by subclasses. Called by connect() after resetting
399
directory state.
400
401
=cut
402
403
sub _connect {
404
    my ($self) = @_;
405
    die "Subclass must implement _connect";
200
}
406
}
201
407
202
=head3 upload_file
408
=head3 upload_file
203
409
204
    $transport->upload_file($file);
410
    # Signature:
411
    my $success = $transport->upload_file($local_file, $remote_file, \%options);
412
413
Uploads a file to the remote server. Automatically establishes a connection if needed.
414
415
B<Parameters:>
416
417
=over 4
418
419
=item * C<$local_file> - Path to local file to upload (required)
420
421
=item * C<$remote_file> - Remote filename (not a path, just the filename) (required)
422
423
=item * C<\%options> - Optional hashref with keys:
424
425
=over 4
426
427
=item * C<path> - Directory path to upload to. If provided, uses this directory
428
for this operation only (simplified API). If omitted, behavior depends on whether
429
change_directory() has been called explicitly (see DESCRIPTION).
430
431
=back
432
433
=back
434
435
B<Usage Patterns:>
436
437
    # Pattern 1: Simplified API with custom path
438
    $transport->upload_file('/tmp/data.csv', 'data.csv', { path => '/uploads/' });
439
440
    # Pattern 2: Simplified API with configured upload_directory
441
    $transport->upload_file('/tmp/data.csv', 'data.csv');
205
442
206
Method for uploading a file to the current file server
443
    # Pattern 3: Traditional API with explicit directory
444
    $transport->change_directory('/uploads/');
445
    $transport->upload_file('/tmp/data.csv', 'data.csv');
446
447
B<Returns:> True on success, undef on failure. Check object_messages() for details.
207
448
208
=cut
449
=cut
209
450
210
sub upload_file {
451
sub upload_file {
211
    my ( $self, $local_file, $remote_file ) = @_;
452
    my ( $self, $local_file, $remote_file, $options ) = @_;
212
    die "Subclass must implement upload_file";
453
454
    return unless $self->_ensure_connected();
455
456
    # Only auto-change directory if:
457
    # 1. Options provided with custom path (simplified API), OR
458
    # 2. No explicit directory set by user AND default upload_directory exists (traditional API)
459
    if ( $options && $options->{path} ) {
460
461
        # Simplified API - use custom path
462
        return unless $self->_auto_change_directory( 'upload', $options->{path} );
463
    } elsif ( !$self->{_user_set_directory} ) {
464
465
        # Traditional API - use default directory only if user hasn't set one
466
        return unless $self->_auto_change_directory( 'upload', undef );
467
    }
468
469
    return $self->_upload_file( $local_file, $remote_file );
213
}
470
}
214
471
215
=head3 download_file
472
=head3 _upload_file
216
473
217
    $transport->download_file($file);
474
    $transport->_upload_file($local_file, $remote_file);
218
475
219
Method for downloading a file from the current file server
476
Internal method that performs the protocol-specific upload operation.
477
Must be implemented by subclasses. Called by upload_file after connection
478
and directory management.
220
479
221
=cut
480
=cut
222
481
223
sub download_file {
482
sub _upload_file {
224
    my ( $self, $remote_file, $local_file ) = @_;
483
    my ($self) = @_;
225
    die "Subclass must implement download_file";
484
    die "Subclass must implement _upload_file";
226
}
485
}
227
486
228
=head3 change_directory
487
=head3 download_file
488
489
    # Signature:
490
    my $success = $transport->download_file($remote_file, $local_file, \%options);
229
491
230
    my $files = $transport->change_directory($path);
492
Downloads a file from the remote server. Automatically establishes a connection if needed.
231
493
232
Method for changing the current directory on the connected file server
494
B<Parameters:>
495
496
=over 4
497
498
=item * C<$remote_file> - Remote filename (not a path, just the filename) (required)
499
500
=item * C<$local_file> - Path where the downloaded file should be saved (required)
501
502
=item * C<\%options> - Optional hashref with keys:
503
504
=over 4
505
506
=item * C<path> - Directory path to download from. If provided, uses this directory
507
for this operation only (simplified API). If omitted, behavior depends on whether
508
change_directory() has been called explicitly (see DESCRIPTION).
509
510
=back
511
512
=back
513
514
B<Usage Patterns:>
515
516
    # Pattern 1: Simplified API with custom path
517
    $transport->download_file('data.csv', '/tmp/data.csv', { path => '/downloads/' });
518
519
    # Pattern 2: Simplified API with configured download_directory
520
    $transport->download_file('data.csv', '/tmp/data.csv');
521
522
    # Pattern 3: Traditional API with explicit directory
523
    $transport->change_directory('/downloads/');
524
    $transport->download_file('data.csv', '/tmp/data.csv');
525
526
B<Returns:> True on success, undef on failure. Check object_messages() for details.
233
527
234
=cut
528
=cut
235
529
236
sub change_directory {
530
sub download_file {
237
    my ( $self, $path ) = @_;
531
    my ( $self, $remote_file, $local_file, $options ) = @_;
238
    die "Subclass must implement change_directory";
532
533
    return unless $self->_ensure_connected();
534
535
    # Only auto-change directory if:
536
    # 1. Options provided with custom path (simplified API), OR
537
    # 2. No explicit directory set by user AND default download_directory exists (traditional API)
538
    if ( $options && $options->{path} ) {
539
540
        # Simplified API - use custom path
541
        return unless $self->_auto_change_directory( 'download', $options->{path} );
542
    } elsif ( !$self->{_user_set_directory} ) {
543
544
        # Traditional API - use default directory only if user hasn't set one
545
        return unless $self->_auto_change_directory( 'download', undef );
546
    }
547
548
    return $self->_download_file( $remote_file, $local_file );
239
}
549
}
240
550
241
=head3 list_files
551
=head3 _download_file
242
552
243
    my $files = $transport->list_files($path);
553
    $transport->_download_file($remote_file, $local_file);
244
554
245
Method for listing files in the current directory of the connected file server
555
Internal method that performs the protocol-specific download operation.
556
Must be implemented by subclasses. Called by download_file after connection
557
and directory management.
246
558
247
=cut
559
=cut
248
560
249
sub list_files {
561
sub _download_file {
250
    my ( $self, $path ) = @_;
562
    my ($self) = @_;
251
    die "Subclass must implement list_files";
563
    die "Subclass must implement _download_file";
252
}
564
}
253
565
254
=head3 rename_file
566
=head3 rename_file
Lines 261-280 Method for renaming a file on the current file server Link Here
261
573
262
sub rename_file {
574
sub rename_file {
263
    my ( $self, $old_name, $new_name ) = @_;
575
    my ( $self, $old_name, $new_name ) = @_;
264
    die "Subclass must implement rename_file";
576
577
    return unless $self->_ensure_connected();
578
579
    return $self->_rename_file( $old_name, $new_name );
580
}
581
582
=head3 _rename_file
583
584
    $transport->_rename_file($old_name, $new_name);
585
586
Internal method that performs the protocol-specific file rename operation.
587
Must be implemented by subclasses. Called by rename_file after connection
588
verification.
589
590
=cut
591
592
sub _rename_file {
593
    my ($self) = @_;
594
    die "Subclass must implement _rename_file";
595
}
596
597
=head3 list_files
598
599
    # Signature:
600
    my $files = $transport->list_files(\%options);
601
602
Lists files in a directory on the remote server. Automatically establishes a connection if needed.
603
604
B<Parameters:>
605
606
=over 4
607
608
=item * C<\%options> - Optional hashref with keys:
609
610
=over 4
611
612
=item * C<path> - Directory path to list files from. If provided, uses this directory
613
for this operation only (simplified API). If omitted, behavior depends on whether
614
change_directory() has been called explicitly (see DESCRIPTION).
615
616
=back
617
618
=back
619
620
B<Usage Patterns:>
621
622
    # Pattern 1: Simplified API with custom path
623
    my $files = $transport->list_files({ path => '/incoming/' });
624
625
    # Pattern 2: Simplified API with configured download_directory
626
    my $files = $transport->list_files();
627
628
    # Pattern 3: Traditional API with explicit directory
629
    $transport->change_directory('/incoming/');
630
    my $files = $transport->list_files();
631
632
B<Returns:> Arrayref of hashrefs on success, undef on failure. Each hashref contains
633
file metadata (filename, size, permissions, etc.). The exact structure varies by
634
transport type but always includes a 'filename' key.
635
636
=cut
637
638
sub list_files {
639
    my ( $self, $options ) = @_;
640
641
    return unless $self->_ensure_connected();
642
643
    # Only auto-change directory if:
644
    # 1. Options provided with custom path (simplified API), OR
645
    # 2. No explicit directory set by user AND default download_directory exists (traditional API)
646
    if ( $options && $options->{path} ) {
647
648
        # Simplified API - use custom path
649
        return unless $self->_auto_change_directory( 'download', $options->{path} );
650
    } elsif ( !$self->{_user_set_directory} ) {
651
652
        # Traditional API - use default directory only if user hasn't set one
653
        return unless $self->_auto_change_directory( 'download', undef );
654
    }
655
656
    return $self->_list_files();
657
}
658
659
=head3 _list_files
660
661
    my $files = $transport->_list_files();
662
663
Internal method that performs the protocol-specific file listing operation.
664
Must be implemented by subclasses. Called by list_files after connection
665
and directory management.
666
667
=cut
668
669
sub _list_files {
670
    my ($self) = @_;
671
    die "Subclass must implement _list_files";
265
}
672
}
266
673
267
=head3 disconnect
674
=head3 disconnect
268
675
269
    $transport->disconnect();
676
    $transport->disconnect();
270
677
271
Method for disconnecting from the current file server
678
Closes the connection to the remote server.
679
680
B<Note:> Calling this method is entirely optional. Connections are automatically
681
cleaned up when the transport object is destroyed (goes out of scope). You only
682
need to call this explicitly if you want to free resources before the object
683
is destroyed, such as in long-running processes.
684
685
B<Returns:> True on success, undef on failure.
272
686
273
=cut
687
=cut
274
688
275
sub disconnect {
689
sub disconnect {
276
    my ($self) = @_;
690
    my ($self) = @_;
277
    die "Subclass must implement disconnect";
691
692
    # Reset directory state when disconnecting
693
    $self->{_user_set_directory} = 0;
694
695
    return $self->_disconnect();
696
}
697
698
=head3 _disconnect
699
700
    $transport->_disconnect();
701
702
Internal method that performs the protocol-specific disconnection operation.
703
Must be implemented by subclasses. Called by disconnect() after resetting
704
directory state.
705
706
=cut
707
708
sub _disconnect {
709
    my ($self) = @_;
710
    die "Subclass must implement _disconnect";
711
}
712
713
=head3 change_directory
714
715
    my $success = $transport->change_directory($path);
716
717
Changes the current working directory on the remote server.
718
719
B<Important:> Calling this method explicitly switches the transport to "manual mode"
720
and disables automatic directory management. After calling this method, all subsequent
721
file operations will use this directory (or relative paths from it) until you call
722
change_directory() again or create a new connection.
723
724
B<Parameters:>
725
726
=over 4
727
728
=item * C<$path> - Directory path to change to (required)
729
730
=back
731
732
B<Example:>
733
734
    # After calling change_directory explicitly:
735
    $transport->change_directory('/work/dir/');
736
    $transport->upload_file($local, $remote);  # Uses /work/dir/, not upload_directory
737
    $transport->list_files();                  # Lists /work/dir/, not download_directory
738
739
B<Returns:> True on success, undef on failure. Check object_messages() for details.
740
741
=cut
742
743
sub change_directory {
744
    my ( $self, $path ) = @_;
745
746
    # Mark that user has explicitly set a directory
747
    # This prevents auto-directory management from interfering
748
    $self->{_user_set_directory} = 1;
749
750
    return $self->_change_directory($path);
751
}
752
753
=head3 _change_directory
754
755
    my $success = $transport->_change_directory($path);
756
757
Internal method that performs the protocol-specific directory change operation.
758
Must be implemented by subclasses. Called by change_directory() after setting
759
the _user_set_directory flag.
760
761
=cut
762
763
sub _change_directory {
764
    my ($self) = @_;
765
    die "Subclass must implement _change_directory";
278
}
766
}
279
767
280
=head3 _post_store_trigger
768
=head3 _post_store_trigger
(-)a/Koha/File/Transport/FTP.pm (-29 / +34 lines)
Lines 27-41 Koha::File::Transport::FTP - FTP implementation of file transport Link Here
27
27
28
=head2 Class methods
28
=head2 Class methods
29
29
30
=head3 connect
30
=head3 _connect
31
31
32
    my $success = $self->connect;
32
    my $success = $self->_connect;
33
33
34
Start the FTP transport connect, returns true on success or undefined on failure.
34
Start the FTP transport connect, returns true on success or undefined on failure.
35
35
36
=cut
36
=cut
37
37
38
sub connect {
38
sub _connect {
39
    my ($self) = @_;
39
    my ($self) = @_;
40
    my $operation = "connection";
40
    my $operation = "connection";
41
41
Lines 64-80 sub connect { Link Here
64
    return 1;
64
    return 1;
65
}
65
}
66
66
67
=head3 upload_file
67
=head3 _upload_file
68
68
69
    my $success =  $transport->upload_file($fh);
69
Internal method that performs the FTP-specific upload operation.
70
71
Passed a filehandle, this will upload the file to the current directory of the server connection.
72
70
73
Returns true on success or undefined on failure.
71
Returns true on success or undefined on failure.
74
72
75
=cut
73
=cut
76
74
77
sub upload_file {
75
sub _upload_file {
78
    my ( $self, $local_file, $remote_file ) = @_;
76
    my ( $self, $local_file, $remote_file ) = @_;
79
    my $operation = "upload";
77
    my $operation = "upload";
80
78
Lines 95-111 sub upload_file { Link Here
95
    return 1;
93
    return 1;
96
}
94
}
97
95
98
=head3 download_file
96
=head3 _download_file
99
100
    my $success =  $transport->download_file($filename);
101
97
102
Passed a filename, this will download the file from the current directory of the server connection.
98
Internal method that performs the FTP-specific download operation.
103
99
104
Returns true on success or undefined on failure.
100
Returns true on success or undefined on failure.
105
101
106
=cut
102
=cut
107
103
108
sub download_file {
104
sub _download_file {
109
    my ( $self, $remote_file, $local_file ) = @_;
105
    my ( $self, $remote_file, $local_file ) = @_;
110
    my $operation = "download";
106
    my $operation = "download";
111
107
Lines 126-134 sub download_file { Link Here
126
    return 1;
122
    return 1;
127
}
123
}
128
124
129
=head3 change_directory
125
=head3 _change_directory
130
126
131
    my $success = $server->change_directory($directory);
127
    my $success = $server->_change_directory($directory);
132
128
133
Passed a directory name, this will change the current directory of the server connection.
129
Passed a directory name, this will change the current directory of the server connection.
134
130
Lines 136-142 Returns true on success or undefined on failure. Link Here
136
132
137
=cut
133
=cut
138
134
139
sub change_directory {
135
sub _change_directory {
140
    my ( $self, $remote_directory ) = @_;
136
    my ( $self, $remote_directory ) = @_;
141
    my $operation = "change_directory";
137
    my $operation = "change_directory";
142
138
Lines 156-171 sub change_directory { Link Here
156
    return 1;
152
    return 1;
157
}
153
}
158
154
159
=head3 list_files
155
=head3 _list_files
160
161
    my $files = $server->list_files;
162
156
163
Returns an array reference of hashrefs with file information found in the current directory of the server connection.
157
Internal method that performs the FTP-specific file listing operation.
158
Returns an array reference of hashrefs with file information.
164
Each hashref contains: filename, longname, size, perms.
159
Each hashref contains: filename, longname, size, perms.
165
160
166
=cut
161
=cut
167
162
168
sub list_files {
163
sub _list_files {
169
    my ($self) = @_;
164
    my ($self) = @_;
170
    my $operation = "list";
165
    my $operation = "list";
171
166
Lines 207-223 sub list_files { Link Here
207
    return \@file_list;
202
    return \@file_list;
208
}
203
}
209
204
210
=head3 rename_file
205
=head3 _rename_file
211
206
212
    my $success = $server->rename_file($old_name, $new_name);
207
Internal method that performs the FTP-specific file rename operation.
213
214
Renames a file on the server connection.
215
208
216
Returns true on success or undefined on failure.
209
Returns true on success or undefined on failure.
217
210
218
=cut
211
=cut
219
212
220
sub rename_file {
213
sub _rename_file {
221
    my ( $self, $old_name, $new_name ) = @_;
214
    my ( $self, $old_name, $new_name ) = @_;
222
    my $operation = "rename";
215
    my $operation = "rename";
223
216
Lines 234-248 sub rename_file { Link Here
234
    return 1;
227
    return 1;
235
}
228
}
236
229
237
=head3 disconnect
230
=head3 _disconnect
238
231
239
    $server->disconnect();
232
    $server->_disconnect();
240
233
241
Disconnects from the FTP server.
234
Disconnects from the FTP server.
242
235
243
=cut
236
=cut
244
237
245
sub disconnect {
238
sub _disconnect {
246
    my ($self) = @_;
239
    my ($self) = @_;
247
240
248
    if ( $self->{connection} ) {
241
    if ( $self->{connection} ) {
Lines 253-258 sub disconnect { Link Here
253
    return 1;
246
    return 1;
254
}
247
}
255
248
249
=head3 _is_connected
250
251
Internal method to check if transport is currently connected.
252
253
=cut
254
255
sub _is_connected {
256
    my ($self) = @_;
257
258
    return $self->{connection} && $self->{connection}->pwd();
259
}
260
256
sub _abort_operation {
261
sub _abort_operation {
257
    my ( $self, $operation ) = @_;
262
    my ( $self, $operation ) = @_;
258
263
(-)a/Koha/File/Transport/Local.pm (-33 / +39 lines)
Lines 28-42 Koha::File::Transport::Local - Local file system implementation of file transpor Link Here
28
28
29
=head2 Class methods
29
=head2 Class methods
30
30
31
=head3 connect
31
=head3 _connect
32
32
33
    my $success = $self->connect;
33
    my $success = $self->_connect;
34
34
35
Validates that the configured directories exist and have appropriate permissions.
35
Validates that the configured directories exist and have appropriate permissions.
36
36
37
=cut
37
=cut
38
38
39
sub connect {
39
sub _connect {
40
    my ($self) = @_;
40
    my ($self) = @_;
41
    my $operation = "connection";
41
    my $operation = "connection";
42
42
Lines 117-137 sub connect { Link Here
117
    return 1;
117
    return 1;
118
}
118
}
119
119
120
=head3 upload_file
120
=head3 _upload_file
121
121
122
    my $success =  $transport->upload_file($local_file, $remote_file);
122
Internal method that performs the local file system upload operation.
123
124
Copies a local file to the upload directory.
125
123
126
Returns true on success or undefined on failure.
124
Returns true on success or undefined on failure.
127
125
128
=cut
126
=cut
129
127
130
sub upload_file {
128
sub _upload_file {
131
    my ( $self, $local_file, $remote_file ) = @_;
129
    my ( $self, $local_file, $remote_file ) = @_;
132
    my $operation = "upload";
130
    my $operation = "upload";
133
131
134
    my $upload_dir  = $self->upload_directory || $self->{current_directory} || '.';
132
    my $upload_dir  = $self->{current_directory} || $self->upload_directory || '.';
135
    my $destination = File::Spec->catfile( $upload_dir, $remote_file );
133
    my $destination = File::Spec->catfile( $upload_dir, $remote_file );
136
134
137
    unless ( copy( $local_file, $destination ) ) {
135
    unless ( copy( $local_file, $destination ) ) {
Lines 159-179 sub upload_file { Link Here
159
    return 1;
157
    return 1;
160
}
158
}
161
159
162
=head3 download_file
160
=head3 _download_file
163
164
    my $success =  $transport->download_file($remote_file, $local_file);
165
161
166
Copies a file from the download directory to a local file.
162
Internal method that performs the local file system download operation.
167
163
168
Returns true on success or undefined on failure.
164
Returns true on success or undefined on failure.
169
165
170
=cut
166
=cut
171
167
172
sub download_file {
168
sub _download_file {
173
    my ( $self, $remote_file, $local_file ) = @_;
169
    my ( $self, $remote_file, $local_file ) = @_;
174
    my $operation = 'download';
170
    my $operation = 'download';
175
171
176
    my $download_dir = $self->download_directory || $self->{current_directory} || '.';
172
    my $download_dir = $self->{current_directory} || $self->download_directory || '.';
177
    my $source       = File::Spec->catfile( $download_dir, $remote_file );
173
    my $source       = File::Spec->catfile( $download_dir, $remote_file );
178
174
179
    unless ( -f $source ) {
175
    unless ( -f $source ) {
Lines 215-223 sub download_file { Link Here
215
    return 1;
211
    return 1;
216
}
212
}
217
213
218
=head3 change_directory
214
=head3 _change_directory
219
215
220
    my $success = $server->change_directory($directory);
216
    my $success = $server->_change_directory($directory);
221
217
222
Sets the current working directory for file operations.
218
Sets the current working directory for file operations.
223
219
Lines 225-231 Returns true on success or undefined on failure. Link Here
225
221
226
=cut
222
=cut
227
223
228
sub change_directory {
224
sub _change_directory {
229
    my ( $self, $remote_directory ) = @_;
225
    my ( $self, $remote_directory ) = @_;
230
    my $operation = 'change_directory';
226
    my $operation = 'change_directory';
231
227
Lines 257-276 sub change_directory { Link Here
257
    return 1;
253
    return 1;
258
}
254
}
259
255
260
=head3 list_files
256
=head3 _list_files
261
262
    my $files = $server->list_files;
263
257
264
Returns an array reference of hashrefs with file information found in the current directory.
258
Internal method that performs the local file system file listing operation.
259
Returns an array reference of hashrefs with file information.
265
Each hashref contains: filename, longname, size, perms, mtime.
260
Each hashref contains: filename, longname, size, perms, mtime.
266
261
267
=cut
262
=cut
268
263
269
sub list_files {
264
sub _list_files {
270
    my ($self) = @_;
265
    my ($self) = @_;
271
    my $operation = "list";
266
    my $operation = "list";
272
267
273
    my $directory = $self->download_directory || $self->{current_directory} || '.';
268
    my $directory = $self->{current_directory} || $self->download_directory || '.';
274
269
275
    unless ( -d $directory ) {
270
    unless ( -d $directory ) {
276
        $self->add_message(
271
        $self->add_message(
Lines 340-360 sub list_files { Link Here
340
    return \@files;
335
    return \@files;
341
}
336
}
342
337
343
=head3 rename_file
338
=head3 _rename_file
344
339
345
    my $success = $server->rename_file($old_name, $new_name);
340
Internal method that performs the local file system file rename operation.
346
347
Renames a file in the current directory.
348
341
349
Returns true on success or undefined on failure.
342
Returns true on success or undefined on failure.
350
343
351
=cut
344
=cut
352
345
353
sub rename_file {
346
sub _rename_file {
354
    my ( $self, $old_name, $new_name ) = @_;
347
    my ( $self, $old_name, $new_name ) = @_;
355
    my $operation = "rename";
348
    my $operation = "rename";
356
349
357
    my $directory = $self->download_directory || $self->{current_directory} || '.';
350
    my $directory = $self->{current_directory} || $self->download_directory || '.';
358
    my $old_path  = File::Spec->catfile( $directory, $old_name );
351
    my $old_path  = File::Spec->catfile( $directory, $old_name );
359
    my $new_path  = File::Spec->catfile( $directory, $new_name );
352
    my $new_path  = File::Spec->catfile( $directory, $new_name );
360
353
Lines 397-411 sub rename_file { Link Here
397
    return 1;
390
    return 1;
398
}
391
}
399
392
400
=head3 disconnect
393
=head3 _is_connected
394
395
Internal method to check if transport is currently connected.
396
For local transport, always returns true as local filesystem is always accessible.
397
398
=cut
399
400
sub _is_connected {
401
    my ($self) = @_;
402
403
    return 1;    # Local filesystem is always "connected"
404
}
405
406
=head3 _disconnect
401
407
402
    $server->disconnect();
408
    $server->_disconnect();
403
409
404
For local transport, this is a no-op as there are no connections to close.
410
For local transport, this is a no-op as there are no connections to close.
405
411
406
=cut
412
=cut
407
413
408
sub disconnect {
414
sub _disconnect {
409
    my ($self) = @_;
415
    my ($self) = @_;
410
416
411
    # No-op for local transport
417
    # No-op for local transport
(-)a/Koha/File/Transport/SFTP.pm (-29 / +34 lines)
Lines 32-46 Koha::File::Transport::SFTP - SFTP implementation of file transport Link Here
32
32
33
=head2 Class methods
33
=head2 Class methods
34
34
35
=head3 connect
35
=head3 _connect
36
36
37
    my $success = $self->connect;
37
    my $success = $self->_connect;
38
38
39
Start the SFTP transport connect, returns true on success or undefined on failure.
39
Start the SFTP transport connect, returns true on success or undefined on failure.
40
40
41
=cut
41
=cut
42
42
43
sub connect {
43
sub _connect {
44
    my ($self) = @_;
44
    my ($self) = @_;
45
    my $operation = "connection";
45
    my $operation = "connection";
46
46
Lines 76-92 sub connect { Link Here
76
    return 1;
76
    return 1;
77
}
77
}
78
78
79
=head3 upload_file
79
=head3 _upload_file
80
80
81
    my $success =  $transport->upload_file($fh);
81
Internal method that performs the SFTP-specific upload operation.
82
83
Passed a filehandle, this will upload the file to the current directory of the server connection.
84
82
85
Returns true on success or undefined on failure.
83
Returns true on success or undefined on failure.
86
84
87
=cut
85
=cut
88
86
89
sub upload_file {
87
sub _upload_file {
90
    my ( $self, $local_file, $remote_file ) = @_;
88
    my ( $self, $local_file, $remote_file ) = @_;
91
    my $operation = "upload";
89
    my $operation = "upload";
92
90
Lines 107-123 sub upload_file { Link Here
107
    return 1;
105
    return 1;
108
}
106
}
109
107
110
=head3 download_file
108
=head3 _download_file
111
112
    my $success =  $transport->download_file($filename);
113
109
114
Passed a filename, this will download the file from the current directory of the server connection.
110
Internal method that performs the SFTP-specific download operation.
115
111
116
Returns true on success or undefined on failure.
112
Returns true on success or undefined on failure.
117
113
118
=cut
114
=cut
119
115
120
sub download_file {
116
sub _download_file {
121
    my ( $self, $remote_file, $local_file ) = @_;
117
    my ( $self, $remote_file, $local_file ) = @_;
122
    my $operation = 'download';
118
    my $operation = 'download';
123
119
Lines 138-146 sub download_file { Link Here
138
    return 1;
134
    return 1;
139
}
135
}
140
136
141
=head3 change_directory
137
=head3 _change_directory
142
138
143
    my $success = $server->change_directory($directory);
139
    my $success = $server->_change_directory($directory);
144
140
145
Passed a directory name, this will change the current directory of the server connection.
141
Passed a directory name, this will change the current directory of the server connection.
146
142
Lines 148-154 Returns true on success or undefined on failure. Link Here
148
144
149
=cut
145
=cut
150
146
151
sub change_directory {
147
sub _change_directory {
152
    my ( $self, $remote_directory ) = @_;
148
    my ( $self, $remote_directory ) = @_;
153
    my $operation = 'change_directory';
149
    my $operation = 'change_directory';
154
150
Lines 169-184 sub change_directory { Link Here
169
    return 1;
165
    return 1;
170
}
166
}
171
167
172
=head3 list_files
168
=head3 _list_files
173
174
    my $files = $server->list_files;
175
169
176
Returns an array reference of hashrefs with file information found in the current directory of the server connection.
170
Internal method that performs the SFTP-specific file listing operation.
171
Returns an array reference of hashrefs with file information.
177
Each hashref contains: filename, longname, a (attributes).
172
Each hashref contains: filename, longname, a (attributes).
178
173
179
=cut
174
=cut
180
175
181
sub list_files {
176
sub _list_files {
182
    my ($self) = @_;
177
    my ($self) = @_;
183
    my $operation = "list";
178
    my $operation = "list";
184
179
Lines 199-215 sub list_files { Link Here
199
    return $file_list;
194
    return $file_list;
200
}
195
}
201
196
202
=head3 rename_file
197
=head3 _rename_file
203
198
204
    my $success = $server->rename_file($old_name, $new_name);
199
Internal method that performs the SFTP-specific file rename operation.
205
206
Renames a file on the server connection.
207
200
208
Returns true on success or undefined on failure.
201
Returns true on success or undefined on failure.
209
202
210
=cut
203
=cut
211
204
212
sub rename_file {
205
sub _rename_file {
213
    my ( $self, $old_name, $new_name ) = @_;
206
    my ( $self, $old_name, $new_name ) = @_;
214
    my $operation = "rename";
207
    my $operation = "rename";
215
208
Lines 232-246 sub rename_file { Link Here
232
    return 1;
225
    return 1;
233
}
226
}
234
227
235
=head3 disconnect
228
=head3 _is_connected
229
230
Internal method to check if transport is currently connected.
231
232
=cut
233
234
sub _is_connected {
235
    my ($self) = @_;
236
237
    return $self->{connection} && !$self->{connection}->error;
238
}
239
240
=head3 _disconnect
236
241
237
    $server->disconnect();
242
    $server->_disconnect();
238
243
239
Disconnects from the SFTP server.
244
Disconnects from the SFTP server.
240
245
241
=cut
246
=cut
242
247
243
sub disconnect {
248
sub _disconnect {
244
    my ($self) = @_;
249
    my ($self) = @_;
245
250
246
    if ( $self->{connection} ) {
251
    if ( $self->{connection} ) {
(-)a/t/db_dependent/Koha/File/Transports.t (-2 / +89 lines)
Lines 17-24 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 4;
20
use Test::More tests => 5;
21
use Test::NoWarnings;
21
use Test::NoWarnings;
22
use File::Temp qw(tempdir);
23
use File::Spec;
22
24
23
use Koha::Database;
25
use Koha::Database;
24
use Koha::File::Transports;
26
use Koha::File::Transports;
Lines 171-173 subtest 'find() tests' => sub { Link Here
171
173
172
    $schema->storage->txn_rollback;
174
    $schema->storage->txn_rollback;
173
};
175
};
174
- 
176
177
subtest 'Dual API functionality (_user_set_directory flag)' => sub {
178
    plan tests => 8;
179
180
    $schema->storage->txn_begin;
181
182
    # Create temporary directories for testing
183
    my $tempdir      = tempdir( CLEANUP => 1 );
184
    my $download_dir = File::Spec->catdir( $tempdir, 'download' );
185
    my $upload_dir   = File::Spec->catdir( $tempdir, 'upload' );
186
    my $custom_dir   = File::Spec->catdir( $tempdir, 'custom' );
187
188
    mkdir $download_dir or die "Cannot create download_dir: $!";
189
    mkdir $upload_dir   or die "Cannot create upload_dir: $!";
190
    mkdir $custom_dir   or die "Cannot create custom_dir: $!";
191
192
    # Create test files
193
    my $test_file = File::Spec->catfile( $download_dir, 'test.txt' );
194
    open my $fh, '>', $test_file or die "Cannot create test file: $!";
195
    print $fh "Test content\n";
196
    close $fh;
197
198
    # Create local transport with default directories
199
    my $local_transport = $builder->build_object(
200
        {
201
            class => 'Koha::File::Transports',
202
            value => {
203
                transport          => 'local',
204
                name               => 'Dual API Test',
205
                host               => 'localhost',
206
                download_directory => $download_dir,
207
                upload_directory   => $upload_dir,
208
            }
209
        }
210
    );
211
212
    # TEST 1: Traditional API with explicit change_directory should set flag
213
    $local_transport->connect();
214
    ok(
215
        !$local_transport->{_user_set_directory},
216
        'Flag should be false after connect'
217
    );
218
219
    $local_transport->change_directory($custom_dir);
220
    ok(
221
        $local_transport->{_user_set_directory},
222
        'Flag should be true after explicit change_directory'
223
    );
224
225
    # TEST 2: Verify flag prevents auto-directory management
226
    # After setting directory explicitly, list_files should NOT auto-change to download_directory
227
    my $custom_test_file = File::Spec->catfile( $custom_dir, 'custom.txt' );
228
    open my $fh2, '>', $custom_test_file or die "Cannot create custom test file: $!";
229
    print $fh2 "Custom content\n";
230
    close $fh2;
231
232
    my $files = $local_transport->list_files();
233
    ok( $files, 'list_files() works after explicit change_directory' );
234
    is( scalar(@$files),       1,            'Should find 1 file in custom directory' );
235
    is( $files->[0]{filename}, 'custom.txt', 'Should find custom.txt, not test.txt' );
236
237
    # TEST 3: Simplified API with options should work
238
    $local_transport = $builder->build_object(
239
        {
240
            class => 'Koha::File::Transports',
241
            value => {
242
                transport          => 'local',
243
                name               => 'Simplified API Test',
244
                host               => 'localhost',
245
                download_directory => $download_dir,
246
                upload_directory   => $upload_dir,
247
            }
248
        }
249
    );
250
251
    # Simplified API - no explicit connect or change_directory
252
    my $files_simplified = $local_transport->list_files( { path => $custom_dir } );
253
    ok( $files_simplified, 'Simplified API list_files() with custom path works' );
254
    is( scalar(@$files_simplified), 1, 'Should find 1 file via simplified API' );
255
    is(
256
        $files_simplified->[0]{filename}, 'custom.txt',
257
        'Simplified API should find custom.txt'
258
    );
259
260
    $schema->storage->txn_rollback;
261
};

Return to bug 40811