Bug 17196: Fix build_oai_sets.pl
[koha.git] / Koha / Upload.pm
blobceff56aa33945c80f07507f058a427929b294cf0
1 package Koha::Upload;
3 # Copyright 2007 LibLime, Galen Charlton
4 # Copyright 2011-2012 BibLibre
5 # Copyright 2015 Rijksmuseum
7 # This file is part of Koha.
9 # Koha is free software; you can redistribute it and/or modify it under the
10 # terms of the GNU General Public License as published by the Free Software
11 # Foundation; either version 3 of the License, or (at your option) any later
12 # version.
14 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License along
19 # with Koha; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 =head1 NAME
24 Koha::Upload - Facilitate file uploads (temporary and permanent)
26 =head1 SYNOPSIS
28 use Koha::Upload;
30 # add an upload (see tools/upload-file.pl)
31 # the public flag allows retrieval via OPAC
32 my $upload = Koha::Upload->new( public => 1, category => 'A' );
33 my $cgi = $upload->cgi;
34 # Do something with $upload->count, $upload->result or $upload->err
36 # get some upload records (in staff)
37 # Note: use the public flag for OPAC
38 my @uploads = Koha::Upload->new->get( term => $term );
39 $template->param( uploads => \@uploads );
41 # staff download
42 my $rec = Koha::Upload->new->get({ id => $id, filehandle => 1 });
43 my $fh = $rec->{fh};
44 my @hdr = Koha::Upload->httpheaders( $rec->{name} );
45 print Encode::encode_utf8( $input->header( @hdr ) );
46 while( <$fh> ) { print $_; }
47 $fh->close;
49 # delete an upload
50 my ( $fn ) = Koha::Upload->new->delete({ id => $id });
52 =head1 DESCRIPTION
54 This module is a refactored version of C4::UploadedFile but adds on top
55 of that the new functions from report 6874 (Upload plugin in editor).
56 That report added module UploadedFiles.pm. This module contains the
57 functionality of both.
59 =head1 METHODS
61 =cut
63 use constant KOHA_UPLOAD => 'koha_upload';
64 use constant BYTES_DIGEST => 2048;
66 use Modern::Perl;
67 use CGI; # no utf8 flag, since it may interfere with binary uploads
68 use Digest::MD5;
69 use Encode;
70 use File::Spec;
71 use IO::File;
72 use Time::HiRes;
74 use base qw(Class::Accessor);
76 use C4::Context;
77 use C4::Koha;
79 __PACKAGE__->mk_ro_accessors( qw|| );
81 =head2 new
83 Returns new object based on Class::Accessor.
84 Use tmp or temp flag for temporary storage.
85 Use public flag to mark uploads as available in OPAC.
86 The category parameter is only useful for permanent storage.
88 =cut
90 sub new {
91 my ( $class, $params ) = @_;
92 my $self = $class->SUPER::new();
93 $self->_init( $params );
94 return $self;
97 =head2 cgi
99 Returns CGI object. The CGI hook is used to store the uploaded files.
101 =cut
103 sub cgi {
104 my ( $self ) = @_;
106 # Next call handles the actual upload via CGI hook.
107 # The third parameter (0) below means: no CGI temporary storage.
108 # Cancelling an upload will make CGI abort the script; no problem,
109 # the file(s) without db entry will be removed later.
110 my $query = CGI::->new( sub { $self->_hook(@_); }, {}, 0 );
111 if( $query ) {
112 $self->_done;
113 return $query;
117 =head2 count
119 Returns number of uploaded files without errors
121 =cut
123 sub count {
124 my ( $self ) = @_;
125 return scalar grep { !exists $self->{files}->{$_}->{errcode} } keys %{ $self->{files} };
128 =head2 result
130 Returns a string of id's for each successful upload separated by commas.
132 =cut
134 sub result {
135 my ( $self ) = @_;
136 my @a = map { $self->{files}->{$_}->{id} }
137 grep { !exists $self->{files}->{$_}->{errcode} }
138 keys %{ $self->{files} };
139 return @a? ( join ',', @a ): undef;
142 =head2 err
144 Returns hash with errors in format { file => err, ... }
145 Undefined if there are no errors.
147 =cut
149 sub err {
150 my ( $self ) = @_;
151 my $err;
152 foreach my $f ( keys %{ $self->{files} } ) {
153 my $e = $self->{files}->{$f}->{errcode};
154 $err->{ $f } = $e if $e;
156 return $err;
159 =head2 get
161 Returns arrayref of uploaded records (hash) or one uploaded record.
162 You can pass id => $id or hashvalue => $hash or term => $term.
163 Optional parameter filehandle => 1 returns you a filehandle too.
165 =cut
167 sub get {
168 my ( $self, $params ) = @_;
169 my $temp= $self->_lookup( $params );
170 my ( @rv, $res);
171 foreach my $r ( @$temp ) {
172 undef $res;
173 foreach( qw[id hashvalue filesize uploadcategorycode public permanent owner] ) {
174 $res->{$_} = $r->{$_};
176 $res->{name} = $r->{filename};
177 $res->{path} = $self->_full_fname($r);
178 if( $res->{path} && -r $res->{path} ) {
179 if( $params->{filehandle} ) {
180 my $fh = IO::File->new( $res->{path}, "r" );
181 $fh->binmode if $fh;
182 $res->{fh} = $fh;
184 push @rv, $res;
185 } else {
186 $self->{files}->{ $r->{filename} }->{errcode}=5; #not readable
188 last if !wantarray;
190 return wantarray? @rv: $res;
193 =head2 delete
195 Returns array of deleted filenames or undef.
196 Since it now only accepts id as parameter, you should not expect more
197 than one filename.
199 =cut
201 sub delete {
202 my ( $self, $params ) = @_;
203 return if !$params->{id};
204 my @res;
205 my $temp = $self->_lookup({ id => $params->{id} });
206 foreach( @$temp ) {
207 my $d = $self->_delete( $_ );
208 push @res, $d if $d;
210 return if !@res;
211 return @res;
214 =head1 CLASS METHODS
216 =head2 getCategories
218 getCategories returns a list of upload category codes and names
220 =cut
222 sub getCategories {
223 my ( $class ) = @_;
224 my $cats = C4::Koha::GetAuthorisedValues('UPLOAD');
225 [ map {{ code => $_->{authorised_value}, name => $_->{lib} }} @$cats ];
228 =head2 httpheaders
230 httpheaders returns http headers for a retrievable upload
231 Will be extended by report 14282
233 =cut
235 sub httpheaders {
236 my ( $class, $name ) = @_;
237 return (
238 '-type' => 'application/octet-stream',
239 '-attachment' => $name,
243 =head2 allows_add_by
245 allows_add_by checks if $userid has permission to add uploaded files
247 =cut
249 sub allows_add_by {
250 my ( $class, $userid ) = @_; # do not confuse with borrowernumber
251 my $flags = [
252 { tools => 'upload_general_files' },
253 { circulate => 'circulate_remaining_permissions' },
254 { tools => 'stage_marc_import' },
255 { tools => 'upload_local_cover_images' },
257 require C4::Auth;
258 foreach( @$flags ) {
259 return 1 if C4::Auth::haspermission( $userid, $_ );
261 return;
264 =head1 INTERNAL ROUTINES
266 =cut
268 sub _init {
269 my ( $self, $params ) = @_;
271 $self->{rootdir} = C4::Context->config('upload_path');
272 $self->{tmpdir} = File::Spec->tmpdir;
274 $params->{tmp} = $params->{temp} if !exists $params->{tmp};
275 $self->{temporary} = $params->{tmp}? 1: 0; #default false
276 if( $params->{tmp} ) {
277 my $db = C4::Context->config('database');
278 $self->{category} = KOHA_UPLOAD;
279 $self->{category} =~ s/koha/$db/;
280 } else {
281 $self->{category} = $params->{category} || KOHA_UPLOAD;
284 $self->{files} = {};
285 $self->{uid} = C4::Context->userenv->{number} if C4::Context->userenv;
286 $self->{public} = $params->{public}? 1: undef;
289 sub _fh {
290 my ( $self, $filename ) = @_;
291 if( $self->{files}->{$filename} ) {
292 return $self->{files}->{$filename}->{fh};
296 sub _create_file {
297 my ( $self, $filename ) = @_;
298 my $fh;
299 if( $self->{files}->{$filename} &&
300 $self->{files}->{$filename}->{errcode} ) {
301 #skip
302 } elsif( !$self->{temporary} && !$self->{rootdir} ) {
303 $self->{files}->{$filename}->{errcode} = 3; #no rootdir
304 } elsif( $self->{temporary} && !$self->{tmpdir} ) {
305 $self->{files}->{$filename}->{errcode} = 4; #no tempdir
306 } else {
307 my $dir = $self->_dir;
308 my $fn = $self->{files}->{$filename}->{hash}. '_'. $filename;
309 if( -e "$dir/$fn" && @{ $self->_lookup({
310 hashvalue => $self->{files}->{$filename}->{hash} }) } ) {
311 # if the file exists and it is registered, then set error
312 $self->{files}->{$filename}->{errcode} = 1; #already exists
313 return;
315 $fh = IO::File->new( "$dir/$fn", "w");
316 if( $fh ) {
317 $fh->binmode;
318 $self->{files}->{$filename}->{fh}= $fh;
319 } else {
320 $self->{files}->{$filename}->{errcode} = 2; #not writable
323 return $fh;
326 sub _dir {
327 my ( $self ) = @_;
328 my $dir = $self->{temporary}? $self->{tmpdir}: $self->{rootdir};
329 $dir.= '/'. $self->{category};
330 mkdir $dir if !-d $dir;
331 return $dir;
334 sub _full_fname {
335 my ( $self, $rec ) = @_;
336 my $p;
337 if( ref $rec ) {
338 $p = File::Spec->catfile(
339 $rec->{permanent}? $self->{rootdir}: $self->{tmpdir},
340 $rec->{dir},
341 $rec->{hashvalue}. '_'. $rec->{filename}
344 return $p;
347 sub _hook {
348 my ( $self, $filename, $buffer, $bytes_read, $data ) = @_;
349 $filename= Encode::decode_utf8( $filename ); # UTF8 chars in filename
350 $self->_compute( $filename, $buffer );
351 my $fh = $self->_fh( $filename ) // $self->_create_file( $filename );
352 print $fh $buffer if $fh;
355 sub _done {
356 my ( $self ) = @_;
357 $self->{done} = 1;
358 foreach my $f ( keys %{ $self->{files} } ) {
359 my $fh = $self->_fh($f);
360 $self->_register( $f, $fh? tell( $fh ): undef )
361 if !$self->{files}->{$f}->{errcode};
362 $fh->close if $fh;
366 sub _register {
367 my ( $self, $filename, $size ) = @_;
368 my $dbh= C4::Context->dbh;
369 my $sql= 'INSERT INTO uploaded_files (hashvalue, filename, dir, filesize,
370 owner, uploadcategorycode, public, permanent) VALUES (?,?,?,?,?,?,?,?)';
371 my @pars= (
372 $self->{files}->{$filename}->{hash},
373 $filename,
374 $self->{category},
375 $size,
376 $self->{uid},
377 $self->{category},
378 $self->{public},
379 $self->{temporary}? 0: 1,
381 $dbh->do( $sql, undef, @pars );
382 my $i = $dbh->last_insert_id(undef, undef, 'uploaded_files', undef);
383 $self->{files}->{$filename}->{id} = $i if $i;
386 sub _lookup {
387 my ( $self, $params ) = @_;
388 my $dbh = C4::Context->dbh;
389 my $sql = q|
390 SELECT id,hashvalue,filename,dir,filesize,uploadcategorycode,public,permanent,owner
391 FROM uploaded_files
393 my @pars;
394 if( $params->{id} ) {
395 return [] if $params->{id} !~ /^\d+(,\d+)*$/;
396 $sql.= 'WHERE id IN ('.$params->{id}.')';
397 @pars = ();
398 } elsif( $params->{hashvalue} ) {
399 $sql.= 'WHERE hashvalue=?';
400 @pars = ( $params->{hashvalue} );
401 } elsif( $params->{term} ) {
402 $sql.= 'WHERE (filename LIKE ? OR hashvalue LIKE ?)';
403 @pars = ( '%'.$params->{term}.'%', '%'.$params->{term}.'%' );
404 } else {
405 return [];
407 $sql.= $self->{public}? ' AND public=1': '';
408 $sql.= ' ORDER BY id';
409 my $temp= $dbh->selectall_arrayref( $sql, { Slice => {} }, @pars );
410 return $temp;
413 sub _delete {
414 my ( $self, $rec ) = @_;
415 my $dbh = C4::Context->dbh;
416 my $sql = 'DELETE FROM uploaded_files WHERE id=?';
417 my $file = $self->_full_fname($rec);
418 if( !-e $file ) { # we will just delete the record
419 # TODO Should we add a trace here for the missing file?
420 $dbh->do( $sql, undef, ( $rec->{id} ) );
421 return $rec->{filename};
422 } elsif( unlink($file) ) {
423 $dbh->do( $sql, undef, ( $rec->{id} ) );
424 return $rec->{filename};
426 $self->{files}->{ $rec->{filename} }->{errcode} = 7;
427 #NOTE: errcode=6 is used to report successful delete (see template)
428 return;
431 sub _compute {
432 # Computes hash value when sub hook feeds the first block
433 # For temporary files, the id is made unique with time
434 my ( $self, $name, $block ) = @_;
435 if( !$self->{files}->{$name}->{hash} ) {
436 my $str = $name. ( $self->{uid} // '0' ).
437 ( $self->{temporary}? Time::HiRes::time(): '' ).
438 $self->{category}. substr( $block, 0, BYTES_DIGEST );
439 # since Digest cannot handle wide chars, we need to encode here
440 # there could be a wide char in the filename or the category
441 my $h = Digest::MD5::md5_hex( Encode::encode_utf8( $str ) );
442 $self->{files}->{$name}->{hash} = $h;
446 =head1 AUTHOR
448 Koha Development Team
449 Larger parts from Galen Charlton, Julian Maurice and Marcel de Rooy
451 =cut