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

(-)a/C4/ImportBatch.pm (-9 / +26 lines)
Lines 20-25 package C4::ImportBatch; Link Here
20
use strict;
20
use strict;
21
use warnings;
21
use warnings;
22
22
23
use Scalar::Util qw(openhandle);
24
23
use C4::Context;
25
use C4::Context;
24
use C4::Koha;
26
use C4::Koha;
25
use C4::Biblio;
27
use C4::Biblio;
Lines 1488-1494 sub SetImportRecordMatches { Link Here
1488
1490
1489
Reads ISO2709 binary porridge from the given file and creates MARC::Record-objects out of it.
1491
Reads ISO2709 binary porridge from the given file and creates MARC::Record-objects out of it.
1490
1492
1491
@PARAM1, String, absolute path to the ISO2709 file.
1493
@PARAM1, String, absolute path to the ISO2709 file or an open filehandle.
1492
@PARAM2, String, see stage_file.pl
1494
@PARAM2, String, see stage_file.pl
1493
@PARAM3, String, should be utf8
1495
@PARAM3, String, should be utf8
1494
1496
Lines 1503-1512 sub RecordsFromISO2709File { Link Here
1503
    my $marc_type = C4::Context->preference('marcflavour');
1505
    my $marc_type = C4::Context->preference('marcflavour');
1504
    $marc_type .= 'AUTH' if ($marc_type eq 'UNIMARC' && $record_type eq 'auth');
1506
    $marc_type .= 'AUTH' if ($marc_type eq 'UNIMARC' && $record_type eq 'auth');
1505
1507
1506
    open IN, "<$input_file" or die "$0: cannot open input file $input_file: $!\n";
1508
    my $fh;
1509
    if (openhandle($input_file)) {
1510
        $fh = $input_file;
1511
    } else {
1512
        open $fh, '<', $input_file or die "$0: cannot open input file $input_file: $!\n";
1513
    }
1514
1507
    my @marc_records;
1515
    my @marc_records;
1508
    $/ = "\035";
1516
    $/ = "\035";
1509
    while (<IN>) {
1517
    while (<$fh>) {
1510
        s/^\s+//;
1518
        s/^\s+//;
1511
        s/\s+$//;
1519
        s/\s+$//;
1512
        next unless $_; # skip if record has only whitespace, as might occur
1520
        next unless $_; # skip if record has only whitespace, as might occur
Lines 1518-1524 sub RecordsFromISO2709File { Link Here
1518
                "Unexpected charset $charset_guessed, expecting $encoding";
1526
                "Unexpected charset $charset_guessed, expecting $encoding";
1519
        }
1527
        }
1520
    }
1528
    }
1521
    close IN;
1529
    close $fh;
1530
1522
    return ( \@errors, \@marc_records );
1531
    return ( \@errors, \@marc_records );
1523
}
1532
}
1524
1533
Lines 1528-1534 sub RecordsFromISO2709File { Link Here
1528
1537
1529
Creates MARC::Record-objects out of the given MARCXML-file.
1538
Creates MARC::Record-objects out of the given MARCXML-file.
1530
1539
1531
@PARAM1, String, absolute path to the ISO2709 file.
1540
@PARAM1, String, absolute path to the ISO2709 file or an open filehandle
1532
@PARAM2, String, should be utf8
1541
@PARAM2, String, should be utf8
1533
1542
1534
Returns two array refs.
1543
Returns two array refs.
Lines 1551-1557 sub RecordsFromMARCXMLFile { Link Here
1551
1560
1552
=head2 RecordsFromMarcPlugin
1561
=head2 RecordsFromMarcPlugin
1553
1562
1554
    Converts text of input_file into array of MARC records with to_marc plugin
1563
Converts text of C<$input_file> into array of MARC records with to_marc plugin
1564
1565
C<$input_file> can be either a filename or an open filehandle.
1555
1566
1556
=cut
1567
=cut
1557
1568
Lines 1561-1575 sub RecordsFromMarcPlugin { Link Here
1561
    return \@return if !$input_file || !$plugin_class;
1572
    return \@return if !$input_file || !$plugin_class;
1562
1573
1563
    # Read input file
1574
    # Read input file
1564
    open IN, "<$input_file" or die "$0: cannot open input file $input_file: $!\n";
1575
    my $fh;
1576
    if (openhandle($input_file)) {
1577
        $fh = $input_file;
1578
    } else {
1579
        open $fh, '<', $input_file or die "$0: cannot open input file $input_file: $!\n";
1580
    }
1581
1565
    $/ = "\035";
1582
    $/ = "\035";
1566
    while (<IN>) {
1583
    while (<$fh>) {
1567
        s/^\s+//;
1584
        s/^\s+//;
1568
        s/\s+$//;
1585
        s/\s+$//;
1569
        next unless $_;
1586
        next unless $_;
1570
        $text .= $_;
1587
        $text .= $_;
1571
    }
1588
    }
1572
    close IN;
1589
    close $fh;
1573
1590
1574
    # Convert to large MARC blob with plugin
1591
    # Convert to large MARC blob with plugin
1575
    $text = Koha::Plugins::Handler->run({
1592
    $text = Koha::Plugins::Handler->run({
(-)a/C4/Installer/PerlDependencies.pm (-1 / +1 lines)
Lines 673-679 our $PERL_DEPS = { Link Here
673
        'min_ver'  => '0.60',
673
        'min_ver'  => '0.60',
674
    },
674
    },
675
    'Archive::Zip' => {
675
    'Archive::Zip' => {
676
        'usage'    => 'Plugins',
676
        'usage'    => 'Plugins, Local Cover Images',
677
        'required' => '0',
677
        'required' => '0',
678
        'min_ver'  => '1.30',
678
        'min_ver'  => '1.30',
679
    },
679
    },
(-)a/Koha/Storage.pm (+366 lines)
Line 0 Link Here
1
package Koha::Storage;
2
3
# Copyright 2018 BibLibre
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 NAME
21
22
Koha::Storage - Manage file storages
23
24
=head1 SYNOPSIS
25
26
    use Koha::Storage;
27
28
    # Get all available storages
29
    my $config = Koha::Storage->config;
30
31
    # Get storage instance by name
32
    # By default, 'TMP' and 'DEFAULT' are available
33
    # Others can be added in $KOHA_CONF
34
    my $storage = Koha::Storage->get_instance($name)
35
36
    my $directories = $storage->directories();
37
38
    my $filepath = $storage->filepath({
39
        hashvalue => $hashvalue,
40
        filename => $filename,
41
        dir => $dir,
42
    });
43
44
    my $exists = $storage->exists($filepath);
45
    my $deleted_count = $storage->delete($filepath);
46
47
    my $fh = $storage->fh($filepath, $mode);
48
49
    my $url = $storage->url($hashfile, $filepath);
50
51
=cut
52
53
use Modern::Perl;
54
55
use File::Spec;
56
use List::Util qw( first );
57
58
use C4::Context;
59
60
use constant KOHA_UPLOAD => 'koha_upload';
61
62
=head1 CLASS METHODS
63
64
=head2 config
65
66
Returns the configuration for all available storages.
67
68
    my $config = Koha::Storage->config
69
70
Returns an arrayref containing hashrefs. Example:
71
72
    [
73
        {
74
            name => 'TMP',
75
            adapter => 'directory',
76
            adapter_params => {
77
                path => '/tmp',
78
            },
79
            temporary => 1,
80
            hash_filename => 1,
81
        },
82
        {
83
            name => 'DEFAULT',
84
            ...
85
        },
86
        ...
87
    ]
88
89
Storages can be configured in C<$KOHA_CONF> by adding <storage> elements:
90
91
    <yazgfs>
92
        <config>
93
            <storage>
94
                <!-- Mandatory: Storage's identifier -->
95
                <name>MyStorage</name>
96
97
                <!-- Mandatory: 'directory' is the only available adapter actually -->
98
                <adapter>directory</adapter>
99
100
101
                <!-- Parameters specific to storage's adapter -->
102
                <adapter_params>
103
                    <!-- Mandatory for 'directory' adapter -->
104
                    <path>/mnt/mystorage</path>
105
                </adapter_params>
106
107
                <!-- Whether or not to prepend the hashvalue to filename -->
108
                <!-- Default: 0 -->
109
                <hash_filename>1</hash_filename>
110
111
                <!-- Whether or not the storage is temporary -->
112
                <!-- Default: 0 -->
113
                <temporary>0</temporary>
114
115
                <!-- If a baseurl is set, the file's URL is built by concatenating the baseurl and the filepath -->
116
                <!-- Otherwise it falls back to using opac-retrieve-file.pl -->
117
                <baseurl>https://mystorage.example.com/</baseurl>
118
            </storage>
119
            <!-- ... -->
120
        </config>
121
    </yazgfs>
122
123
The 'TMP' storage is always available and cannot be configured.
124
125
The 'DEFAULT' storage is available if:
126
127
=over
128
129
=item * C<upload_path> is set in C<$KOHA_CONF>, or
130
131
=item * a storage named 'DEFAULT' is configured in C<$KOHA_CONF>
132
133
=back
134
135
=cut
136
137
sub config {
138
    my $storage = C4::Context->config('storage');
139
140
    my $config;
141
    if (ref $storage eq 'ARRAY') {
142
        $config = [ @$storage ];
143
    } elsif ($storage) {
144
        $config = [ $storage ];
145
    } else {
146
        $config = [];
147
    }
148
149
    my $default = first { $_->{name} eq 'DEFAULT' } @$config;
150
    unless ($default) {
151
        # Backward compatibility for those who haven't changed their $KOHA_CONF
152
        warn "No 'DEFAULT' storage configured. Using upload_path as a fallback.";
153
154
        my $upload_path = C4::Context->config('upload_path');
155
        if ($upload_path) {
156
            unshift @$config, {
157
                name => 'DEFAULT',
158
                adapter => 'directory',
159
                adapter_params => {
160
                    path => C4::Context->config('upload_path'),
161
                },
162
                hash_filename => 1,
163
            };
164
        } else {
165
            warn "No upload_path defined."
166
        }
167
    }
168
169
    my $database = C4::Context->config('database');
170
    my $subdir = KOHA_UPLOAD =~ s/koha/$database/r;
171
    unshift @$config, {
172
        name => 'TMP',
173
        adapter => 'directory',
174
        adapter_params => {
175
            path => File::Spec->catfile(File::Spec->tmpdir, $subdir),
176
        },
177
        temporary => 1,
178
        hash_filename => 1,
179
    };
180
181
    return $config;
182
}
183
184
=head2 get_instance
185
186
Retrieves an instance of Koha::Storage
187
188
    my $storage = Koha::Storage->get_instance($name);
189
190
Returns a Koha::Storage object
191
192
=cut
193
194
my $instances = {};
195
196
sub get_instance {
197
    my ($class, $name) = @_;
198
199
    unless (exists $instances->{$name}) {
200
        my $storages = $class->config;
201
        my $storage = first { $_->{name} eq $name } @$storages;
202
203
        if ($storage) {
204
            $instances->{$name} = $class->new($storage);
205
        } else {
206
            warn "There is no storage named $name";
207
        }
208
    }
209
210
    return $instances->{$name};
211
}
212
213
=head2 new
214
215
Creates a new Koha::Storage object
216
217
    my $storage = Koha::Storage->new(\%params);
218
219
C<%params> can contain the same keys as the one returned by C<Koha::Storage-E<gt>config>
220
221
You shouldn't use this directly. Use C<Koha::Storage-E<gt>get_instance> instead
222
223
=cut
224
225
sub new {
226
    my ($class, $params) = @_;
227
228
    my $adapter_class = 'Koha::Storage::Adapter::' . ucfirst(lc($params->{adapter}));
229
    my $adapter;
230
    eval {
231
        my $adapter_file = $adapter_class =~ s,::,/,gr . '.pm';
232
        require $adapter_file;
233
        $adapter = $adapter_class->new($params->{adapter_params});
234
    };
235
    if ($@) {
236
        warn "Unable to create an instance of $adapter_class : $@";
237
238
        return;
239
    }
240
241
    my $self = $params;
242
    $self->{adapter} = $adapter;
243
244
    return bless $self, $class;
245
}
246
247
=head1 INSTANCE METHODS
248
249
=head2 filepath
250
251
Returns relative filepath of a file according to storage's parameters and file's
252
properties (filename, hashvalue, dir)
253
254
    my $filepath = $storage->filepath({
255
        hashvalue => $hashvalue,
256
        filename => $filename,
257
        dir => $dir,
258
    })
259
260
The return value is a required parameter for several other methods.
261
262
=cut
263
264
sub filepath {
265
    my ($self, $params) = @_;
266
267
    my $filepath;
268
    if ($params->{dir}) {
269
        $filepath .= $params->{dir} . '/';
270
    }
271
    if ($params->{hashvalue} && $self->{hash_filename}) {
272
        $filepath .= $params->{hashvalue} . '_';
273
    }
274
    $filepath .= $params->{filename};
275
276
    return $filepath;
277
}
278
279
=head2 exists
280
281
Check file existence
282
283
    my $filepath = $storage->filepath(\%params);
284
    my $exists = $storage->exists($filepath);
285
286
Returns a true value if the file exists, and a false value otherwise.
287
288
=cut
289
290
sub exists {
291
    my ($self, $filepath) = @_;
292
293
    return $self->{adapter}->exists($filepath);
294
}
295
296
=head2 fh
297
298
Returns a file handle for the given C<$filepath>
299
300
    my $filepath = $storage->filepath(\%params);
301
    my $fh = $storage->fh($filepath, $mode);
302
303
For possible values of C<$mode>, see L<perlfunc/open>
304
305
=cut
306
307
sub fh {
308
    my ($self, $filepath, $mode) = @_;
309
310
    return $self->{adapter}->fh($filepath, $mode);
311
}
312
313
=head2 directories
314
315
Returns a list of writable directories in storage
316
317
    my $directories = $storage->directories();
318
319
=cut
320
321
sub directories {
322
    my ($self) = @_;
323
324
    return $self->{adapter}->directories();
325
}
326
327
=head2 delete
328
329
Deletes a file
330
331
    my $filepath = $storage->filepath(\%params);
332
    $storage->delete($filepath);
333
334
=cut
335
336
sub delete {
337
    my ($self, $filepath) = @_;
338
339
    return $self->{adapter}->delete($filepath);
340
}
341
342
=head2 url
343
344
Returns the URL to access the file
345
346
    my $filepath = $storage->filepath(\%params);
347
    my $url = $storage->url($hashvalue, $filepath);
348
349
=cut
350
351
sub url {
352
    my ($self, $hashvalue, $filepath) = @_;
353
354
    if ($self->{baseurl}) {
355
        return $self->{baseurl} . $filepath;
356
    }
357
358
    # Default to opac-retrieve-file.pl
359
    my $url = C4::Context->preference('OPACBaseURL');
360
    $url =~ s/\/$//;
361
    $url .= '/cgi-bin/koha/opac-retrieve-file.pl?id=' . $hashvalue;
362
363
    return $url;
364
}
365
366
1;
(-)a/Koha/Storage/Adapter/Directory.pm (+174 lines)
Line 0 Link Here
1
package Koha::Storage::Adapter::Directory;
2
3
# Copyright 2018 BibLibre
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 NAME
21
22
Koha::Storage::Adapter::Directory - Storage adapter for a filesystem directory
23
24
=head1 DESCRIPTION
25
26
This is the default storage adapter. It stores files in a directory on the
27
filesystem.
28
29
You shouldn't use this directly. Use C<Koha::Storage> instead.
30
31
=cut
32
33
use Modern::Perl;
34
35
use File::Basename;
36
use File::Find;
37
use File::Path qw(make_path);
38
use File::Spec;
39
use IO::File;
40
41
=head1 INSTANCE METHODS
42
43
=head2 new
44
45
Creates a new C<Koha::Storage::Adapter::Directory> object.
46
47
    my $adapter = Koha::Storage::Adapter::Directory->new(\%params):
48
49
C<%params> contains the following keys:
50
51
=over
52
53
=item * C<path>: Mandatory. Absolute path of storage
54
55
=back
56
57
=cut
58
59
sub new {
60
    my ($class, $params) = @_;
61
62
    unless ($params->{path}) {
63
        die "Missing parameter 'path'";
64
    }
65
66
    my $self = { %$params };
67
68
    return bless $self, $class;
69
}
70
71
=head2 exists
72
73
See L<Koha::Storage/exists>.
74
75
=cut
76
77
sub exists {
78
    my ($self, $filepath) = @_;
79
80
    return -e $self->abspath($filepath);
81
}
82
83
=head2 fh
84
85
See L<Koha::Storage/fh>.
86
87
=cut
88
89
sub fh {
90
    my ($self, $filepath, $mode) = @_;
91
92
    my $abspath = $self->abspath($filepath);
93
94
    my $dirname = dirname($abspath);
95
    unless (-e $dirname) {
96
        eval {
97
            make_path($dirname);
98
        };
99
        if ($@) {
100
            warn "Unable to create path $dirname: $@";
101
            return;
102
        }
103
    }
104
105
    unless (-w $dirname) {
106
        warn "Directory $dirname is not writable";
107
        return;
108
    }
109
110
    my $fh = IO::File->new($abspath, $mode);
111
    unless ($fh) {
112
        warn "File handle creation failed for $abspath (mode $mode)";
113
        return;
114
    }
115
116
    $fh->binmode;
117
118
    return $fh;
119
}
120
121
=head2 directories
122
123
See L<Koha::Storage/directories>.
124
125
=cut
126
127
sub directories {
128
    my ($self) = @_;
129
130
    my @directories;
131
132
    if (-e $self->{path}) {
133
        find(sub {
134
            if (-d $File::Find::name) {
135
                my $relpath = $File::Find::name =~ s/^\Q$self->{path}\E\/?//r;
136
                push @directories, $relpath if $relpath;
137
            }
138
        }, $self->{path});
139
    }
140
141
    return \@directories;
142
}
143
144
=head2 delete
145
146
See L<Koha::Storage/delete>.
147
148
=cut
149
150
sub delete {
151
    my ($self, $filepath) = @_;
152
153
    return unlink $self->abspath($filepath);
154
}
155
156
=head1 INTERNAL METHODS
157
158
=head2 abspath
159
160
Returns the absolute path of a file
161
162
    my $abspath = $adapter->abspath($filepath);
163
164
=cut
165
166
sub abspath {
167
    my ($self, $filepath) = @_;
168
169
    my $abspath = File::Spec->catfile($self->{path}, $filepath);
170
171
    return $abspath;
172
}
173
174
1;
(-)a/Koha/UploadedFile.pm (-40 / +47 lines)
Lines 18-24 package Koha::UploadedFile; Link Here
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
use File::Spec;
21
22
use Koha::Storage;
22
23
23
use parent qw(Koha::Object);
24
use parent qw(Koha::Object);
24
25
Lines 36-44 Koha::UploadedFile - Koha::Object class for single uploaded file Link Here
36
    # get a file handle on an uploaded_file
37
    # get a file handle on an uploaded_file
37
    my $fh = $upload->file_handle;
38
    my $fh = $upload->file_handle;
38
39
39
    # get full path
40
    my $path = $upload->full_path;
41
42
    # delete uploaded file
40
    # delete uploaded file
43
    $upload->delete;
41
    $upload->delete;
44
42
Lines 46-53 Koha::UploadedFile - Koha::Object class for single uploaded file Link Here
46
44
47
Allows regular CRUD operations on uploaded_files via Koha::Object / DBIx.
45
Allows regular CRUD operations on uploaded_files via Koha::Object / DBIx.
48
46
49
The delete method also takes care of deleting files. The full_path method
47
The delete method also takes care of deleting files.
50
returns a fully qualified path for an upload.
51
48
52
Additional methods include: file_handle, httpheaders.
49
Additional methods include: file_handle, httpheaders.
53
50
Lines 72-78 sub delete { Link Here
72
    my ( $self, $params ) = @_;
69
    my ( $self, $params ) = @_;
73
70
74
    my $name = $self->filename;
71
    my $name = $self->filename;
75
    my $file = $self->full_path;
76
72
77
    my $retval = $self->SUPER::delete;
73
    my $retval = $self->SUPER::delete;
78
    if( !defined($retval) ) { # undef is Unknown (-1)
74
    if( !defined($retval) ) { # undef is Unknown (-1)
Lines 84-114 sub delete { Link Here
84
    }
80
    }
85
    return $retval if $params->{keep_file};
81
    return $retval if $params->{keep_file};
86
82
87
    if( ! -e $file ) {
83
    my $storage = Koha::Storage->get_instance($self->storage);
88
        warn "Removing record for $name within category ".
84
    my $filepath = $self->filepath;
89
            $self->uploadcategorycode. ", but file was missing.";
90
    } elsif( ! unlink($file) ) {
91
        warn "Problem while deleting: $file";
92
    }
93
    return $retval;
94
}
95
96
=head3 full_path
97
98
Returns the fully qualified path name for an uploaded file.
99
85
100
=cut
86
    if ( ! $storage->exists($filepath) ) {
87
        warn "Removing record for $name within storage " . $self->storage . ", but file was missing.";
88
    } elsif ( ! $storage->delete($filepath) ) {
89
        warn "Problem while deleting: $filepath";
90
    }
101
91
102
sub full_path {
92
    return $retval;
103
    my ( $self ) = @_;
104
    my $path = File::Spec->catfile(
105
        $self->permanent
106
            ? $self->permanent_directory
107
            : C4::Context->temporary_directory,
108
        $self->dir,
109
        $self->hashvalue. '_'. $self->filename,
110
    );
111
    return $path;
112
}
93
}
113
94
114
=head3 file_handle
95
=head3 file_handle
Lines 119-128 Returns a file handle for an uploaded file. Link Here
119
100
120
sub file_handle {
101
sub file_handle {
121
    my ( $self ) = @_;
102
    my ( $self ) = @_;
122
    $self->{_file_handle} = IO::File->new( $self->full_path, "r" );
103
123
    return if !$self->{_file_handle};
104
    my $storage = Koha::Storage->get_instance($self->storage);
124
    $self->{_file_handle}->binmode;
105
125
    return $self->{_file_handle};
106
    return $storage->fh($self->filepath, 'r');
126
}
107
}
127
108
128
=head3 httpheaders
109
=head3 httpheaders
Lines 148-166 sub httpheaders { Link Here
148
    }
129
    }
149
}
130
}
150
131
151
=head2 CLASS METHODS
132
=head3 url
152
133
153
=head3 permanent_directory
134
Returns the URL to access the file
154
135
155
Returns root directory for permanent storage
136
    my $url = $uploaded_file->url;
156
137
157
=cut
138
=cut
158
139
159
sub permanent_directory {
140
sub url {
160
    my ( $class ) = @_;
141
    my ($self) = @_;
161
    return C4::Context->config('upload_path');
142
143
    my $storage = Koha::Storage->get_instance($self->storage);
144
145
    return $storage->url($self->hashvalue, $self->filepath);
162
}
146
}
163
147
148
=head3 filepath
149
150
Returns the filepath of a file relative to the storage root path
151
152
    my $filepath = $uploaded_file->filepath;
153
154
=cut
155
156
sub filepath {
157
    my ($self) = @_;
158
159
    my $storage = Koha::Storage->get_instance($self->storage);
160
    my $filepath = $storage->filepath({
161
        hashvalue => $self->hashvalue,
162
        filename => $self->filename,
163
        dir => $self->dir,
164
    });
165
166
    return $filepath;
167
}
168
169
=head2 CLASS METHODS
170
164
=head3 _type
171
=head3 _type
165
172
166
Returns name of corresponding DBIC resultset
173
Returns name of corresponding DBIC resultset
(-)a/Koha/UploadedFiles.pm (-14 / +3 lines)
Lines 22-27 use Modern::Perl; Link Here
22
use C4::Koha;
22
use C4::Koha;
23
use Koha::Database;
23
use Koha::Database;
24
use Koha::DateUtils;
24
use Koha::DateUtils;
25
use Koha::Storage;
25
use Koha::UploadedFile;
26
use Koha::UploadedFile;
26
27
27
use parent qw(Koha::Objects);
28
use parent qw(Koha::Objects);
Lines 126-133 sub delete_missing { Link Here
126
    $self = Koha::UploadedFiles->new if !ref($self); # handle class call
127
    $self = Koha::UploadedFiles->new if !ref($self); # handle class call
127
    my $rv = 0;
128
    my $rv = 0;
128
    while( my $row = $self->next ) {
129
    while( my $row = $self->next ) {
129
        my $file = $row->full_path;
130
        my $storage = Koha::Storage->get_instance($row->storage);
130
        next if -e $file;
131
        next if $storage->exists($row->filepath);
131
        if( $params->{keep_record} ) {
132
        if( $params->{keep_record} ) {
132
            $rv++;
133
            $rv++;
133
            next;
134
            next;
Lines 166-183 sub search_term { Link Here
166
167
167
=head2 CLASS METHODS
168
=head2 CLASS METHODS
168
169
169
=head3 getCategories
170
171
getCategories returns a list of upload category codes and names
172
173
=cut
174
175
sub getCategories {
176
    my ( $class ) = @_;
177
    my $cats = C4::Koha::GetAuthorisedValues('UPLOAD');
178
    [ map {{ code => $_->{authorised_value}, name => $_->{lib} }} @$cats ];
179
}
180
181
=head3 _type
170
=head3 _type
182
171
183
Returns name of corresponding DBIC resultset
172
Returns name of corresponding DBIC resultset
(-)a/Koha/Uploader.pm (-70 / +55 lines)
Lines 31-37 Koha::Uploader - Facilitate file uploads (temporary and permanent) Link Here
31
31
32
    # add an upload (see tools/upload-file.pl)
32
    # add an upload (see tools/upload-file.pl)
33
    # the public flag allows retrieval via OPAC
33
    # the public flag allows retrieval via OPAC
34
    my $upload = Koha::Uploader->new( public => 1, category => 'A' );
34
    my $upload = Koha::Uploader->new( public => 1, storage => 'DEFAULT' );
35
    my $cgi = $upload->cgi;
35
    my $cgi = $upload->cgi;
36
    # Do something with $upload->count, $upload->result or $upload->err
36
    # Do something with $upload->count, $upload->result or $upload->err
37
37
Lines 58-76 Koha::Uploader - Facilitate file uploads (temporary and permanent) Link Here
58
58
59
=cut
59
=cut
60
60
61
use constant KOHA_UPLOAD  => 'koha_upload';
62
use constant BYTES_DIGEST => 2048;
61
use constant BYTES_DIGEST => 2048;
63
use constant ERR_EXISTS   => 'UPLERR_ALREADY_EXISTS';
62
use constant ERR_EXISTS   => 'UPLERR_ALREADY_EXISTS';
64
use constant ERR_PERMS    => 'UPLERR_CANNOT_WRITE';
63
use constant ERR_PERMS    => 'UPLERR_CANNOT_WRITE';
65
use constant ERR_ROOT     => 'UPLERR_NO_ROOT_DIR';
66
use constant ERR_TEMP     => 'UPLERR_NO_TEMP_DIR';
67
64
68
use Modern::Perl;
65
use Modern::Perl;
69
use CGI; # no utf8 flag, since it may interfere with binary uploads
66
use CGI; # no utf8 flag, since it may interfere with binary uploads
70
use Digest::MD5;
67
use Digest::MD5;
71
use Encode;
68
use Encode;
72
use File::Spec;
73
use IO::File;
74
use Time::HiRes;
69
use Time::HiRes;
75
70
76
use base qw(Class::Accessor);
71
use base qw(Class::Accessor);
Lines 79-84 use C4::Context; Link Here
79
use C4::Koha;
74
use C4::Koha;
80
use Koha::UploadedFile;
75
use Koha::UploadedFile;
81
use Koha::UploadedFiles;
76
use Koha::UploadedFiles;
77
use Koha::Storage;
82
78
83
__PACKAGE__->mk_ro_accessors( qw|| );
79
__PACKAGE__->mk_ro_accessors( qw|| );
84
80
Lines 86-95 __PACKAGE__->mk_ro_accessors( qw|| ); Link Here
86
82
87
=head2 new
83
=head2 new
88
84
89
    Returns new object based on Class::Accessor.
85
Returns new object based on Class::Accessor.
90
    Use tmp or temp flag for temporary storage.
86
91
    Use public flag to mark uploads as available in OPAC.
87
    my $uploader = Koha::Uploader->new(\%params);
92
    The category parameter is only useful for permanent storage.
88
89
C<%params> contains the following keys:
90
91
=over
92
93
=item * C<storage>: Mandatory. Storage's name
94
95
=item * C<dir>: Subdirectory in storage
96
97
=item * C<public>: Whether or not the uploaded files are public (available in OPAC).
98
99
=back
93
100
94
=cut
101
=cut
95
102
Lines 192-265 sub allows_add_by { Link Here
192
sub _init {
199
sub _init {
193
    my ( $self, $params ) = @_;
200
    my ( $self, $params ) = @_;
194
201
195
    $self->{rootdir} = Koha::UploadedFile->permanent_directory;
202
    $self->{storage} = Koha::Storage->get_instance($params->{storage});
196
    $self->{tmpdir} = C4::Context::temporary_directory;
197
198
    $params->{tmp} = $params->{temp} if !exists $params->{tmp};
199
    $self->{temporary} = $params->{tmp}? 1: 0; #default false
200
    if( $params->{tmp} ) {
201
        my $db =  C4::Context->config('database');
202
        $self->{category} = KOHA_UPLOAD;
203
        $self->{category} =~ s/koha/$db/;
204
    } else {
205
        $self->{category} = $params->{category} || KOHA_UPLOAD;
206
    }
207
208
    $self->{files} = {};
203
    $self->{files} = {};
209
    $self->{uid} = C4::Context->userenv->{number} if C4::Context->userenv;
204
    $self->{uid} = C4::Context->userenv->{number} if C4::Context->userenv;
210
    $self->{public} = $params->{public}? 1: undef;
205
    $self->{public} = $params->{public} ? 1 : 0;
206
    $self->{dir} = $params->{dir} // '';
211
}
207
}
212
208
213
sub _fh {
209
sub _fh {
214
    my ( $self, $filename ) = @_;
210
    my ( $self, $filename ) = @_;
215
    if( $self->{files}->{$filename} ) {
211
212
    if ( $self->{files}->{$filename} ) {
216
        return $self->{files}->{$filename}->{fh};
213
        return $self->{files}->{$filename}->{fh};
217
    }
214
    }
218
}
215
}
219
216
220
sub _create_file {
217
sub _create_file {
221
    my ( $self, $filename ) = @_;
218
    my ( $self, $filename ) = @_;
222
    my $fh;
219
223
    if( $self->{files}->{$filename} &&
220
    return if ($self->{files}->{$filename} && $self->{files}->{$filename}->{errcode});
224
            $self->{files}->{$filename}->{errcode} ) {
221
    my $hashval = $self->{files}->{$filename}->{hash};
225
        #skip
222
    my $filepath = $self->{storage}->filepath({
226
    } elsif( !$self->{temporary} && !$self->{rootdir} ) {
223
        hashvalue => $hashval,
227
        $self->{files}->{$filename}->{errcode} = ERR_ROOT; #no rootdir
224
        filename => $filename,
228
    } elsif( $self->{temporary} && !$self->{tmpdir} ) {
225
        dir => $self->{dir},
229
        $self->{files}->{$filename}->{errcode} = ERR_TEMP; #no tempdir
226
    });
227
228
    # if the file exists and it is registered, then set error
229
    # if it exists, but is not in the database, we will overwrite
230
    if ( $self->{storage}->exists($filepath) &&
231
    Koha::UploadedFiles->search({
232
        hashvalue => $hashval,
233
        storage => $self->{storage}->{name},
234
    })->count ) {
235
        $self->{files}->{$filename}->{errcode} = ERR_EXISTS; #already exists
236
        return;
237
    }
238
239
    my $fh = $self->{storage}->fh($filepath, 'w');
240
    if ($fh) {
241
        $self->{files}->{$filename}->{fh} = $fh;
230
    } else {
242
    } else {
231
        my $dir = $self->_dir;
243
        $self->{files}->{$filename}->{errcode} = ERR_PERMS; #not writable
232
        my $hashval = $self->{files}->{$filename}->{hash};
233
        my $fn = $hashval. '_'. $filename;
234
235
        # if the file exists and it is registered, then set error
236
        # if it exists, but is not in the database, we will overwrite
237
        if( -e "$dir/$fn" &&
238
        Koha::UploadedFiles->search({
239
            hashvalue          => $hashval,
240
            uploadcategorycode => $self->{category},
241
        })->count ) {
242
            $self->{files}->{$filename}->{errcode} = ERR_EXISTS;
243
            return;
244
        }
245
246
        $fh = IO::File->new( "$dir/$fn", "w");
247
        if( $fh ) {
248
            $fh->binmode;
249
            $self->{files}->{$filename}->{fh}= $fh;
250
        } else {
251
            $self->{files}->{$filename}->{errcode} = ERR_PERMS;
252
        }
253
    }
244
    }
254
    return $fh;
255
}
256
245
257
sub _dir {
246
    return $fh;
258
    my ( $self ) = @_;
259
    my $dir = $self->{temporary}? $self->{tmpdir}: $self->{rootdir};
260
    $dir.= '/'. $self->{category};
261
    mkdir $dir if !-d $dir;
262
    return $dir;
263
}
247
}
264
248
265
sub _hook {
249
sub _hook {
Lines 284-297 sub _done { Link Here
284
sub _register {
268
sub _register {
285
    my ( $self, $filename, $size ) = @_;
269
    my ( $self, $filename, $size ) = @_;
286
    my $rec = Koha::UploadedFile->new({
270
    my $rec = Koha::UploadedFile->new({
271
        storage   => $self->{storage}->{name},
287
        hashvalue => $self->{files}->{$filename}->{hash},
272
        hashvalue => $self->{files}->{$filename}->{hash},
288
        filename  => $filename,
273
        filename  => $filename,
289
        dir       => $self->{category},
274
        dir       => $self->{dir},
290
        filesize  => $size,
275
        filesize  => $size,
291
        owner     => $self->{uid},
276
        owner     => $self->{uid},
292
        uploadcategorycode => $self->{category},
293
        public    => $self->{public},
277
        public    => $self->{public},
294
        permanent => $self->{temporary}? 0: 1,
278
        permanent => $self->{storage}->{temporary} ? 0 : 1,
295
    })->store;
279
    })->store;
296
    $self->{files}->{$filename}->{id} = $rec->id if $rec;
280
    $self->{files}->{$filename}->{id} = $rec->id if $rec;
297
}
281
}
Lines 301-311 sub _compute { Link Here
301
# For temporary files, the id is made unique with time
285
# For temporary files, the id is made unique with time
302
    my ( $self, $name, $block ) = @_;
286
    my ( $self, $name, $block ) = @_;
303
    if( !$self->{files}->{$name}->{hash} ) {
287
    if( !$self->{files}->{$name}->{hash} ) {
304
        my $str = $name. ( $self->{uid} // '0' ).
288
        my $str = $name . ( $self->{uid} // '0' ) .
305
            ( $self->{temporary}? Time::HiRes::time(): '' ).
289
            ( $self->{storage}->{temporary} ? Time::HiRes::time() : '' ) .
306
            $self->{category}. substr( $block, 0, BYTES_DIGEST );
290
            $self->{storage}->{name} . $self->{dir} .
291
            substr( $block, 0, BYTES_DIGEST );
307
        # since Digest cannot handle wide chars, we need to encode here
292
        # since Digest cannot handle wide chars, we need to encode here
308
        # there could be a wide char in the filename or the category
293
        # there could be a wide char in the filename
309
        my $h = Digest::MD5::md5_hex( Encode::encode_utf8( $str ) );
294
        my $h = Digest::MD5::md5_hex( Encode::encode_utf8( $str ) );
310
        $self->{files}->{$name}->{hash} = $h;
295
        $self->{files}->{$name}->{hash} = $h;
311
    }
296
    }
(-)a/cataloguing/value_builder/upload.pl (+3 lines)
Lines 41-46 my $builder = sub { Link Here
41
            if( str && str.match(/id=([0-9a-f]+)/) ) {
41
            if( str && str.match(/id=([0-9a-f]+)/) ) {
42
                term = RegExp.\$1;
42
                term = RegExp.\$1;
43
                myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1';
43
                myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1';
44
            } else if (str && str.match(/\\/([^\\/]*)\$/)) {
45
                term = RegExp.\$1;
46
                myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1';
44
            } else {
47
            } else {
45
                myurl = '../tools/upload.pl?op=new&index='+index+'&plugin=1';
48
                myurl = '../tools/upload.pl?op=new&index='+index+'&plugin=1';
46
            }
49
            }
(-)a/etc/koha-conf.xml (-2 / +9 lines)
Lines 92-99 __PAZPAR2_TOGGLE_XML_POST__ Link Here
92
 <authorityservershadow>1</authorityservershadow>
92
 <authorityservershadow>1</authorityservershadow>
93
 <pluginsdir>__PLUGINS_DIR__</pluginsdir> <!-- This entry can be repeated to use multiple directories -->
93
 <pluginsdir>__PLUGINS_DIR__</pluginsdir> <!-- This entry can be repeated to use multiple directories -->
94
 <enable_plugins>0</enable_plugins>
94
 <enable_plugins>0</enable_plugins>
95
 <upload_path></upload_path>
95
 <storage>
96
 <tmp_path></tmp_path>
96
    <name>DEFAULT</name>
97
    <adapter>directory</adapter>
98
    <adapter_params>
99
        <!-- Set storage path to enable the default permanent storage -->
100
        <path></path>
101
    </adapter_params>
102
    <hash_filename>1</hash_filename>
103
 </storage>
97
 <intranetdir>__INTRANET_CGI_DIR__</intranetdir>
104
 <intranetdir>__INTRANET_CGI_DIR__</intranetdir>
98
 <opacdir>__OPAC_CGI_DIR__/opac</opacdir>
105
 <opacdir>__OPAC_CGI_DIR__/opac</opacdir>
99
 <opachtdocs>__OPAC_TMPL_DIR__</opachtdocs>
106
 <opachtdocs>__OPAC_TMPL_DIR__</opachtdocs>
(-)a/installer/data/mysql/atomicupdate/bug_19318.perl (+36 lines)
Line 0 Link Here
1
$DBversion = 'XXX';
2
if( CheckVersion( $DBversion ) ) {
3
    unless (column_exists('uploaded_files', 'storage')) {
4
        $dbh->do(q{
5
            ALTER TABLE uploaded_files
6
            ADD COLUMN storage VARCHAR(255) NULL DEFAULT NULL AFTER id
7
        });
8
    }
9
10
    if (column_exists('uploaded_files', 'uploadcategorycode')) {
11
        $dbh->do(q{
12
            ALTER TABLE uploaded_files
13
            DROP COLUMN uploadcategorycode
14
        });
15
    }
16
17
    $dbh->do(q{
18
        UPDATE uploaded_files
19
        SET storage = IF(permanent, 'DEFAULT', 'TMP')
20
        WHERE storage IS NULL OR storage = ''
21
    });
22
23
    $dbh->do(q{
24
        UPDATE uploaded_files
25
        SET dir = ''
26
        WHERE storage = 'TMP'
27
    });
28
29
    $dbh->do(q{
30
        ALTER TABLE uploaded_files
31
        MODIFY COLUMN storage VARCHAR(255) NOT NULL
32
    });
33
34
    SetVersion( $DBversion );
35
    print "Upgrade to $DBversion done (Bug 19318 - Allow multiple storage spaces for file uploads)\n";
36
}
(-)a/installer/data/mysql/kohastructure.sql (-1 / +1 lines)
Lines 3462-3473 CREATE TABLE IF NOT EXISTS `borrower_modifications` ( Link Here
3462
DROP TABLE IF EXISTS uploaded_files;
3462
DROP TABLE IF EXISTS uploaded_files;
3463
CREATE TABLE uploaded_files (
3463
CREATE TABLE uploaded_files (
3464
    id int(11) NOT NULL AUTO_INCREMENT,
3464
    id int(11) NOT NULL AUTO_INCREMENT,
3465
    storage VARCHAR(255) NOT NULL,
3465
    hashvalue CHAR(40) NOT NULL,
3466
    hashvalue CHAR(40) NOT NULL,
3466
    filename MEDIUMTEXT NOT NULL,
3467
    filename MEDIUMTEXT NOT NULL,
3467
    dir MEDIUMTEXT NOT NULL,
3468
    dir MEDIUMTEXT NOT NULL,
3468
    filesize int(11),
3469
    filesize int(11),
3469
    dtcreated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
3470
    dtcreated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
3470
    uploadcategorycode TEXT,
3471
    owner int(11),
3471
    owner int(11),
3472
    public tinyint,
3472
    public tinyint,
3473
    permanent tinyint,
3473
    permanent tinyint,
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/offline_circ/process_koc.tt (-1 / +1 lines)
Lines 97-103 Link Here
97
            $("#fileuploadstatus").show();
97
            $("#fileuploadstatus").show();
98
            $("form#processfile #uploadedfileid").val('');
98
            $("form#processfile #uploadedfileid").val('');
99
            $("form#enqueuefile #uploadedfileid").val('');
99
            $("form#enqueuefile #uploadedfileid").val('');
100
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'temp=1', cbUpload );
100
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'storage=TMP', cbUpload );
101
        }
101
        }
102
102
103
        function cbUpload( status, fileid, errors ) {
103
        function cbUpload( status, fileid, errors ) {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt (-1 / +1 lines)
Lines 240-246 Link Here
240
            $("#processfile").hide();
240
            $("#processfile").hide();
241
            $("#fileuploadstatus").show();
241
            $("#fileuploadstatus").show();
242
            $("#uploadedfileid").val('');
242
            $("#uploadedfileid").val('');
243
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'temp=1', cbUpload );
243
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'storage=TMP', cbUpload );
244
            $("#fileuploadcancel").show();
244
            $("#fileuploadcancel").show();
245
        }
245
        }
246
        function CancelUpload() {
246
        function CancelUpload() {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt (-1 / +1 lines)
Lines 120-126 Link Here
120
            $('#uploadform button.submit').prop('disabled',true);
120
            $('#uploadform button.submit').prop('disabled',true);
121
            $("#fileuploadstatus").show();
121
            $("#fileuploadstatus").show();
122
            $("#uploadedfileid").val('');
122
            $("#uploadedfileid").val('');
123
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'temp=1', cbUpload );
123
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'storage=TMP', cbUpload );
124
        }
124
        }
125
        function cbUpload( status, fileid, errors ) {
125
        function cbUpload( status, fileid, errors ) {
126
            if( status=='done' ) {
126
            if( status=='done' ) {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload.tt (-42 / +67 lines)
Lines 1-6 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE Asset %]
2
[% USE Asset %]
3
[% USE Koha %]
3
[% USE Koha %]
4
[% USE JSON.Escape %]
4
[% SET footerjs = 1 %]
5
[% SET footerjs = 1 %]
5
[% INCLUDE 'doc-head-open.inc' %]
6
[% INCLUDE 'doc-head-open.inc' %]
6
[% IF plugin %]
7
[% IF plugin %]
Lines 11-16 Link Here
11
[% INCLUDE 'doc-head-close.inc' %]
12
[% INCLUDE 'doc-head-close.inc' %]
12
[% Asset.css("css/datatables.css") | $raw %]
13
[% Asset.css("css/datatables.css") | $raw %]
13
14
15
[% BLOCK storage_label %]
16
    [% SWITCH name %]
17
        [% CASE 'TMP' %]Temporary
18
        [% CASE 'DEFAULT' %]Default
19
        [% CASE %][% name | html %]
20
    [% END %]
21
[% END %]
22
14
[% BLOCK plugin_pars %]
23
[% BLOCK plugin_pars %]
15
    [% IF plugin %]
24
    [% IF plugin %]
16
        <input type="hidden" name="plugin" value="1" />
25
        <input type="hidden" name="plugin" value="1" />
Lines 48-75 Link Here
48
            <input type="file" id="fileToUpload" name="fileToUpload" multiple/>
57
            <input type="file" id="fileToUpload" name="fileToUpload" multiple/>
49
        </div>
58
        </div>
50
        </li>
59
        </li>
51
        [% IF uploadcategories %]
60
        <li>
52
            <li>
61
            <label for="storage">Storage: </label>
53
                <label for="uploadcategory">Category: </label>
62
            <select id="storage" name="storage">
54
                <select id="uploadcategory" name="uploadcategory">
63
                [% FOREACH storage IN storages %]
55
                [% IF !plugin %]
64
                    [% UNLESS plugin && storage.temporary %]
56
                    <option value=""></option>
65
                        <option value="[% storage.name | html %]">[% PROCESS storage_label name=storage.name %]</option>
57
                [% END %]
66
                    [% END %]
58
                [% FOREACH cat IN uploadcategories %]
59
                    <option value="[% cat.code | html %]">[% cat.name | html %]</option>
60
                [% END %]
67
                [% END %]
61
                </select>
68
            </select>
62
            </li>
69
        </li>
63
        [% END %]
70
        <li>
64
        [% IF !plugin %]
71
            <label for="dir">Directory: </label>
65
            <li>
72
            <select id="dir" name="dir">
66
            [% IF uploadcategories %]
73
            </select>
67
                <div class="hint">Note: For temporary uploads do not select a category.</div>
74
        </li>
68
            [% ELSE %]
69
                <div class="hint">Note: No upload categories are defined. Add values to the UPLOAD authorized value category otherwise all uploads will be marked as temporary.</div>
70
            [% END %]
71
            </li>
72
        [% END %]
73
        <li>
75
        <li>
74
            [% IF plugin %]
76
            [% IF plugin %]
75
                <input type="hidden" id="public" name="public" value="1"/>
77
                <input type="hidden" id="public" name="public" value="1"/>
Lines 157-165 Link Here
157
        <th>Filename</th>
159
        <th>Filename</th>
158
        <th>Size</th>
160
        <th>Size</th>
159
        <th>Hashvalue</th>
161
        <th>Hashvalue</th>
160
        <th>Category</th>
162
        <th>Storage</th>
161
        [% IF !plugin %]<th>Public</th>[% END %]
163
        <th>Directory</th>
162
        [% IF !plugin %]<th>Temporary</th>[% END %]
164
        [% IF !plugin %]
165
            <th>Public</th>
166
            <th>Temporary</th>
167
        [% END %]
163
        <th class="nosort">Actions</th>
168
        <th class="nosort">Actions</th>
164
    </tr>
169
    </tr>
165
    </thead>
170
    </thead>
Lines 169-182 Link Here
169
        <td>[% record.filename | html %]</td>
174
        <td>[% record.filename | html %]</td>
170
        <td>[% record.filesize | html %]</td>
175
        <td>[% record.filesize | html %]</td>
171
        <td>[% record.hashvalue | html %]</td>
176
        <td>[% record.hashvalue | html %]</td>
172
        <td>[% record.uploadcategorycode | html %]</td>
177
        <td>[% PROCESS storage_label name=record.storage %]</td>
178
        <td>[% record.dir | html %]</td>
173
        [% IF !plugin %]
179
        [% IF !plugin %]
174
            <td>[% IF record.public %]Yes[% ELSE %]No[% END %]</td>
180
            <td>[% IF record.public %]Yes[% ELSE %]No[% END %]</td>
175
            <td>[% IF record.permanent %]No[% ELSE %]Yes[% END %]</td>
181
            <td>[% IF record.permanent %]No[% ELSE %]Yes[% END %]</td>
176
        [% END %]
182
        [% END %]
177
        <td class="actions">
183
        <td class="actions">
178
            [% IF plugin %]
184
            [% IF plugin %]
179
                <button class="btn btn-default btn-xs choose_entry" data-record-hashvalue="[% record.hashvalue | html %]"><i class="fa fa-plus"></i> Choose</button>
185
                <button class="btn btn-default btn-xs choose_entry" data-record-url="[% record.url | html %]"><i class="fa fa-plus"></i> Choose</button>
180
            [% END %]
186
            [% END %]
181
            <button class="btn btn-default btn-xs download_entry" data-record-id="[% record.id | html %]"><i class="fa fa-download"></i> Download</button>
187
            <button class="btn btn-default btn-xs download_entry" data-record-id="[% record.id | html %]"><i class="fa fa-download"></i> Download</button>
182
            [% IF record.owner == owner || CAN_user_tools_upload_manage %]
188
            [% IF record.owner == owner || CAN_user_tools_upload_manage %]
Lines 265-281 Link Here
265
            $("#searchfile").hide();
271
            $("#searchfile").hide();
266
            $("#lastbreadcrumb").text( _("Add a new upload") );
272
            $("#lastbreadcrumb").text( _("Add a new upload") );
267
273
268
            var cat, xtra='';
274
            var xtra = 'storage=' + $('#storage').val();
269
            if( $("#uploadcategory").val() )
275
            xtra = xtra + '&dir=' + $('#dir').val();
270
                cat = encodeURIComponent( $("#uploadcategory").val() );
271
            if( cat ) xtra= 'category=' + cat + '&';
272
            [% IF plugin %]
276
            [% IF plugin %]
273
                xtra = xtra + 'public=1&temp=0';
277
                xtra = xtra + '&public=1';
274
            [% ELSE %]
278
            [% ELSE %]
275
                if( !cat ) xtra = 'temp=1&';
279
                if ( $('#public').prop('checked') ) {
276
                if( $('#public').prop('checked') ) xtra = xtra + 'public=1';
280
                    xtra = xtra + '&public=1';
281
                }
277
            [% END %]
282
            [% END %]
278
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), xtra, cbUpload );
283
            xhr = AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), xtra, cbUpload );
279
        }
284
        }
280
        function CancelUpload() {
285
        function CancelUpload() {
281
            if( xhr ) xhr.abort();
286
            if( xhr ) xhr.abort();
Lines 318-324 Link Here
318
            var rv;
323
            var rv;
319
            switch(code) {
324
            switch(code) {
320
                case 'UPLERR_ALREADY_EXISTS':
325
                case 'UPLERR_ALREADY_EXISTS':
321
                    rv = _("This file already exists (in this category).");
326
                    rv = _("This file already exists (in this storage).");
322
                    break;
327
                    break;
323
                case 'UPLERR_CANNOT_WRITE':
328
                case 'UPLERR_CANNOT_WRITE':
324
                    rv = _("File could not be created. Check permissions.");
329
                    rv = _("File could not be created. Check permissions.");
Lines 367-378 Link Here
367
                $(window.opener.document).find('#[% index | html %]').val( '' );
372
                $(window.opener.document).find('#[% index | html %]').val( '' );
368
            [% END %]
373
            [% END %]
369
        }
374
        }
370
        function Choose(hashval) {
375
        function Choose(url) {
371
            var res = '[% Koha.Preference('OPACBaseURL') | html %]';
372
            res = res.replace( /\/$/, '');
373
            res = res + '/cgi-bin/koha/opac-retrieve-file.pl?id=' + hashval;
374
            [% IF index %]
376
            [% IF index %]
375
                $(window.opener.document).find('#[% index | html %]').val( res );
377
                $(window.opener.document).find('#[% index | html %]').val( url );
376
            [% END %]
378
            [% END %]
377
            window.close();
379
            window.close();
378
        }
380
        }
Lines 403-410 Link Here
403
            });
405
            });
404
            $(".choose_entry").on("click",function(e){
406
            $(".choose_entry").on("click",function(e){
405
                e.preventDefault();
407
                e.preventDefault();
406
                var record_hashvalue = $(this).data("record-hashvalue");
408
                var record_url = $(this).data("record-url");
407
                Choose( record_hashvalue );
409
                Choose( record_url );
408
            });
410
            });
409
            $(".download_entry").on("click",function(e){
411
            $(".download_entry").on("click",function(e){
410
                e.preventDefault();
412
                e.preventDefault();
Lines 422-427 Link Here
422
            });
424
            });
423
        });
425
        });
424
    </script>
426
    </script>
427
    <script>
428
        [% FOREACH storage IN storages %]
429
            [% name = storage.name %]
430
            [% storage_directories.$name = storage.directories %]
431
        [% END %]
432
433
        $(document).ready(function () {
434
            let storage_directories = [% storage_directories.json %];
435
            $('#storage').on('change', function () {
436
                $('#dir').empty();
437
                $('#dir').append($('<option>').val('').html(_("(root)")));
438
                let name = $(this).val()
439
                if (name in storage_directories) {
440
                    storage_directories[name].forEach(function (dir) {
441
                        let option = $('<option>')
442
                            .val(dir)
443
                            .html(dir);
444
                        $('#dir').append(option);
445
                    })
446
                }
447
            }).change();
448
        });
449
    </script>
425
[% END %]
450
[% END %]
426
451
427
[% INCLUDE 'intranet-bottom.inc' %]
452
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/t/ImportBatch.t (-2 / +13 lines)
Lines 31-37 BEGIN { Link Here
31
t::lib::Mocks::mock_preference('marcflavour', 'MARC21');
31
t::lib::Mocks::mock_preference('marcflavour', 'MARC21');
32
32
33
subtest 'RecordsFromISO2709File' => sub {
33
subtest 'RecordsFromISO2709File' => sub {
34
    plan tests => 4;
34
    plan tests => 5;
35
35
36
    my ( $errors, $recs );
36
    my ( $errors, $recs );
37
    my $file = create_file({ whitespace => 1, format => 'marc' });
37
    my $file = create_file({ whitespace => 1, format => 'marc' });
Lines 48-57 subtest 'RecordsFromISO2709File' => sub { Link Here
48
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromISO2709File( $file, 'biblio', 'UTF-8' );
48
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromISO2709File( $file, 'biblio', 'UTF-8' );
49
    is( @$recs, 2, 'File contains 2 records' );
49
    is( @$recs, 2, 'File contains 2 records' );
50
50
51
    $file = create_file({ two => 1, format => 'marc' });
52
    open my $fh, '<', $file;
53
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromISO2709File( $fh, 'biblio', 'UTF-8' );
54
    close $fh;
55
    is( @$recs, 2, 'Can take a file handle as parameter' );
56
51
};
57
};
52
58
53
subtest 'RecordsFromMARCXMLFile' => sub {
59
subtest 'RecordsFromMARCXMLFile' => sub {
54
    plan tests => 3;
60
    plan tests => 4;
55
61
56
    my ( $errors, $recs );
62
    my ( $errors, $recs );
57
    my $file = create_file({ whitespace => 1, format => 'marcxml' });
63
    my $file = create_file({ whitespace => 1, format => 'marcxml' });
Lines 66-71 subtest 'RecordsFromMARCXMLFile' => sub { Link Here
66
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' );
72
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' );
67
    is( @$recs, 2, 'File has two records' );
73
    is( @$recs, 2, 'File has two records' );
68
74
75
    $file = create_file({ two => 1, format => 'marcxml' });
76
    open my $fh, '<', $file;
77
    ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $fh, 'UTF-8' );
78
    close $fh;
79
    is( @$recs, 2, 'Can take a filehandle as parameter' );
69
};
80
};
70
81
71
sub create_file {
82
sub create_file {
(-)a/t/Koha/Storage.t (+183 lines)
Line 0 Link Here
1
# Copyright 2018 BibLibre
2
#
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use File::Path qw(remove_tree make_path);
21
use File::Spec;
22
use File::Temp qw(tempdir);
23
use Test::More tests => 5;
24
use Test::MockModule;
25
26
use C4::Context;
27
28
BEGIN { use_ok('Koha::Storage') }
29
30
my $storage_config = [
31
    {
32
        name => 'DEFAULT',
33
        adapter => 'directory',
34
        adapter_params => {
35
            path => tempdir('koha-storage-DEFAULT-XXXXXX', TMPDIR => 1, CLEANUP => 1),
36
        },
37
        hash_filename => 1,
38
    },
39
    {
40
        name => 'external',
41
        adapter => 'directory',
42
        adapter_params => {
43
            path => tempdir('koha-storage-external-XXXXXX', TMPDIR => 1, CLEANUP => 1),
44
        },
45
        baseurl => 'https://external.example.com/',
46
    },
47
];
48
49
my $c4_context = Test::MockModule->new('C4::Context');
50
$c4_context->mock('config', sub {
51
    my ($class, $name) = @_;
52
53
    if ($name eq 'storage') {
54
        return $storage_config;
55
    }
56
57
    return C4::Context::_common_config($name, 'config')
58
});
59
60
my $config = Koha::Storage->config;
61
62
my $expected = [
63
    {
64
        name => 'TMP',
65
        adapter => 'directory',
66
        adapter_params => {
67
            path => File::Spec->catfile(File::Spec->tmpdir, C4::Context->config('database') . '_upload'),
68
        },
69
        temporary => 1,
70
        hash_filename => 1,
71
    },
72
    @$storage_config,
73
];
74
75
is_deeply($config, $expected, 'Koha::Storage->config return value is as expected');
76
77
subtest 'TMP' => sub {
78
    plan tests => 8;
79
80
    # Clean temporary storage
81
    my $tmpdir = $config->[0]->{adapter_params}->{path};
82
    if (-e $tmpdir) {
83
        remove_tree($tmpdir);
84
    }
85
86
    my $tmp_storage = Koha::Storage->get_instance('TMP');
87
    isa_ok($tmp_storage, 'Koha::Storage', 'Koha::Storage->get_instance return value');
88
    is($tmp_storage->{name}, 'TMP', 'Koha::Storage->get_instance returns the correct instance');
89
90
    my $filepath = $tmp_storage->filepath({
91
        dir => 'one/two',
92
        hashvalue => 'abcdef',
93
        filename => 'foo.bar',
94
    });
95
    is($filepath, 'one/two/abcdef_foo.bar', 'filepath is correct');
96
97
    ok(!$tmp_storage->exists($filepath), "$filepath doesn't exist yet");
98
99
    my $fh = $tmp_storage->fh($filepath, 'w');
100
    print $fh 'foo.bar content';
101
    close $fh;
102
103
    ok($tmp_storage->exists($filepath), "$filepath now exists");
104
105
    $fh = $tmp_storage->fh($filepath, 'r');
106
    my $content = <$fh>;
107
    is($content, 'foo.bar content', "$filepath content is as expected");
108
109
    my $directories = $tmp_storage->directories;
110
    is_deeply($directories, ['one', 'one/two'], 'directories() return value is as expected');
111
112
    my $url = $tmp_storage->url('abcdef', $filepath);
113
    my $expected_url = C4::Context->preference('OPACBaseURL') . '/cgi-bin/koha/opac-retrieve-file.pl?id=abcdef';
114
    is($url, $expected_url, 'url() return value is as expected');
115
};
116
117
subtest 'DEFAULT' => sub {
118
    plan tests => 8;
119
120
    my $storage = Koha::Storage->get_instance('DEFAULT');
121
    isa_ok($storage, 'Koha::Storage', 'Koha::Storage->get_instance return value');
122
    is($storage->{name}, 'DEFAULT', 'Koha::Storage->get_instance returns the correct instance');
123
124
    my $filepath = $storage->filepath({
125
        dir => 'one/two',
126
        hashvalue => 'abcdef',
127
        filename => 'foo.bar',
128
    });
129
    is($filepath, 'one/two/abcdef_foo.bar', 'filepath is correct');
130
131
    ok(!$storage->exists($filepath), "$filepath doesn't exist yet");
132
133
    my $fh = $storage->fh($filepath, 'w');
134
    print $fh 'foo.bar content';
135
    close $fh;
136
137
    ok($storage->exists($filepath), "$filepath now exists");
138
139
    $fh = $storage->fh($filepath, 'r');
140
    my $content = <$fh>;
141
    is($content, 'foo.bar content', "$filepath content is as expected");
142
143
    my $directories = $storage->directories;
144
    is_deeply($directories, ['one', 'one/two'], 'directories() return value is as expected');
145
146
    my $url = $storage->url('abcdef', $filepath);
147
    my $expected_url = C4::Context->preference('OPACBaseURL') . '/cgi-bin/koha/opac-retrieve-file.pl?id=abcdef';
148
    is($url, $expected_url, 'url() return value is as expected');
149
};
150
151
subtest 'external' => sub {
152
    plan tests => 8;
153
154
    my $storage = Koha::Storage->get_instance('external');
155
    isa_ok($storage, 'Koha::Storage', 'Koha::Storage->get_instance return value');
156
    is($storage->{name}, 'external', 'Koha::Storage->get_instance returns the correct instance');
157
158
    my $filepath = $storage->filepath({
159
        dir => 'one/two',
160
        hashvalue => 'abcdef',
161
        filename => 'foo.bar',
162
    });
163
    is($filepath, 'one/two/foo.bar', 'filepath is correct');
164
165
    ok(!$storage->exists($filepath), "$filepath doesn't exist yet");
166
167
    my $fh = $storage->fh($filepath, 'w');
168
    print $fh 'foo.bar content';
169
    close $fh;
170
171
    ok($storage->exists($filepath), "$filepath now exists");
172
173
    $fh = $storage->fh($filepath, 'r');
174
    my $content = <$fh>;
175
    is($content, 'foo.bar content', "$filepath content is as expected");
176
177
    my $directories = $storage->directories;
178
    is_deeply($directories, ['one', 'one/two'], 'directories() return value is as expected');
179
180
    my $url = $storage->url('abcdef', $filepath);
181
    my $expected_url = 'https://external.example.com/one/two/foo.bar';
182
    is($url, $expected_url, 'url() return value is as expected');
183
};
(-)a/t/db_dependent/ImportBatch.t (-1 / +6 lines)
Lines 171-177 my $batch3_results = $dbh->do('SELECT * FROM import_batches WHERE import_batch_i Link Here
171
is( $batch3_results, "0E0", "Batch 3 has been deleted");
171
is( $batch3_results, "0E0", "Batch 3 has been deleted");
172
172
173
subtest "RecordsFromMarcPlugin" => sub {
173
subtest "RecordsFromMarcPlugin" => sub {
174
    plan tests => 5;
174
    plan tests => 6;
175
175
176
    # Create a test file
176
    # Create a test file
177
    my ( $fh, $name ) = tempfile();
177
    my ( $fh, $name ) = tempfile();
Lines 196-201 subtest "RecordsFromMarcPlugin" => sub { Link Here
196
        'Checked one field in first record' );
196
        'Checked one field in first record' );
197
    is( $records->[1]->subfield('100', 'a'), 'Another',
197
    is( $records->[1]->subfield('100', 'a'), 'Another',
198
        'Checked one field in second record' );
198
        'Checked one field in second record' );
199
200
    open my $fh2, '<', $name;
201
    $records = C4::ImportBatch::RecordsFromMarcPlugin( $fh2, ref $plugin, 'UTF-8' );
202
    close $fh2;
203
    is( @$records, 2, 'Can take a filehandle as parameter' );
199
};
204
};
200
205
201
$schema->storage->txn_rollback;
206
$schema->storage->txn_rollback;
(-)a/t/db_dependent/Upload.t (-46 / +60 lines)
Lines 2-8 Link Here
2
2
3
use Modern::Perl;
3
use Modern::Perl;
4
use File::Temp qw/ tempdir /;
4
use File::Temp qw/ tempdir /;
5
use Test::More tests => 13;
5
use Test::More tests => 12;
6
use Test::Warn;
6
use Test::Warn;
7
7
8
use Test::MockModule;
8
use Test::MockModule;
Lines 23-56 our $builder = t::lib::TestBuilder->new; Link Here
23
our $current_upload = 0;
23
our $current_upload = 0;
24
our $uploads = [
24
our $uploads = [
25
    [
25
    [
26
        { name => 'file1', cat => 'A', size => 6000 },
26
        { name => 'file1', storage => 'DEFAULT', dir => 'A', size => 6000 },
27
        { name => 'file2', cat => 'A', size => 8000 },
27
        { name => 'file2', storage => 'DEFAULT', dir => 'A', size => 8000 },
28
    ],
28
    ],
29
    [
29
    [
30
        { name => 'file3', cat => 'B', size => 1000 },
30
        { name => 'file3', storage => 'DEFAULT', dir => 'B', size => 1000 },
31
    ],
31
    ],
32
    [
32
    [
33
        { name => 'file4', cat => undef, size => 5000 }, # temporary
33
        { name => 'file4', storage => 'TMP', dir => undef, size => 5000 },
34
    ],
34
    ],
35
    [
35
    [
36
        { name => 'file2', cat => 'A', size => 8000 },
36
        { name => 'file2', storage => 'DEFAULT', dir => 'A', size => 8000 },
37
        # uploading a duplicate in cat A should fail
37
        # uploading a duplicate in dir A should fail
38
    ],
38
    ],
39
    [
39
    [
40
        { name => 'file4', cat => undef, size => 5000 }, # temp duplicate
40
        { name => 'file4', storage => 'TMP', dir => undef, size => 5000 }, # temp duplicate
41
    ],
41
    ],
42
    [
42
    [
43
        { name => 'file5', cat => undef, size => 7000 },
43
        { name => 'file5', storage => 'DEFAULT', dir => undef, size => 7000 },
44
    ],
44
    ],
45
    [
45
    [
46
        { name => 'file6', cat => undef, size => 6500 },
46
        { name => 'file6', storage => 'TMP', dir => undef, size => 6500 },
47
        { name => 'file7', cat => undef, size => 6501 },
47
        { name => 'file7', storage => 'TMP', dir => undef, size => 6501 },
48
    ],
48
    ],
49
];
49
];
50
50
51
# Redirect upload dir structure and mock C4::Context and CGI
51
# Redirect upload dir structure and mock C4::Context and CGI
52
my $tempdir = tempdir( CLEANUP => 1 );
52
my $tempdir = tempdir( CLEANUP => 1 );
53
t::lib::Mocks::mock_config('upload_path', $tempdir);
53
my $storage_config = [
54
    {
55
        name => 'DEFAULT',
56
        adapter => 'directory',
57
        adapter_params => {
58
            path => $tempdir,
59
        },
60
        hash_filename => 1,
61
    },
62
];
63
t::lib::Mocks::mock_config('storage', $storage_config);
54
my $specmod = Test::MockModule->new( 'C4::Context' );
64
my $specmod = Test::MockModule->new( 'C4::Context' );
55
$specmod->mock( 'temporary_directory' => sub { return $tempdir; } );
65
$specmod->mock( 'temporary_directory' => sub { return $tempdir; } );
56
my $cgimod = Test::MockModule->new( 'CGI' );
66
my $cgimod = Test::MockModule->new( 'CGI' );
Lines 68-88 subtest 'Make a fresh start' => sub { Link Here
68
    is( Koha::UploadedFiles->count, 0, 'No records left' );
78
    is( Koha::UploadedFiles->count, 0, 'No records left' );
69
};
79
};
70
80
71
subtest 'permanent_directory and temporary_directory' => sub {
72
    plan tests => 2;
73
74
    # Check mocked directories
75
    is( Koha::UploadedFile->permanent_directory, $tempdir,
76
        'Check permanent directory' );
77
    is( C4::Context::temporary_directory, $tempdir,
78
        'Check temporary directory' );
79
};
80
81
subtest 'Add two uploads in category A' => sub {
81
subtest 'Add two uploads in category A' => sub {
82
    plan tests => 9;
82
    plan tests => 9;
83
83
84
    my $upl = Koha::Uploader->new({
84
    my $upl = Koha::Uploader->new({
85
        category => $uploads->[$current_upload]->[0]->{cat},
85
        storage => $uploads->[$current_upload]->[0]->{storage},
86
        dir => $uploads->[$current_upload]->[0]->{dir},
86
    });
87
    });
87
    my $cgi= $upl->cgi;
88
    my $cgi= $upl->cgi;
88
    my $res= $upl->result;
89
    my $res= $upl->result;
Lines 95-120 subtest 'Add two uploads in category A' => sub { Link Here
95
    }, { order_by => { -asc => 'filename' }});
96
    }, { order_by => { -asc => 'filename' }});
96
    my $rec = $rs->next;
97
    my $rec = $rs->next;
97
    is( $rec->filename, 'file1', 'Check file name' );
98
    is( $rec->filename, 'file1', 'Check file name' );
98
    is( $rec->uploadcategorycode, 'A', 'Check category A' );
99
    is( $rec->dir, 'A', 'Check dir A' );
99
    is( $rec->filesize, 6000, 'Check size of file1' );
100
    is( $rec->filesize, 6000, 'Check size of file1' );
100
    $rec = $rs->next;
101
    $rec = $rs->next;
101
    is( $rec->filename, 'file2', 'Check file name 2' );
102
    is( $rec->filename, 'file2', 'Check file name 2' );
102
    is( $rec->filesize, 8000, 'Check size of file2' );
103
    is( $rec->filesize, 8000, 'Check size of file2' );
103
    is( $rec->public, undef, 'Check public undefined' );
104
    is( $rec->public, 0, 'Check public 0' );
104
};
105
};
105
106
106
subtest 'Add another upload, check file_handle' => sub {
107
subtest 'Add another upload, check file_handle' => sub {
107
    plan tests => 5;
108
    plan tests => 5;
108
109
109
    my $upl = Koha::Uploader->new({
110
    my $upl = Koha::Uploader->new({
110
        category => $uploads->[$current_upload]->[0]->{cat},
111
        storage => $uploads->[$current_upload]->[0]->{storage},
112
        dir => $uploads->[$current_upload]->[0]->{dir},
111
        public => 1,
113
        public => 1,
112
    });
114
    });
113
    my $cgi= $upl->cgi;
115
    my $cgi= $upl->cgi;
114
    is( $upl->count, 1, 'Upload 2 includes one file' );
116
    is( $upl->count, 1, 'Upload 2 includes one file' );
115
    my $res= $upl->result;
117
    my $res= $upl->result;
116
    my $rec = Koha::UploadedFiles->find( $res );
118
    my $rec = Koha::UploadedFiles->find( $res );
117
    is( $rec->uploadcategorycode, 'B', 'Check category B' );
119
    is( $rec->dir, 'B', 'Check dir B' );
118
    is( $rec->public, 1, 'Check public == 1' );
120
    is( $rec->public, 1, 'Check public == 1' );
119
    my $fh = $rec->file_handle;
121
    my $fh = $rec->file_handle;
120
    is( ref($fh) eq 'IO::File' && $fh->opened, 1, 'Get returns a file handle' );
122
    is( ref($fh) eq 'IO::File' && $fh->opened, 1, 'Get returns a file handle' );
Lines 128-145 subtest 'Add another upload, check file_handle' => sub { Link Here
128
subtest 'Add temporary upload' => sub {
130
subtest 'Add temporary upload' => sub {
129
    plan tests => 2;
131
    plan tests => 2;
130
132
131
    my $upl = Koha::Uploader->new({ tmp => 1 }); #temporary
133
    my $upl = Koha::Uploader->new({
134
        storage => $uploads->[$current_upload]->[0]->{storage},
135
        dir => $uploads->[$current_upload]->[0]->{dir},
136
    });
132
    my $cgi= $upl->cgi;
137
    my $cgi= $upl->cgi;
133
    is( $upl->count, 1, 'Upload 3 includes one temporary file' );
138
    is( $upl->count, 1, 'Upload 3 includes one temporary file' );
134
    my $rec = Koha::UploadedFiles->find( $upl->result );
139
    my $rec = Koha::UploadedFiles->find( $upl->result );
135
    is( $rec->uploadcategorycode =~ /_upload$/, 1, 'Check category temp file' );
140
    is( $rec->dir, '', 'Check dir is empty' );
136
};
141
};
137
142
138
subtest 'Add same file in same category' => sub {
143
subtest 'Add same file in same category' => sub {
139
    plan tests => 3;
144
    plan tests => 3;
140
145
141
    my $upl = Koha::Uploader->new({
146
    my $upl = Koha::Uploader->new({
142
        category => $uploads->[$current_upload]->[0]->{cat},
147
        storage => $uploads->[$current_upload]->[0]->{storage},
148
        dir => $uploads->[$current_upload]->[0]->{dir},
143
    });
149
    });
144
    my $cgi= $upl->cgi;
150
    my $cgi= $upl->cgi;
145
    is( $upl->count, 0, 'Upload 4 failed as expected' );
151
    is( $upl->count, 0, 'Upload 4 failed as expected' );
Lines 152-182 subtest 'Test delete via UploadedFile as well as UploadedFiles' => sub { Link Here
152
    plan tests => 10;
158
    plan tests => 10;
153
159
154
    # add temporary file with same name and contents (file4)
160
    # add temporary file with same name and contents (file4)
155
    my $upl = Koha::Uploader->new({ tmp => 1 });
161
    my $upl = Koha::Uploader->new({
162
        storage => $uploads->[$current_upload]->[0]->{storage},
163
        dir => $uploads->[$current_upload]->[0]->{dir},
164
    });
156
    my $cgi= $upl->cgi;
165
    my $cgi= $upl->cgi;
157
    is( $upl->count, 1, 'Add duplicate temporary file (file4)' );
166
    is( $upl->count, 1, 'Add duplicate temporary file (file4)' );
158
    my $id = $upl->result;
167
    my $id = $upl->result;
159
    my $path = Koha::UploadedFiles->find( $id )->full_path;
168
    my $uploaded_file = Koha::UploadedFiles->find( $id );
169
    my $storage = Koha::Storage->get_instance($uploaded_file->storage);
160
170
161
    # testing delete via UploadedFiles (plural)
171
    # testing delete via UploadedFiles (plural)
162
    my $delete = Koha::UploadedFiles->search({ id => $id })->delete;
172
    my $delete = Koha::UploadedFiles->search({ id => $id })->delete;
163
    isnt( $delete, "0E0", 'Delete successful' );
173
    isnt( $delete, "0E0", 'Delete successful' );
164
    isnt( -e $path, 1, 'File no longer found after delete' );
174
    isnt( $storage->exists($uploaded_file->filepath), 1, 'File no longer found after delete' );
165
    is( Koha::UploadedFiles->find( $id ), undef, 'Record also gone' );
175
    is( Koha::UploadedFiles->find( $id ), undef, 'Record also gone' );
166
176
167
    # testing delete via UploadedFile (singular)
177
    # testing delete via UploadedFile (singular)
168
    # Note that find returns a Koha::Object
178
    # Note that find returns a Koha::Object
169
    $upl = Koha::Uploader->new({ tmp => 1 });
179
    $upl = Koha::Uploader->new({
180
        storage => $uploads->[$current_upload]->[0]->{storage},
181
        dir => $uploads->[$current_upload]->[0]->{dir},
182
    });
170
    $upl->cgi;
183
    $upl->cgi;
171
    my $kohaobj = Koha::UploadedFiles->find( $upl->result );
184
    my $kohaobj = Koha::UploadedFiles->find( $upl->result );
172
    $path = $kohaobj->full_path;
185
    $storage = Koha::Storage->get_instance($kohaobj->storage);
173
    $delete = $kohaobj->delete;
186
    $delete = $kohaobj->delete;
174
    ok( $delete=~/^-?1$/, 'Delete successful' );
187
    ok( $delete=~/^-?1$/, 'Delete successful' );
175
    isnt( -e $path, 1, 'File no longer found after delete' );
188
    isnt($storage->exists($kohaobj->filepath), 1, 'File no longer found after delete' );
176
189
177
    # add another record with TestBuilder, so file does not exist
190
    # add another record with TestBuilder, so file does not exist
178
    # catch warning
191
    # catch warning
179
    my $upload01 = $builder->build({ source => 'UploadedFile' });
192
    my $upload01 = $builder->build({ source => 'UploadedFile', value => {storage => 'TMP'} });
180
    warning_like { $delete = Koha::UploadedFiles->find( $upload01->{id} )->delete; }
193
    warning_like { $delete = Koha::UploadedFiles->find( $upload01->{id} )->delete; }
181
        qr/file was missing/,
194
        qr/file was missing/,
182
        'delete warns when file is missing';
195
        'delete warns when file is missing';
Lines 196-203 subtest 'Test delete_missing' => sub { Link Here
196
    plan tests => 5;
209
    plan tests => 5;
197
210
198
    # If we add files via TestBuilder, they do not exist
211
    # If we add files via TestBuilder, they do not exist
199
    my $upload01 = $builder->build({ source => 'UploadedFile' });
212
    my $upload01 = $builder->build({ source => 'UploadedFile', value => {storage => 'TMP'} });
200
    my $upload02 = $builder->build({ source => 'UploadedFile' });
213
    my $upload02 = $builder->build({ source => 'UploadedFile', value => {storage => 'TMP'} });
201
    # dry run first
214
    # dry run first
202
    my $deleted = Koha::UploadedFiles->delete_missing({ keep_record => 1 });
215
    my $deleted = Koha::UploadedFiles->delete_missing({ keep_record => 1 });
203
    is( $deleted, 2, 'Expect two records with missing files' );
216
    is( $deleted, 2, 'Expect two records with missing files' );
Lines 224-238 subtest 'Call search_term with[out] private flag' => sub { Link Here
224
    })->count, 4, 'Returns now four results' );
237
    })->count, 4, 'Returns now four results' );
225
};
238
};
226
239
227
subtest 'Simple tests for httpheaders and getCategories' => sub {
240
subtest 'Simple tests for httpheaders' => sub {
228
    plan tests => 2;
241
    plan tests => 1;
229
242
230
    my $rec = Koha::UploadedFiles->search_term({ term => 'file' })->next;
243
    my $rec = Koha::UploadedFiles->search_term({ term => 'file' })->next;
231
    my @hdrs = $rec->httpheaders;
244
    my @hdrs = $rec->httpheaders;
232
    is( @hdrs == 4 && $hdrs[1] =~ /application\/octet-stream/, 1, 'Simple test for httpheaders');
245
    is( @hdrs == 4 && $hdrs[1] =~ /application\/octet-stream/, 1, 'Simple test for httpheaders');
233
    $builder->build({ source => 'AuthorisedValue', value => { category => 'UPLOAD', authorised_value => 'HAVE_AT_LEAST_ONE', lib => 'Hi there' } });
246
    $builder->build({ source => 'AuthorisedValue', value => { category => 'UPLOAD', authorised_value => 'HAVE_AT_LEAST_ONE', lib => 'Hi there' } });
234
    my $cat = Koha::UploadedFiles->getCategories;
235
    is( @$cat >= 1, 1, 'getCategories returned at least one category' );
236
};
247
};
237
248
238
subtest 'Testing allows_add_by' => sub {
249
subtest 'Testing allows_add_by' => sub {
Lines 277-283 subtest 'Testing delete_temporary' => sub { Link Here
277
    plan tests => 9;
288
    plan tests => 9;
278
289
279
    # Add two temporary files: result should be 3 + 3
290
    # Add two temporary files: result should be 3 + 3
280
    Koha::Uploader->new({ tmp => 1 })->cgi; # add file6 and file7
291
    Koha::Uploader->new({
292
        storage => $uploads->[$current_upload]->[0]->{storage},
293
        dir => $uploads->[$current_upload]->[0]->{dir},
294
    })->cgi; # add file6 and file7
281
    is( Koha::UploadedFiles->search->count, 6, 'Test starting count' );
295
    is( Koha::UploadedFiles->search->count, 6, 'Test starting count' );
282
    is( Koha::UploadedFiles->search({ permanent => 1 })->count, 3,
296
    is( Koha::UploadedFiles->search({ permanent => 1 })->count, 3,
283
        'Includes 3 permanent' );
297
        'Includes 3 permanent' );
Lines 318-325 subtest 'Testing delete_temporary' => sub { Link Here
318
332
319
subtest 'Testing download headers' => sub {
333
subtest 'Testing download headers' => sub {
320
    plan tests => 2;
334
    plan tests => 2;
321
    my $test_pdf = Koha::UploadedFile->new({ filename => 'pdf.pdf', uploadcategorycode => 'B', filesize => 1000 });
335
    my $test_pdf = Koha::UploadedFile->new({ filename => 'pdf.pdf', storage => 'DEFAULT', dir => 'B', filesize => 1000 });
322
    my $test_not = Koha::UploadedFile->new({ filename => 'pdf.not', uploadcategorycode => 'B', filesize => 1000 });
336
    my $test_not = Koha::UploadedFile->new({ filename => 'pdf.not', storage => 'DEFAULT', dir => 'B', filesize => 1000 });
323
    my @pdf_expect = ( '-type'=>'application/pdf','Content-Disposition'=>'inline; filename=pdf.pdf' );
337
    my @pdf_expect = ( '-type'=>'application/pdf','Content-Disposition'=>'inline; filename=pdf.pdf' );
324
    my @not_expect = ( '-type'=>'application/octet-stream','-attachment'=>'pdf.not' );
338
    my @not_expect = ( '-type'=>'application/octet-stream','-attachment'=>'pdf.not' );
325
    my @pdf_head = $test_pdf->httpheaders;
339
    my @pdf_head = $test_pdf->httpheaders;
(-)a/tools/stage-marc-import.pl (-4 / +5 lines)
Lines 84-100 if ($completedJobID) { Link Here
84
    $template->param(map { $_ => $results->{$_} } keys %{ $results });
84
    $template->param(map { $_ => $results->{$_} } keys %{ $results });
85
} elsif ($fileID) {
85
} elsif ($fileID) {
86
    my $upload = Koha::UploadedFiles->find( $fileID );
86
    my $upload = Koha::UploadedFiles->find( $fileID );
87
    my $file = $upload->full_path;
87
    my $storage = Koha::Storage->get_instance($upload->storage);
88
    my $fh = $storage->fh($upload->filepath, 'r');
88
    my $filename = $upload->filename;
89
    my $filename = $upload->filename;
89
90
90
    my ( $errors, $marcrecords );
91
    my ( $errors, $marcrecords );
91
    if( $format eq 'MARCXML' ) {
92
    if( $format eq 'MARCXML' ) {
92
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, $encoding);
93
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromMARCXMLFile( $fh, $encoding);
93
    } elsif( $format eq 'ISO2709' ) {
94
    } elsif( $format eq 'ISO2709' ) {
94
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromISO2709File( $file, $record_type, $encoding );
95
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromISO2709File( $fh, $record_type, $encoding );
95
    } else { # plugin based
96
    } else { # plugin based
96
        $errors = [];
97
        $errors = [];
97
        $marcrecords = C4::ImportBatch::RecordsFromMarcPlugin( $file, $format, $encoding );
98
        $marcrecords = C4::ImportBatch::RecordsFromMarcPlugin( $fh, $format, $encoding );
98
    }
99
    }
99
    warn "$filename: " . ( join ',', @$errors ) if @$errors;
100
    warn "$filename: " . ( join ',', @$errors ) if @$errors;
100
        # no need to exit if we have no records (or only errors) here
101
        # no need to exit if we have no records (or only errors) here
(-)a/tools/upload-cover-image.pl (-2 / +8 lines)
Lines 39-44 resized, maintaining aspect ratio. Link Here
39
39
40
use Modern::Perl;
40
use Modern::Perl;
41
41
42
use Archive::Zip qw(:ERROR_CODES);
42
use File::Temp;
43
use File::Temp;
43
use CGI qw ( -utf8 );
44
use CGI qw ( -utf8 );
44
use GD;
45
use GD;
Lines 102-110 if ($fileID) { Link Here
102
        undef $srcimage;
103
        undef $srcimage;
103
    }
104
    }
104
    else {
105
    else {
105
        my $filename = $upload->full_path;
106
        my $storage = Koha::Storage->get_instance($upload->storage);
107
        my $fh = $storage->fh($upload->filepath, 'r');
106
        my $dirname = File::Temp::tempdir( CLEANUP => 1 );
108
        my $dirname = File::Temp::tempdir( CLEANUP => 1 );
107
        unless ( system( "unzip", $filename, '-d', $dirname ) == 0 ) {
109
        my $zip = Archive::Zip->new();
110
        $zip->readFromFileHandle($fh);
111
        unless (AZ_OK == $zip->extractTree(undef, $dirname)) {
108
            $error = 'UZIPFAIL';
112
            $error = 'UZIPFAIL';
109
        }
113
        }
110
        else {
114
        else {
Lines 144-149 if ($fileID) { Link Here
144
                            $error = 'DELERR';
148
                            $error = 'DELERR';
145
                        }
149
                        }
146
                        else {
150
                        else {
151
                            my $filename;
147
                            ( $biblionumber, $filename ) = split $delim, $line, 2;
152
                            ( $biblionumber, $filename ) = split $delim, $line, 2;
148
                            $biblionumber =~
153
                            $biblionumber =~
149
                              s/[\"\r\n]//g;    # remove offensive characters
154
                              s/[\"\r\n]//g;    # remove offensive characters
Lines 176-181 if ($fileID) { Link Here
176
                }
181
                }
177
            }
182
            }
178
        }
183
        }
184
        close $fh;
179
    }
185
    }
180
    $template->{VARS}->{'total'}        = $total;
186
    $template->{VARS}->{'total'}        = $total;
181
    $template->{VARS}->{'uploadimage'}  = 1;
187
    $template->{VARS}->{'uploadimage'}  = 1;
(-)a/tools/upload-file.pl (-15 / +1 lines)
Lines 48-54 if( $auth_status ne 'ok' || !$allowed ) { Link Here
48
    exit 0;
48
    exit 0;
49
}
49
}
50
50
51
my $upload = Koha::Uploader->new( upload_pars($ENV{QUERY_STRING}) );
51
my $upload = Koha::Uploader->new( { CGI->new($ENV{QUERY_STRING})->Vars } );
52
if( !$upload || !$upload->cgi || !$upload->count ) {
52
if( !$upload || !$upload->cgi || !$upload->count ) {
53
    # not one upload succeeded
53
    # not one upload succeeded
54
    send_reply( 'failed', undef, $upload? $upload->err: undef );
54
    send_reply( 'failed', undef, $upload? $upload->err: undef );
Lines 68-84 sub send_reply { # response will be sent back as JSON Link Here
68
        errors => $error,
68
        errors => $error,
69
   });
69
   });
70
}
70
}
71
72
sub upload_pars { # this sub parses QUERY_STRING in order to build the
73
                  # parameter hash for Koha::Uploader
74
    my ( $qstr ) = @_;
75
    $qstr = Encode::decode_utf8( uri_unescape( $qstr ) );
76
    # category could include a utf8 character
77
    my $rv = {};
78
    foreach my $p ( qw[public category temp] ) {
79
        if( $qstr =~ /(^|&)$p=(\w+)(&|$)/ ) {
80
            $rv->{$p} = $2;
81
        }
82
    }
83
    return $rv;
84
}
(-)a/tools/upload.pl (-5 / +12 lines)
Lines 22-28 use CGI qw/-utf8/; Link Here
22
use JSON;
22
use JSON;
23
23
24
use C4::Auth;
24
use C4::Auth;
25
use C4::Context;
25
use C4::Output;
26
use C4::Output;
27
use Koha::Storage;
26
use Koha::UploadedFiles;
28
use Koha::UploadedFiles;
27
29
28
use constant ERR_READING     => 'UPLERR_FILE_NOT_READ';
30
use constant ERR_READING     => 'UPLERR_FILE_NOT_READ';
Lines 46-56 my ( $template, $loggedinuser, $cookie ) = get_template_and_user( Link Here
46
    }
48
    }
47
);
49
);
48
50
51
my @storages;
52
foreach my $config (@{ Koha::Storage->config }) {
53
    my $storage = Koha::Storage->get_instance($config->{name});
54
    push @storages, $storage if $storage;
55
}
56
49
$template->param(
57
$template->param(
50
    index      => $index,
58
    index      => $index,
51
    owner      => $loggedinuser,
59
    owner      => $loggedinuser,
52
    plugin     => $plugin,
60
    plugin     => $plugin,
53
    uploadcategories => Koha::UploadedFiles->getCategories,
61
    storages   => \@storages,
54
);
62
);
55
63
56
if ( $op eq 'new' ) {
64
if ( $op eq 'new' ) {
Lines 65-79 if ( $op eq 'new' ) { Link Here
65
        my @id = split /,/, $id;
73
        my @id = split /,/, $id;
66
        foreach my $recid (@id) {
74
        foreach my $recid (@id) {
67
            my $rec = Koha::UploadedFiles->find( $recid );
75
            my $rec = Koha::UploadedFiles->find( $recid );
68
            push @$uploads, $rec->unblessed
76
            push @$uploads, $rec
69
                if $rec && ( $rec->public || !$plugin );
77
                if $rec && ( $rec->public || !$plugin );
70
                # Do not show private uploads in the plugin mode (:editor)
78
                # Do not show private uploads in the plugin mode (:editor)
71
        }
79
        }
72
    } else {
80
    } else {
73
        $uploads = Koha::UploadedFiles->search_term({
81
        $uploads = [ Koha::UploadedFiles->search_term({
74
            term => $term,
82
            term => $term,
75
            $plugin? (): ( include_private => 1 ),
83
            $plugin? (): ( include_private => 1 ),
76
        })->unblessed;
84
        }) ];
77
    }
85
    }
78
86
79
    $template->param(
87
    $template->param(
80
- 

Return to bug 19318