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
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.
24 Koha::Upload - Facilitate file uploads (temporary and permanent)
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 );
42 my $rec = Koha::Upload->new->get({ id => $id, filehandle => 1 });
44 my @hdr = Koha::Upload->httpheaders( $rec->{name} );
45 print Encode::encode_utf8( $input->header( @hdr ) );
46 while( <$fh> ) { print $_; }
50 my ( $fn ) = Koha::Upload->new->delete({ id => $id });
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.
63 use constant KOHA_UPLOAD
=> 'koha_upload';
64 use constant BYTES_DIGEST
=> 2048;
67 use CGI
; # no utf8 flag, since it may interfere with binary uploads
74 use base
qw(Class::Accessor);
79 __PACKAGE__
->mk_ro_accessors( qw
|| );
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.
91 my ( $class, $params ) = @_;
92 my $self = $class->SUPER::new
();
93 $self->_init( $params );
99 Returns CGI object. The CGI hook is used to store the uploaded files.
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 );
119 Returns number of uploaded files without errors
125 return scalar grep { !exists $self->{files
}->{$_}->{errcode
} } keys %{ $self->{files
} };
130 Returns a string of id's for each successful upload separated by commas.
136 my @a = map { $self->{files
}->{$_}->{id
} }
137 grep { !exists $self->{files
}->{$_}->{errcode
} }
138 keys %{ $self->{files
} };
139 return @a?
( join ',', @a ): undef;
144 Returns hash with errors in format { file => err, ... }
145 Undefined if there are no errors.
152 foreach my $f ( keys %{ $self->{files
} } ) {
153 my $e = $self->{files
}->{$f}->{errcode
};
154 $err->{ $f } = $e if $e;
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.
168 my ( $self, $params ) = @_;
169 my $temp= $self->_lookup( $params );
171 foreach my $r ( @
$temp ) {
173 foreach( qw
[id hashvalue filesize uploadcategorycode public permanent
] ) {
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" );
186 $self->{files
}->{ $r->{filename
} }->{errcode
}=5; #not readable
190 return wantarray?
@rv: $res;
195 Returns array of deleted filenames or undef.
196 Since it now only accepts id as parameter, you should not expect more
202 my ( $self, $params ) = @_;
203 return if !$params->{id
};
205 my $temp = $self->_lookup({ id
=> $params->{id
} });
207 my $d = $self->_delete( $_ );
218 getCategories returns a list of upload category codes and names
224 my $cats = C4
::Koha
::GetAuthorisedValues
('UPLOAD');
225 [ map {{ code
=> $_->{authorised_value
}, name
=> $_->{lib
} }} @
$cats ];
230 httpheaders returns http headers for a retrievable upload
231 Will be extended by report 14282
236 my ( $class, $name ) = @_;
238 '-type' => 'application/octet-stream',
239 '-attachment' => $name,
243 =head1 INTERNAL ROUTINES
248 my ( $self, $params ) = @_;
250 $self->{rootdir
} = C4
::Context
->config('upload_path');
251 $self->{tmpdir
} = File
::Spec
->tmpdir;
253 $params->{tmp
} = $params->{temp
} if !exists $params->{tmp
};
254 $self->{temporary
} = $params->{tmp
}?
1: 0; #default false
255 if( $params->{tmp
} ) {
256 my $db = C4
::Context
->config('database');
257 $self->{category
} = KOHA_UPLOAD
;
258 $self->{category
} =~ s/koha/$db/;
260 $self->{category
} = $params->{category
} || KOHA_UPLOAD
;
264 $self->{uid
} = C4
::Context
->userenv->{number
} if C4
::Context
->userenv;
265 $self->{public
} = $params->{public
}?
1: undef;
269 my ( $self, $filename ) = @_;
270 if( $self->{files
}->{$filename} ) {
271 return $self->{files
}->{$filename}->{fh
};
276 my ( $self, $filename ) = @_;
278 if( $self->{files
}->{$filename} &&
279 $self->{files
}->{$filename}->{errcode
} ) {
281 } elsif( !$self->{temporary
} && !$self->{rootdir
} ) {
282 $self->{files
}->{$filename}->{errcode
} = 3; #no rootdir
283 } elsif( $self->{temporary
} && !$self->{tmpdir
} ) {
284 $self->{files
}->{$filename}->{errcode
} = 4; #no tempdir
286 my $dir = $self->_dir;
287 my $fn = $self->{files
}->{$filename}->{hash
}. '_'. $filename;
288 if( -e
"$dir/$fn" && @
{ $self->_lookup({
289 hashvalue
=> $self->{files
}->{$filename}->{hash
} }) } ) {
290 # if the file exists and it is registered, then set error
291 $self->{files
}->{$filename}->{errcode
} = 1; #already exists
294 $fh = IO
::File
->new( "$dir/$fn", "w");
297 $self->{files
}->{$filename}->{fh
}= $fh;
299 $self->{files
}->{$filename}->{errcode
} = 2; #not writable
307 my $dir = $self->{temporary
}?
$self->{tmpdir
}: $self->{rootdir
};
308 $dir.= '/'. $self->{category
};
309 mkdir $dir if !-d
$dir;
314 my ( $self, $rec ) = @_;
317 $p = File
::Spec
->catfile(
318 $rec->{permanent
}?
$self->{rootdir
}: $self->{tmpdir
},
320 $rec->{hashvalue
}. '_'. $rec->{filename
}
327 my ( $self, $filename, $buffer, $bytes_read, $data ) = @_;
328 $filename= Encode
::decode_utf8
( $filename ); # UTF8 chars in filename
329 $self->_compute( $filename, $buffer );
330 my $fh = $self->_fh( $filename ) // $self->_create_file( $filename );
331 print $fh $buffer if $fh;
337 foreach my $f ( keys %{ $self->{files
} } ) {
338 my $fh = $self->_fh($f);
339 $self->_register( $f, $fh?
tell( $fh ): undef )
340 if !$self->{files
}->{$f}->{errcode
};
346 my ( $self, $filename, $size ) = @_;
347 my $dbh= C4
::Context
->dbh;
348 my $sql= 'INSERT INTO uploaded_files (hashvalue, filename, dir, filesize,
349 owner, uploadcategorycode, public, permanent) VALUES (?,?,?,?,?,?,?,?)';
351 $self->{files
}->{$filename}->{hash
},
358 $self->{temporary
}?
0: 1,
360 $dbh->do( $sql, undef, @pars );
361 my $i = $dbh->last_insert_id(undef, undef, 'uploaded_files', undef);
362 $self->{files
}->{$filename}->{id
} = $i if $i;
366 my ( $self, $params ) = @_;
367 my $dbh = C4
::Context
->dbh;
369 SELECT id
,hashvalue
,filename
,dir
,filesize
,uploadcategorycode
,public
,permanent
373 if( $params->{id
} ) {
374 return [] if $params->{id
} !~ /^\d+(,\d+)*$/;
375 $sql.= 'WHERE id IN ('.$params->{id
}.')';
377 } elsif( $params->{hashvalue
} ) {
378 $sql.= 'WHERE hashvalue=?';
379 @pars = ( $params->{hashvalue
} );
380 } elsif( $params->{term
} ) {
381 $sql.= 'WHERE (filename LIKE ? OR hashvalue LIKE ?)';
382 @pars = ( '%'.$params->{term
}.'%', '%'.$params->{term
}.'%' );
386 $sql.= $self->{public
}?
' AND public=1': '';
387 $sql.= ' ORDER BY id';
388 my $temp= $dbh->selectall_arrayref( $sql, { Slice
=> {} }, @pars );
393 my ( $self, $rec ) = @_;
394 my $dbh = C4
::Context
->dbh;
395 my $sql = 'DELETE FROM uploaded_files WHERE id=?';
396 my $file = $self->_full_fname($rec);
397 if( !-e
$file ) { # we will just delete the record
398 # TODO Should we add a trace here for the missing file?
399 $dbh->do( $sql, undef, ( $rec->{id
} ) );
400 return $rec->{filename
};
401 } elsif( unlink($file) ) {
402 $dbh->do( $sql, undef, ( $rec->{id
} ) );
403 return $rec->{filename
};
405 $self->{files
}->{ $rec->{filename
} }->{errcode
} = 7;
406 #NOTE: errcode=6 is used to report successful delete (see template)
411 # Computes hash value when sub hook feeds the first block
412 # For temporary files, the id is made unique with time
413 my ( $self, $name, $block ) = @_;
414 if( !$self->{files
}->{$name}->{hash
} ) {
415 my $str = $name. ( $self->{uid
} // '0' ).
416 ( $self->{temporary
}? Time
::HiRes
::time(): '' ).
417 $self->{category
}. substr( $block, 0, BYTES_DIGEST
);
418 # since Digest cannot handle wide chars, we need to encode here
419 # there could be a wide char in the filename or the category
420 my $h = Digest
::MD5
::md5_hex
( Encode
::encode_utf8
( $str ) );
421 $self->{files
}->{$name}->{hash
} = $h;
427 Koha Development Team
428 Larger parts from Galen Charlton, Julian Maurice and Marcel de Rooy