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

(-)a/C4/ImportBatch.pm (-5 / +22 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 1510-1516 sub SetImportRecordMatches { Link Here
1510
1512
1511
Reads ISO2709 binary porridge from the given file and creates MARC::Record-objects out of it.
1513
Reads ISO2709 binary porridge from the given file and creates MARC::Record-objects out of it.
1512
1514
1513
@PARAM1, String, absolute path to the ISO2709 file.
1515
@PARAM1, String, absolute path to the ISO2709 file or an open filehandle.
1514
@PARAM2, String, see stage_file.pl
1516
@PARAM2, String, see stage_file.pl
1515
@PARAM3, String, should be utf8
1517
@PARAM3, String, should be utf8
1516
1518
Lines 1525-1531 sub RecordsFromISO2709File { Link Here
1525
    my $marc_type = C4::Context->preference('marcflavour');
1527
    my $marc_type = C4::Context->preference('marcflavour');
1526
    $marc_type .= 'AUTH' if ($marc_type eq 'UNIMARC' && $record_type eq 'auth');
1528
    $marc_type .= 'AUTH' if ($marc_type eq 'UNIMARC' && $record_type eq 'auth');
1527
1529
1528
    open my $fh, '<', $input_file or die "$0: cannot open input file $input_file: $!\n";
1530
    my $fh;
1531
    if (openhandle($input_file)) {
1532
        $fh = $input_file;
1533
    } else {
1534
        open $fh, '<', $input_file or die "$0: cannot open input file $input_file: $!\n";
1535
    }
1536
1529
    my @marc_records;
1537
    my @marc_records;
1530
    $/ = "\035";
1538
    $/ = "\035";
1531
    while (<$fh>) {
1539
    while (<$fh>) {
Lines 1541-1546 sub RecordsFromISO2709File { Link Here
1541
        }
1549
        }
1542
    }
1550
    }
1543
    close $fh;
1551
    close $fh;
1552
1544
    return ( \@errors, \@marc_records );
1553
    return ( \@errors, \@marc_records );
1545
}
1554
}
1546
1555
Lines 1550-1556 sub RecordsFromISO2709File { Link Here
1550
1559
1551
Creates MARC::Record-objects out of the given MARCXML-file.
1560
Creates MARC::Record-objects out of the given MARCXML-file.
1552
1561
1553
@PARAM1, String, absolute path to the ISO2709 file.
1562
@PARAM1, String, absolute path to the ISO2709 file or an open filehandle
1554
@PARAM2, String, should be utf8
1563
@PARAM2, String, should be utf8
1555
1564
1556
Returns two array refs.
1565
Returns two array refs.
Lines 1573-1579 sub RecordsFromMARCXMLFile { Link Here
1573
1582
1574
=head2 RecordsFromMarcPlugin
1583
=head2 RecordsFromMarcPlugin
1575
1584
1576
    Converts text of input_file into array of MARC records with to_marc plugin
1585
Converts text of C<$input_file> into array of MARC records with to_marc plugin
1586
1587
C<$input_file> can be either a filename or an open filehandle.
1577
1588
1578
=cut
1589
=cut
1579
1590
Lines 1583-1589 sub RecordsFromMarcPlugin { Link Here
1583
    return \@return if !$input_file || !$plugin_class;
1594
    return \@return if !$input_file || !$plugin_class;
1584
1595
1585
    # Read input file
1596
    # Read input file
1586
    open my $fh, '<', $input_file or die "$0: cannot open input file $input_file: $!\n";
1597
    my $fh;
1598
    if (openhandle($input_file)) {
1599
        $fh = $input_file;
1600
    } else {
1601
        open $fh, '<', $input_file or die "$0: cannot open input file $input_file: $!\n";
1602
    }
1603
1587
    $/ = "\035";
1604
    $/ = "\035";
1588
    while (<$fh>) {
1605
    while (<$fh>) {
1589
        s/^\s+//;
1606
        s/^\s+//;
(-)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
# along with Koha; if not, see <http://www.gnu.org/licenses>.
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
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-107 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
    return $retval if $params->{keep_file};
74
    return $retval if $params->{keep_file};
79
75
80
    if( ! -e $file ) {
76
    my $storage = Koha::Storage->get_instance($self->storage);
81
        warn "Removing record for $name within category ".
77
    my $filepath = $self->filepath;
82
            $self->uploadcategorycode. ", but file was missing.";
83
    } elsif( ! unlink($file) ) {
84
        warn "Problem while deleting: $file";
85
    }
86
    return $retval;
87
}
88
89
=head3 full_path
90
91
Returns the fully qualified path name for an uploaded file.
92
78
93
=cut
79
    if ( ! $storage->exists($filepath) ) {
80
        warn "Removing record for $name within storage " . $self->storage . ", but file was missing.";
81
    } elsif ( ! $storage->delete($filepath) ) {
82
        warn "Problem while deleting: $filepath";
83
    }
94
84
95
sub full_path {
85
    return $retval;
96
    my ( $self ) = @_;
97
    my $path = File::Spec->catfile(
98
        $self->permanent
99
            ? $self->permanent_directory
100
            : C4::Context->temporary_directory,
101
        $self->dir,
102
        $self->hashvalue. '_'. $self->filename,
103
    );
104
    return $path;
105
}
86
}
106
87
107
=head3 file_handle
88
=head3 file_handle
Lines 112-121 Returns a file handle for an uploaded file. Link Here
112
93
113
sub file_handle {
94
sub file_handle {
114
    my ( $self ) = @_;
95
    my ( $self ) = @_;
115
    $self->{_file_handle} = IO::File->new( $self->full_path, "r" );
96
116
    return if !$self->{_file_handle};
97
    my $storage = Koha::Storage->get_instance($self->storage);
117
    $self->{_file_handle}->binmode;
98
118
    return $self->{_file_handle};
99
    return $storage->fh($self->filepath, 'r');
119
}
100
}
120
101
121
=head3 httpheaders
102
=head3 httpheaders
Lines 141-159 sub httpheaders { Link Here
141
    }
122
    }
142
}
123
}
143
124
144
=head2 CLASS METHODS
125
=head3 url
145
126
146
=head3 permanent_directory
127
Returns the URL to access the file
147
128
148
Returns root directory for permanent storage
129
    my $url = $uploaded_file->url;
149
130
150
=cut
131
=cut
151
132
152
sub permanent_directory {
133
sub url {
153
    my ( $class ) = @_;
134
    my ($self) = @_;
154
    return C4::Context->config('upload_path');
135
136
    my $storage = Koha::Storage->get_instance($self->storage);
137
138
    return $storage->url($self->hashvalue, $self->filepath);
155
}
139
}
156
140
141
=head3 filepath
142
143
Returns the filepath of a file relative to the storage root path
144
145
    my $filepath = $uploaded_file->filepath;
146
147
=cut
148
149
sub filepath {
150
    my ($self) = @_;
151
152
    my $storage = Koha::Storage->get_instance($self->storage);
153
    my $filepath = $storage->filepath({
154
        hashvalue => $self->hashvalue,
155
        filename => $self->filename,
156
        dir => $self->dir,
157
    });
158
159
    return $filepath;
160
}
161
162
=head2 CLASS METHODS
163
157
=head3 _type
164
=head3 _type
158
165
159
Returns name of corresponding DBIC resultset
166
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 42-47 my $builder = sub { Link Here
42
            if( str && str.match(/id=([0-9a-f]+)/) ) {
42
            if( str && str.match(/id=([0-9a-f]+)/) ) {
43
                term = RegExp.\$1;
43
                term = RegExp.\$1;
44
                myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1';
44
                myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1';
45
            } else if (str && str.match(/\\/([^\\/]*)\$/)) {
46
                term = RegExp.\$1;
47
                myurl = '../tools/upload.pl?op=search&index='+index+'&term='+term+'&plugin=1';
45
            } else {
48
            } else {
46
                myurl = '../tools/upload.pl?op=new&index='+index+'&plugin=1';
49
                myurl = '../tools/upload.pl?op=new&index='+index+'&plugin=1';
47
            }
50
            }
(-)a/etc/koha-conf.xml (-2 / +9 lines)
Lines 79-86 Link Here
79
 <authorityservershadow>1</authorityservershadow>
79
 <authorityservershadow>1</authorityservershadow>
80
 <pluginsdir>__PLUGINS_DIR__</pluginsdir> <!-- This entry can be repeated to use multiple directories -->
80
 <pluginsdir>__PLUGINS_DIR__</pluginsdir> <!-- This entry can be repeated to use multiple directories -->
81
 <enable_plugins>0</enable_plugins>
81
 <enable_plugins>0</enable_plugins>
82
 <upload_path></upload_path>
82
 <storage>
83
 <tmp_path></tmp_path>
83
    <name>DEFAULT</name>
84
    <adapter>directory</adapter>
85
    <adapter_params>
86
        <!-- Set storage path to enable the default permanent storage -->
87
        <path></path>
88
    </adapter_params>
89
    <hash_filename>1</hash_filename>
90
 </storage>
84
 <intranetdir>__INTRANET_CGI_DIR__</intranetdir>
91
 <intranetdir>__INTRANET_CGI_DIR__</intranetdir>
85
 <opacdir>__OPAC_CGI_DIR__/opac</opacdir>
92
 <opacdir>__OPAC_CGI_DIR__/opac</opacdir>
86
 <opachtdocs>__OPAC_TMPL_DIR__</opachtdocs>
93
 <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 5129-5140 DROP TABLE IF EXISTS `uploaded_files`; Link Here
5129
/*!40101 SET character_set_client = utf8 */;
5129
/*!40101 SET character_set_client = utf8 */;
5130
CREATE TABLE `uploaded_files` (
5130
CREATE TABLE `uploaded_files` (
5131
  `id` int(11) NOT NULL AUTO_INCREMENT,
5131
  `id` int(11) NOT NULL AUTO_INCREMENT,
5132
  `storage` varchar(255) NOT NULL,
5132
  `hashvalue` char(40) COLLATE utf8mb4_unicode_ci NOT NULL,
5133
  `hashvalue` char(40) COLLATE utf8mb4_unicode_ci NOT NULL,
5133
  `filename` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
5134
  `filename` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
5134
  `dir` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
5135
  `dir` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
5135
  `filesize` int(11) DEFAULT NULL,
5136
  `filesize` int(11) DEFAULT NULL,
5136
  `dtcreated` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
5137
  `dtcreated` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
5137
  `uploadcategorycode` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
5138
  `owner` int(11) DEFAULT NULL,
5138
  `owner` int(11) DEFAULT NULL,
5139
  `public` tinyint(4) DEFAULT NULL,
5139
  `public` tinyint(4) DEFAULT NULL,
5140
  `permanent` tinyint(4) DEFAULT NULL,
5140
  `permanent` tinyint(4) DEFAULT NULL,
(-)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 408-414 Link Here
408
            $('#profile_fieldset').hide();
408
            $('#profile_fieldset').hide();
409
            $("#fileuploadstatus").show();
409
            $("#fileuploadstatus").show();
410
            $("#uploadedfileid").val('');
410
            $("#uploadedfileid").val('');
411
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'temp=1', cbUpload );
411
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'storage=TMP', cbUpload );
412
            $("#fileuploadcancel").show();
412
            $("#fileuploadcancel").show();
413
        }
413
        }
414
        function CancelUpload() {
414
        function CancelUpload() {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt (-1 / +1 lines)
Lines 177-183 Link Here
177
            $('#uploadform button.submit').prop('disabled',true);
177
            $('#uploadform button.submit').prop('disabled',true);
178
            $("#fileuploadstatus").show();
178
            $("#fileuploadstatus").show();
179
            $("#uploadedfileid").val('');
179
            $("#uploadedfileid").val('');
180
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'temp=1', cbUpload );
180
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), 'storage=TMP', cbUpload );
181
        }
181
        }
182
        function cbUpload( status, fileid, errors ) {
182
        function cbUpload( status, fileid, errors ) {
183
            if( status=='done' ) {
183
            if( status=='done' ) {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload.tt (-49 / +67 lines)
Lines 2-7 Link Here
2
[% USE Asset %]
2
[% USE Asset %]
3
[% USE Koha %]
3
[% USE Koha %]
4
[% USE TablesSettings %]
4
[% USE TablesSettings %]
5
[% USE JSON.Escape %]
5
[% SET footerjs = 1 %]
6
[% SET footerjs = 1 %]
6
[% INCLUDE 'doc-head-open.inc' %]
7
[% INCLUDE 'doc-head-open.inc' %]
7
[% IF plugin %]
8
[% IF plugin %]
Lines 11-16 Link Here
11
[% END %]
12
[% END %]
12
[% INCLUDE 'doc-head-close.inc' %]
13
[% INCLUDE 'doc-head-close.inc' %]
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-82 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 %]
58
                [% FOREACH cat IN uploadcategories %]
59
                    <option value="[% cat.code | html %]">[% cat.name | html %]</option>
60
                [% END %]
61
                </select>
62
            </li>
63
        [% END %]
64
        [% IF !plugin %]
65
            <li>
66
            [% IF uploadcategories %]
67
                <div class="hint">Note: For temporary uploads do not select a category.</div>
68
            [% ELSE %]
69
                <div class="hint">
70
                    Note: No upload categories are defined.
71
                    [% IF ( CAN_user_parameters_manage_auth_values ) -%]
72
                        Add values to the <a href="/cgi-bin/koha/admin/authorised_values.pl?searchfield=UPLOAD">UPLOAD authorized value category</a> otherwise all uploads will be marked as temporary.
73
                    [% ELSE -%]
74
                        An administrator must add values to the UPLOAD authorized value category otherwise all uploads will be marked as temporary.
75
                    [% END %]
66
                    [% END %]
76
                </div>
67
                [% END %]
77
            [% END %]
68
            </select>
78
            </li>
69
        </li>
79
        [% END %]
70
        <li>
71
            <label for="dir">Directory: </label>
72
            <select id="dir" name="dir">
73
            </select>
74
        </li>
80
        <li>
75
        <li>
81
            [% IF plugin %]
76
            [% IF plugin %]
82
                <input type="hidden" id="public" name="public" value="1"/>
77
                <input type="hidden" id="public" name="public" value="1"/>
Lines 164-172 Link Here
164
        <th>Filename</th>
159
        <th>Filename</th>
165
        <th>Size</th>
160
        <th>Size</th>
166
        <th>Hashvalue</th>
161
        <th>Hashvalue</th>
167
        <th>Category</th>
162
        <th>Storage</th>
168
        [% IF !plugin %]<th>Public</th>[% END %]
163
        <th>Directory</th>
169
        [% IF !plugin %]<th>Temporary</th>[% END %]
164
        [% IF !plugin %]
165
            <th>Public</th>
166
            <th>Temporary</th>
167
        [% END %]
170
        <th class="NoSort noExport">Actions</th>
168
        <th class="NoSort noExport">Actions</th>
171
    </tr>
169
    </tr>
172
    </thead>
170
    </thead>
Lines 176-189 Link Here
176
        <td>[% record.filename | html %]</td>
174
        <td>[% record.filename | html %]</td>
177
        <td>[% record.filesize | html %]</td>
175
        <td>[% record.filesize | html %]</td>
178
        <td>[% record.hashvalue | html %]</td>
176
        <td>[% record.hashvalue | html %]</td>
179
        <td>[% record.uploadcategorycode | html %]</td>
177
        <td>[% PROCESS storage_label name=record.storage %]</td>
178
        <td>[% record.dir | html %]</td>
180
        [% IF !plugin %]
179
        [% IF !plugin %]
181
            <td>[% IF record.public %]Yes[% ELSE %]No[% END %]</td>
180
            <td>[% IF record.public %]Yes[% ELSE %]No[% END %]</td>
182
            <td>[% IF record.permanent %]No[% ELSE %]Yes[% END %]</td>
181
            <td>[% IF record.permanent %]No[% ELSE %]Yes[% END %]</td>
183
        [% END %]
182
        [% END %]
184
        <td class="actions">
183
        <td class="actions">
185
            [% IF plugin %]
184
            [% IF plugin %]
186
                <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>
187
            [% END %]
186
            [% END %]
188
            <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>
189
            [% IF record.owner == owner || CAN_user_tools_upload_manage %]
188
            [% IF record.owner == owner || CAN_user_tools_upload_manage %]
Lines 279-295 Link Here
279
            $("#searchfile").hide();
278
            $("#searchfile").hide();
280
            $("#lastbreadcrumb").text( _("Add a new upload") );
279
            $("#lastbreadcrumb").text( _("Add a new upload") );
281
280
282
            var cat, xtra='';
281
            var xtra = 'storage=' + $('#storage').val();
283
            if( $("#uploadcategory").val() )
282
            xtra = xtra + '&dir=' + $('#dir').val();
284
                cat = encodeURIComponent( $("#uploadcategory").val() );
285
            if( cat ) xtra= 'category=' + cat + '&';
286
            [% IF plugin %]
283
            [% IF plugin %]
287
                xtra = xtra + 'public=1&temp=0';
284
                xtra = xtra + '&public=1';
288
            [% ELSE %]
285
            [% ELSE %]
289
                if( !cat ) xtra = 'temp=1&';
286
                if ( $('#public').prop('checked') ) {
290
                if( $('#public').prop('checked') ) xtra = xtra + 'public=1';
287
                    xtra = xtra + '&public=1';
288
                }
291
            [% END %]
289
            [% END %]
292
            xhr= AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), xtra, cbUpload );
290
            xhr = AjaxUpload( $('#fileToUpload'), $('#fileuploadprogress'), xtra, cbUpload );
293
        }
291
        }
294
        function CancelUpload() {
292
        function CancelUpload() {
295
            if( xhr ) xhr.abort();
293
            if( xhr ) xhr.abort();
Lines 332-338 Link Here
332
            var rv;
330
            var rv;
333
            switch(code) {
331
            switch(code) {
334
                case 'UPLERR_ALREADY_EXISTS':
332
                case 'UPLERR_ALREADY_EXISTS':
335
                    rv = _("This file already exists (in this category).");
333
                    rv = _("This file already exists (in this storage).");
336
                    break;
334
                    break;
337
                case 'UPLERR_CANNOT_WRITE':
335
                case 'UPLERR_CANNOT_WRITE':
338
                    rv = _("File could not be created. Check permissions.");
336
                    rv = _("File could not be created. Check permissions.");
Lines 381-392 Link Here
381
                $(window.opener.document).find('#[% index | html %]').val( '' );
379
                $(window.opener.document).find('#[% index | html %]').val( '' );
382
            [% END %]
380
            [% END %]
383
        }
381
        }
384
        function Choose(hashval) {
382
        function Choose(url) {
385
            var res = '[% Koha.Preference('OPACBaseURL') | html %]';
386
            res = res.replace( /\/$/, '');
387
            res = res + '/cgi-bin/koha/opac-retrieve-file.pl?id=' + hashval;
388
            [% IF index %]
383
            [% IF index %]
389
                $(window.opener.document).find('#[% index | html %]').val( res );
384
                $(window.opener.document).find('#[% index | html %]').val( url );
390
            [% END %]
385
            [% END %]
391
            window.close();
386
            window.close();
392
        }
387
        }
Lines 412-419 Link Here
412
            });
407
            });
413
            $(".choose_entry").on("click",function(e){
408
            $(".choose_entry").on("click",function(e){
414
                e.preventDefault();
409
                e.preventDefault();
415
                var record_hashvalue = $(this).data("record-hashvalue");
410
                var record_url = $(this).data("record-url");
416
                Choose( record_hashvalue );
411
                Choose( record_url );
417
            });
412
            });
418
            $(".download_entry").on("click",function(e){
413
            $(".download_entry").on("click",function(e){
419
                e.preventDefault();
414
                e.preventDefault();
Lines 431-436 Link Here
431
            });
426
            });
432
        });
427
        });
433
    </script>
428
    </script>
429
    <script>
430
        [% FOREACH storage IN storages %]
431
            [% name = storage.name %]
432
            [% storage_directories.$name = storage.directories %]
433
        [% END %]
434
435
        $(document).ready(function () {
436
            let storage_directories = [% storage_directories.json %];
437
            $('#storage').on('change', function () {
438
                $('#dir').empty();
439
                $('#dir').append($('<option>').val('').html(_("(root)")));
440
                let name = $(this).val()
441
                if (name in storage_directories) {
442
                    storage_directories[name].forEach(function (dir) {
443
                        let option = $('<option>')
444
                            .val(dir)
445
                            .html(dir);
446
                        $('#dir').append(option);
447
                    })
448
                }
449
            }).change();
450
        });
451
    </script>
434
[% END %]
452
[% END %]
435
453
436
[% INCLUDE 'intranet-bottom.inc' %]
454
[% 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 183-189 my $batch3_results = $dbh->do('SELECT * FROM import_batches WHERE import_batch_i Link Here
183
is( $batch3_results, "0E0", "Batch 3 has been deleted");
183
is( $batch3_results, "0E0", "Batch 3 has been deleted");
184
184
185
subtest "RecordsFromMarcPlugin" => sub {
185
subtest "RecordsFromMarcPlugin" => sub {
186
    plan tests => 5;
186
    plan tests => 6;
187
187
188
    # Create a test file
188
    # Create a test file
189
    my ( $fh, $name ) = tempfile();
189
    my ( $fh, $name ) = tempfile();
Lines 211-216 subtest "RecordsFromMarcPlugin" => sub { Link Here
211
        'Checked one field in first record' );
211
        'Checked one field in first record' );
212
    is( $records->[1]->subfield('100', 'a'), 'Another',
212
    is( $records->[1]->subfield('100', 'a'), 'Another',
213
        'Checked one field in second record' );
213
        'Checked one field in second record' );
214
215
    open my $fh2, '<', $name;
216
    $records = C4::ImportBatch::RecordsFromMarcPlugin( $fh2, ref $plugin, 'UTF-8' );
217
    close $fh2;
218
    is( @$records, 2, 'Can take a filehandle as parameter' );
214
};
219
};
215
220
216
$schema->storage->txn_rollback;
221
$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
use Try::Tiny;
7
use Try::Tiny;
8
8
Lines 24-57 our $builder = t::lib::TestBuilder->new; Link Here
24
our $current_upload = 0;
24
our $current_upload = 0;
25
our $uploads = [
25
our $uploads = [
26
    [
26
    [
27
        { name => 'file1', cat => 'A', size => 6000 },
27
        { name => 'file1', storage => 'DEFAULT', dir => 'A', size => 6000 },
28
        { name => 'file2', cat => 'A', size => 8000 },
28
        { name => 'file2', storage => 'DEFAULT', dir => 'A', size => 8000 },
29
    ],
29
    ],
30
    [
30
    [
31
        { name => 'file3', cat => 'B', size => 1000 },
31
        { name => 'file3', storage => 'DEFAULT', dir => 'B', size => 1000 },
32
    ],
32
    ],
33
    [
33
    [
34
        { name => 'file4', cat => undef, size => 5000 }, # temporary
34
        { name => 'file4', storage => 'TMP', dir => undef, size => 5000 },
35
    ],
35
    ],
36
    [
36
    [
37
        { name => 'file2', cat => 'A', size => 8000 },
37
        { name => 'file2', storage => 'DEFAULT', dir => 'A', size => 8000 },
38
        # uploading a duplicate in cat A should fail
38
        # uploading a duplicate in dir A should fail
39
    ],
39
    ],
40
    [
40
    [
41
        { name => 'file4', cat => undef, size => 5000 }, # temp duplicate
41
        { name => 'file4', storage => 'TMP', dir => undef, size => 5000 }, # temp duplicate
42
    ],
42
    ],
43
    [
43
    [
44
        { name => 'file5', cat => undef, size => 7000 },
44
        { name => 'file5', storage => 'DEFAULT', dir => undef, size => 7000 },
45
    ],
45
    ],
46
    [
46
    [
47
        { name => 'file6', cat => undef, size => 6500 },
47
        { name => 'file6', storage => 'TMP', dir => undef, size => 6500 },
48
        { name => 'file7', cat => undef, size => 6501 },
48
        { name => 'file7', storage => 'TMP', dir => undef, size => 6501 },
49
    ],
49
    ],
50
];
50
];
51
51
52
# Redirect upload dir structure and mock C4::Context and CGI
52
# Redirect upload dir structure and mock C4::Context and CGI
53
my $tempdir = tempdir( CLEANUP => 1 );
53
my $tempdir = tempdir( CLEANUP => 1 );
54
t::lib::Mocks::mock_config('upload_path', $tempdir);
54
my $storage_config = [
55
    {
56
        name => 'DEFAULT',
57
        adapter => 'directory',
58
        adapter_params => {
59
            path => $tempdir,
60
        },
61
        hash_filename => 1,
62
    },
63
];
64
t::lib::Mocks::mock_config('storage', $storage_config);
55
my $specmod = Test::MockModule->new( 'C4::Context' );
65
my $specmod = Test::MockModule->new( 'C4::Context' );
56
$specmod->mock( 'temporary_directory' => sub { return $tempdir; } );
66
$specmod->mock( 'temporary_directory' => sub { return $tempdir; } );
57
my $cgimod = Test::MockModule->new( 'CGI' );
67
my $cgimod = Test::MockModule->new( 'CGI' );
Lines 69-89 subtest 'Make a fresh start' => sub { Link Here
69
    is( Koha::UploadedFiles->count, 0, 'No records left' );
79
    is( Koha::UploadedFiles->count, 0, 'No records left' );
70
};
80
};
71
81
72
subtest 'permanent_directory and temporary_directory' => sub {
73
    plan tests => 2;
74
75
    # Check mocked directories
76
    is( Koha::UploadedFile->permanent_directory, $tempdir,
77
        'Check permanent directory' );
78
    is( C4::Context::temporary_directory, $tempdir,
79
        'Check temporary directory' );
80
};
81
82
subtest 'Add two uploads in category A' => sub {
82
subtest 'Add two uploads in category A' => sub {
83
    plan tests => 9;
83
    plan tests => 9;
84
84
85
    my $upl = Koha::Uploader->new({
85
    my $upl = Koha::Uploader->new({
86
        category => $uploads->[$current_upload]->[0]->{cat},
86
        storage => $uploads->[$current_upload]->[0]->{storage},
87
        dir => $uploads->[$current_upload]->[0]->{dir},
87
    });
88
    });
88
    my $cgi= $upl->cgi;
89
    my $cgi= $upl->cgi;
89
    my $res= $upl->result;
90
    my $res= $upl->result;
Lines 96-121 subtest 'Add two uploads in category A' => sub { Link Here
96
    }, { order_by => { -asc => 'filename' }});
97
    }, { order_by => { -asc => 'filename' }});
97
    my $rec = $rs->next;
98
    my $rec = $rs->next;
98
    is( $rec->filename, 'file1', 'Check file name' );
99
    is( $rec->filename, 'file1', 'Check file name' );
99
    is( $rec->uploadcategorycode, 'A', 'Check category A' );
100
    is( $rec->dir, 'A', 'Check dir A' );
100
    is( $rec->filesize, 6000, 'Check size of file1' );
101
    is( $rec->filesize, 6000, 'Check size of file1' );
101
    $rec = $rs->next;
102
    $rec = $rs->next;
102
    is( $rec->filename, 'file2', 'Check file name 2' );
103
    is( $rec->filename, 'file2', 'Check file name 2' );
103
    is( $rec->filesize, 8000, 'Check size of file2' );
104
    is( $rec->filesize, 8000, 'Check size of file2' );
104
    is( $rec->public, undef, 'Check public undefined' );
105
    is( $rec->public, 0, 'Check public 0' );
105
};
106
};
106
107
107
subtest 'Add another upload, check file_handle' => sub {
108
subtest 'Add another upload, check file_handle' => sub {
108
    plan tests => 5;
109
    plan tests => 5;
109
110
110
    my $upl = Koha::Uploader->new({
111
    my $upl = Koha::Uploader->new({
111
        category => $uploads->[$current_upload]->[0]->{cat},
112
        storage => $uploads->[$current_upload]->[0]->{storage},
113
        dir => $uploads->[$current_upload]->[0]->{dir},
112
        public => 1,
114
        public => 1,
113
    });
115
    });
114
    my $cgi= $upl->cgi;
116
    my $cgi= $upl->cgi;
115
    is( $upl->count, 1, 'Upload 2 includes one file' );
117
    is( $upl->count, 1, 'Upload 2 includes one file' );
116
    my $res= $upl->result;
118
    my $res= $upl->result;
117
    my $rec = Koha::UploadedFiles->find( $res );
119
    my $rec = Koha::UploadedFiles->find( $res );
118
    is( $rec->uploadcategorycode, 'B', 'Check category B' );
120
    is( $rec->dir, 'B', 'Check dir B' );
119
    is( $rec->public, 1, 'Check public == 1' );
121
    is( $rec->public, 1, 'Check public == 1' );
120
    my $fh = $rec->file_handle;
122
    my $fh = $rec->file_handle;
121
    is( ref($fh) eq 'IO::File' && $fh->opened, 1, 'Get returns a file handle' );
123
    is( ref($fh) eq 'IO::File' && $fh->opened, 1, 'Get returns a file handle' );
Lines 129-146 subtest 'Add another upload, check file_handle' => sub { Link Here
129
subtest 'Add temporary upload' => sub {
131
subtest 'Add temporary upload' => sub {
130
    plan tests => 2;
132
    plan tests => 2;
131
133
132
    my $upl = Koha::Uploader->new({ tmp => 1 }); #temporary
134
    my $upl = Koha::Uploader->new({
135
        storage => $uploads->[$current_upload]->[0]->{storage},
136
        dir => $uploads->[$current_upload]->[0]->{dir},
137
    });
133
    my $cgi= $upl->cgi;
138
    my $cgi= $upl->cgi;
134
    is( $upl->count, 1, 'Upload 3 includes one temporary file' );
139
    is( $upl->count, 1, 'Upload 3 includes one temporary file' );
135
    my $rec = Koha::UploadedFiles->find( $upl->result );
140
    my $rec = Koha::UploadedFiles->find( $upl->result );
136
    is( $rec->uploadcategorycode =~ /_upload$/, 1, 'Check category temp file' );
141
    is( $rec->dir, '', 'Check dir is empty' );
137
};
142
};
138
143
139
subtest 'Add same file in same category' => sub {
144
subtest 'Add same file in same category' => sub {
140
    plan tests => 3;
145
    plan tests => 3;
141
146
142
    my $upl = Koha::Uploader->new({
147
    my $upl = Koha::Uploader->new({
143
        category => $uploads->[$current_upload]->[0]->{cat},
148
        storage => $uploads->[$current_upload]->[0]->{storage},
149
        dir => $uploads->[$current_upload]->[0]->{dir},
144
    });
150
    });
145
    my $cgi= $upl->cgi;
151
    my $cgi= $upl->cgi;
146
    is( $upl->count, 0, 'Upload 4 failed as expected' );
152
    is( $upl->count, 0, 'Upload 4 failed as expected' );
Lines 153-183 subtest 'Test delete via UploadedFile as well as UploadedFiles' => sub { Link Here
153
    plan tests => 10;
159
    plan tests => 10;
154
160
155
    # add temporary file with same name and contents (file4)
161
    # add temporary file with same name and contents (file4)
156
    my $upl = Koha::Uploader->new({ tmp => 1 });
162
    my $upl = Koha::Uploader->new({
163
        storage => $uploads->[$current_upload]->[0]->{storage},
164
        dir => $uploads->[$current_upload]->[0]->{dir},
165
    });
157
    my $cgi= $upl->cgi;
166
    my $cgi= $upl->cgi;
158
    is( $upl->count, 1, 'Add duplicate temporary file (file4)' );
167
    is( $upl->count, 1, 'Add duplicate temporary file (file4)' );
159
    my $id = $upl->result;
168
    my $id = $upl->result;
160
    my $path = Koha::UploadedFiles->find( $id )->full_path;
169
    my $uploaded_file = Koha::UploadedFiles->find( $id );
170
    my $storage = Koha::Storage->get_instance($uploaded_file->storage);
161
171
162
    # testing delete via UploadedFiles (plural)
172
    # testing delete via UploadedFiles (plural)
163
    my $delete = Koha::UploadedFiles->search({ id => $id })->delete;
173
    my $delete = Koha::UploadedFiles->search({ id => $id })->delete;
164
    isnt( $delete, "0E0", 'Delete successful' );
174
    isnt( $delete, "0E0", 'Delete successful' );
165
    isnt( -e $path, 1, 'File no longer found after delete' );
175
    isnt( $storage->exists($uploaded_file->filepath), 1, 'File no longer found after delete' );
166
    is( Koha::UploadedFiles->find( $id ), undef, 'Record also gone' );
176
    is( Koha::UploadedFiles->find( $id ), undef, 'Record also gone' );
167
177
168
    # testing delete via UploadedFile (singular)
178
    # testing delete via UploadedFile (singular)
169
    # Note that find returns a Koha::Object
179
    # Note that find returns a Koha::Object
170
    $upl = Koha::Uploader->new({ tmp => 1 });
180
    $upl = Koha::Uploader->new({
181
        storage => $uploads->[$current_upload]->[0]->{storage},
182
        dir => $uploads->[$current_upload]->[0]->{dir},
183
    });
171
    $upl->cgi;
184
    $upl->cgi;
172
    my $kohaobj = Koha::UploadedFiles->find( $upl->result );
185
    my $kohaobj = Koha::UploadedFiles->find( $upl->result );
173
    $path = $kohaobj->full_path;
186
    $storage = Koha::Storage->get_instance($kohaobj->storage);
174
    $delete = $kohaobj->delete;
187
    $delete = $kohaobj->delete;
175
    ok( $delete, 'Delete successful' );
188
    ok( $delete, 'Delete successful' );
176
    isnt( -e $path, 1, 'File no longer found after delete' );
189
    isnt($storage->exists($kohaobj->filepath), 1, 'File no longer found after delete' );
177
190
178
    # add another record with TestBuilder, so file does not exist
191
    # add another record with TestBuilder, so file does not exist
179
    # catch warning
192
    # catch warning
180
    my $upload01 = $builder->build({ source => 'UploadedFile' });
193
    my $upload01 = $builder->build({ source => 'UploadedFile', value => {storage => 'TMP'} });
181
    warning_like { $delete = Koha::UploadedFiles->find( $upload01->{id} )->delete; }
194
    warning_like { $delete = Koha::UploadedFiles->find( $upload01->{id} )->delete; }
182
        qr/file was missing/,
195
        qr/file was missing/,
183
        'delete warns when file is missing';
196
        'delete warns when file is missing';
Lines 199-206 subtest 'Test delete_missing' => sub { Link Here
199
    plan tests => 5;
212
    plan tests => 5;
200
213
201
    # If we add files via TestBuilder, they do not exist
214
    # If we add files via TestBuilder, they do not exist
202
    my $upload01 = $builder->build({ source => 'UploadedFile' });
215
    my $upload01 = $builder->build({ source => 'UploadedFile', value => {storage => 'TMP'} });
203
    my $upload02 = $builder->build({ source => 'UploadedFile' });
216
    my $upload02 = $builder->build({ source => 'UploadedFile', value => {storage => 'TMP'} });
204
    # dry run first
217
    # dry run first
205
    my $deleted = Koha::UploadedFiles->delete_missing({ keep_record => 1 });
218
    my $deleted = Koha::UploadedFiles->delete_missing({ keep_record => 1 });
206
    is( $deleted, 2, 'Expect two records with missing files' );
219
    is( $deleted, 2, 'Expect two records with missing files' );
Lines 227-241 subtest 'Call search_term with[out] private flag' => sub { Link Here
227
    })->count, 4, 'Returns now four results' );
240
    })->count, 4, 'Returns now four results' );
228
};
241
};
229
242
230
subtest 'Simple tests for httpheaders and getCategories' => sub {
243
subtest 'Simple tests for httpheaders' => sub {
231
    plan tests => 2;
244
    plan tests => 1;
232
245
233
    my $rec = Koha::UploadedFiles->search_term({ term => 'file' })->next;
246
    my $rec = Koha::UploadedFiles->search_term({ term => 'file' })->next;
234
    my @hdrs = $rec->httpheaders;
247
    my @hdrs = $rec->httpheaders;
235
    is( @hdrs == 4 && $hdrs[1] =~ /application\/octet-stream/, 1, 'Simple test for httpheaders');
248
    is( @hdrs == 4 && $hdrs[1] =~ /application\/octet-stream/, 1, 'Simple test for httpheaders');
236
    $builder->build({ source => 'AuthorisedValue', value => { category => 'UPLOAD', authorised_value => 'HAVE_AT_LEAST_ONE', lib => 'Hi there' } });
249
    $builder->build({ source => 'AuthorisedValue', value => { category => 'UPLOAD', authorised_value => 'HAVE_AT_LEAST_ONE', lib => 'Hi there' } });
237
    my $cat = Koha::UploadedFiles->getCategories;
238
    is( @$cat >= 1, 1, 'getCategories returned at least one category' );
239
};
250
};
240
251
241
subtest 'Testing allows_add_by' => sub {
252
subtest 'Testing allows_add_by' => sub {
Lines 280-286 subtest 'Testing delete_temporary' => sub { Link Here
280
    plan tests => 9;
291
    plan tests => 9;
281
292
282
    # Add two temporary files: result should be 3 + 3
293
    # Add two temporary files: result should be 3 + 3
283
    Koha::Uploader->new({ tmp => 1 })->cgi; # add file6 and file7
294
    Koha::Uploader->new({
295
        storage => $uploads->[$current_upload]->[0]->{storage},
296
        dir => $uploads->[$current_upload]->[0]->{dir},
297
    })->cgi; # add file6 and file7
284
    is( Koha::UploadedFiles->search->count, 6, 'Test starting count' );
298
    is( Koha::UploadedFiles->search->count, 6, 'Test starting count' );
285
    is( Koha::UploadedFiles->search({ permanent => 1 })->count, 3,
299
    is( Koha::UploadedFiles->search({ permanent => 1 })->count, 3,
286
        'Includes 3 permanent' );
300
        'Includes 3 permanent' );
Lines 321-328 subtest 'Testing delete_temporary' => sub { Link Here
321
335
322
subtest 'Testing download headers' => sub {
336
subtest 'Testing download headers' => sub {
323
    plan tests => 2;
337
    plan tests => 2;
324
    my $test_pdf = Koha::UploadedFile->new({ filename => 'pdf.pdf', uploadcategorycode => 'B', filesize => 1000 });
338
    my $test_pdf = Koha::UploadedFile->new({ filename => 'pdf.pdf', storage => 'DEFAULT', dir => 'B', filesize => 1000 });
325
    my $test_not = Koha::UploadedFile->new({ filename => 'pdf.not', uploadcategorycode => 'B', filesize => 1000 });
339
    my $test_not = Koha::UploadedFile->new({ filename => 'pdf.not', storage => 'DEFAULT', dir => 'B', filesize => 1000 });
326
    my @pdf_expect = ( '-type'=>'application/pdf','Content-Disposition'=>'inline; filename=pdf.pdf' );
340
    my @pdf_expect = ( '-type'=>'application/pdf','Content-Disposition'=>'inline; filename=pdf.pdf' );
327
    my @not_expect = ( '-type'=>'application/octet-stream','-attachment'=>'pdf.not' );
341
    my @not_expect = ( '-type'=>'application/octet-stream','-attachment'=>'pdf.not' );
328
    my @pdf_head = $test_pdf->httpheaders;
342
    my @pdf_head = $test_pdf->httpheaders;
(-)a/tools/stage-marc-import.pl (-4 / +5 lines)
Lines 89-105 if ($completedJobID) { Link Here
89
    $template->param(map { $_ => $results->{$_} } keys %{ $results });
89
    $template->param(map { $_ => $results->{$_} } keys %{ $results });
90
} elsif ($fileID) {
90
} elsif ($fileID) {
91
    my $upload = Koha::UploadedFiles->find( $fileID );
91
    my $upload = Koha::UploadedFiles->find( $fileID );
92
    my $file = $upload->full_path;
92
    my $storage = Koha::Storage->get_instance($upload->storage);
93
    my $fh = $storage->fh($upload->filepath, 'r');
93
    my $filename = $upload->filename;
94
    my $filename = $upload->filename;
94
95
95
    my ( $errors, $marcrecords );
96
    my ( $errors, $marcrecords );
96
    if( $format eq 'MARCXML' ) {
97
    if( $format eq 'MARCXML' ) {
97
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, $encoding);
98
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromMARCXMLFile( $fh, $encoding);
98
    } elsif( $format eq 'ISO2709' ) {
99
    } elsif( $format eq 'ISO2709' ) {
99
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromISO2709File( $file, $record_type, $encoding );
100
        ( $errors, $marcrecords ) = C4::ImportBatch::RecordsFromISO2709File( $fh, $record_type, $encoding );
100
    } else { # plugin based
101
    } else { # plugin based
101
        $errors = [];
102
        $errors = [];
102
        $marcrecords = C4::ImportBatch::RecordsFromMarcPlugin( $file, $format, $encoding );
103
        $marcrecords = C4::ImportBatch::RecordsFromMarcPlugin( $fh, $format, $encoding );
103
    }
104
    }
104
    warn "$filename: " . ( join ',', @$errors ) if @$errors;
105
    warn "$filename: " . ( join ',', @$errors ) if @$errors;
105
        # no need to exit if we have no records (or only errors) here
106
        # no need to exit if we have no records (or only errors) here
(-)a/tools/upload-cover-image.pl (-4 / +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 125-135 if ($fileID) { Link Here
125
        undef $srcimage;
126
        undef $srcimage;
126
    }
127
    }
127
    else {
128
    else {
128
        my $filename = $upload->full_path;
129
        my $storage = Koha::Storage->get_instance($upload->storage);
130
        my $fh = $storage->fh($upload->filepath, 'r');
129
        my $dirname = File::Temp::tempdir( CLEANUP => 1 );
131
        my $dirname = File::Temp::tempdir( CLEANUP => 1 );
130
        qx/unzip $filename -d $dirname/;
132
        my $zip = Archive::Zip->new();
131
        my $exit_code = $?;
133
        $zip->readFromFileHandle($fh);
132
        unless ( $exit_code == 0 ) {
134
        unless (AZ_OK == $zip->extractTree(undef, $dirname)) {
133
            $error = 'UZIPFAIL';
135
            $error = 'UZIPFAIL';
134
        }
136
        }
135
        else {
137
        else {
Lines 169-174 if ($fileID) { Link Here
169
                            $error = 'DELERR';
171
                            $error = 'DELERR';
170
                        }
172
                        }
171
                        else {
173
                        else {
174
                            my $filename;
172
                            ( $biblionumber, $filename ) = split $delim, $line, 2;
175
                            ( $biblionumber, $filename ) = split $delim, $line, 2;
173
                            $biblionumber =~
176
                            $biblionumber =~
174
                              s/[\"\r\n]//g;    # remove offensive characters
177
                              s/[\"\r\n]//g;    # remove offensive characters
Lines 216-221 if ($fileID) { Link Here
216
                }
219
                }
217
            }
220
            }
218
        }
221
        }
222
        close $fh;
219
    }
223
    }
220
224
221
    $template->param(
225
    $template->param(
(-)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 45-55 my ( $template, $loggedinuser, $cookie ) = get_template_and_user( Link Here
45
    }
47
    }
46
);
48
);
47
49
50
my @storages;
51
foreach my $config (@{ Koha::Storage->config }) {
52
    my $storage = Koha::Storage->get_instance($config->{name});
53
    push @storages, $storage if $storage;
54
}
55
48
$template->param(
56
$template->param(
49
    index      => $index,
57
    index      => $index,
50
    owner      => $loggedinuser,
58
    owner      => $loggedinuser,
51
    plugin     => $plugin,
59
    plugin     => $plugin,
52
    uploadcategories => Koha::UploadedFiles->getCategories,
60
    storages   => \@storages,
53
);
61
);
54
62
55
if ( $op eq 'new' ) {
63
if ( $op eq 'new' ) {
Lines 64-78 if ( $op eq 'new' ) { Link Here
64
        my @id = split /,/, $id;
72
        my @id = split /,/, $id;
65
        foreach my $recid (@id) {
73
        foreach my $recid (@id) {
66
            my $rec = Koha::UploadedFiles->find( $recid );
74
            my $rec = Koha::UploadedFiles->find( $recid );
67
            push @$uploads, $rec->unblessed
75
            push @$uploads, $rec
68
                if $rec && ( $rec->public || !$plugin );
76
                if $rec && ( $rec->public || !$plugin );
69
                # Do not show private uploads in the plugin mode (:editor)
77
                # Do not show private uploads in the plugin mode (:editor)
70
        }
78
        }
71
    } else {
79
    } else {
72
        $uploads = Koha::UploadedFiles->search_term({
80
        $uploads = [ Koha::UploadedFiles->search_term({
73
            term => $term,
81
            term => $term,
74
            $plugin? (): ( include_private => 1 ),
82
            $plugin? (): ( include_private => 1 ),
75
        })->unblessed;
83
        }) ];
76
    }
84
    }
77
85
78
    $template->param(
86
    $template->param(
79
- 

Return to bug 19318