X-Git-Url: http://wagnertech.de/git?p=kivitendo-erp.git;a=blobdiff_plain;f=SL%2FFile.pm;fp=SL%2FFile.pm;h=ca065e0c72fb36510a58162768a82b5ab17b5eda;hp=0000000000000000000000000000000000000000;hb=53593baa211863fbf66540cf1bcc36c8fb37257f;hpb=deb4d2dbb676d7d6f69dfe7815d6e0cb09bd4a44 diff --git a/SL/File.pm b/SL/File.pm new file mode 100644 index 000000000..ca065e0c7 --- /dev/null +++ b/SL/File.pm @@ -0,0 +1,735 @@ +package SL::File; + +use strict; + +use parent qw(Rose::Object); + +use SL::File::Backend; +use SL::File::Object; +use SL::DB::History; +use SL::DB::ShopImage; +use SL::DB::File; +use SL::Helper::UserPreferences; +use SL::Controller::Helper::ThumbnailCreator qw(file_probe_type); +use SL::JSON; + +use constant RENAME_OK => 0; +use constant RENAME_EXISTS => 1; +use constant RENAME_NOFILE => 2; +use constant RENAME_SAME => 3; +use constant RENAME_NEW_VERSION => 4; + +sub get { + my ($self, %params) = @_; + die "no id or dbfile" unless $params{id} || $params{dbfile}; + $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile}; + die 'not found' unless $params{dbfile}; + $main::lxdebug->message(LXDebug->DEBUG2(), "object_id=".$params{dbfile}->object_id." object_type=".$params{dbfile}->object_type." dbfile=".$params{dbfile}); + SL::File::Object->new(db_file => $params{dbfile}, id => $params{dbfile}->id, loaded => 1); +} + +sub get_version_count { + my ($self, %params) = @_; + die "no id or dbfile" unless $params{id} || $params{dbfile}; + $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile}; + die 'not found' unless $params{dbfile}; + my $backend = $self->_get_backend($params{dbfile}->backend); + return $backend->get_version_count(%params); +} + +sub get_all { + my ($self, %params) = @_; + + my @files; + return @files unless $params{object_type}; + return @files unless defined($params{object_id}); + + my @query = ( + object_id => $params{object_id}, + object_type => $params{object_type} + ); + push @query, (file_name => $params{file_name}) if $params{file_name}; + push @query, (file_type => $params{file_type}) if $params{file_type}; + push @query, (mime_type => $params{mime_type}) if $params{mime_type}; + push @query, (source => $params{source}) if $params{source}; + push @query, (print_variant => $params{print_variant}) if $params{print_variant}; + + my $sortby = $params{sort_by} || 'itime DESC,file_name ASC'; + + @files = @{ SL::DB::Manager::File->get_all(query => [@query], sort_by => $sortby) }; + map { SL::File::Object->new(db_file => $_, id => $_->id, loaded => 1) } @files; +} + +sub get_all_versions { + my ($self, %params) = @_; + my @versionobjs; + my @fileobjs; + if ( $params{dbfile} ) { + push @fileobjs, SL::File::Object->new(db_file => $params{dbfile}, id => $params{dbfile}->id, loaded => 1); + } else { + @fileobjs = $self->get_all(%params); + } + foreach my $fileobj (@fileobjs) { + $main::lxdebug->message(LXDebug->DEBUG2(), "obj=" . $fileobj . " id=" . $fileobj->id." versions=".$fileobj->version_count); + my $maxversion = $fileobj->version_count; + $fileobj->version($maxversion); + push @versionobjs, $fileobj; + if ($maxversion > 1) { + for my $version (2..$maxversion) { + $main::lxdebug->message(LXDebug->DEBUG2(), "clone for version=".($maxversion-$version+1)); + eval { + my $clone = $fileobj->clone; + $clone->version($maxversion-$version+1); + $clone->newest(0); + $main::lxdebug->message(LXDebug->DEBUG2(), "clone version=".$clone->version." mtime=". $clone->mtime); + push @versionobjs, $clone; + 1; + } or do {$::lxdebug->message(LXDebug::WARN(), "clone for version=".($maxversion-$version+1) . "failed: " . $@)}; + } + } + } + return @versionobjs; +} + +sub get_all_count { + my ($self, %params) = @_; + return 0 unless $params{object_type}; + + my @query = ( + object_id => $params{object_id}, + object_type => $params{object_type} + ); + push @query, (file_name => $params{file_name}) if $params{file_name}; + push @query, (file_type => $params{file_type}) if $params{file_type}; + push @query, (mime_type => $params{mime_type}) if $params{mime_type}; + push @query, (source => $params{source}) if $params{source}; + push @query, (print_variant => $params{print_variant}) if $params{print_variant}; + + my $cnt = SL::DB::Manager::File->get_all_count(query => [@query]); + return $cnt; +} + +sub delete_all { + my ($self, %params) = @_; + return 0 unless defined($params{object_id}) || $params{object_type}; + my $files = SL::DB::Manager::File->get_all( + query => [ + object_id => $params{object_id}, + object_type => $params{object_type} + ] + ); + foreach my $file (@{$files}) { + $params{dbfile} = $file; + $self->delete(%params); + } +} + +sub delete { + my ($self, %params) = @_; + die "no id or dbfile in delete" unless $params{id} || $params{dbfile}; + my $rc = 0; + eval { + $rc = SL::DB->client->with_transaction(\&_delete, $self, %params); + 1; + } or do { die $@ }; + return $rc; +} + +sub _delete { + my ($self, %params) = @_; + $params{dbfile} = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$params{dbfile}; + + my $backend = $self->_get_backend($params{dbfile}->backend); + if ( $params{dbfile}->file_type eq 'document' && $params{dbfile}->source ne 'created') + { + ## must unimport + my $hist = SL::DB::Manager::History->get_first( + where => [ + addition => 'IMPORT', + trans_id => $params{dbfile}->object_id, + what_done => $params{dbfile}->id + ] + ); + + if ($hist) { + if (!$main::auth->assert('import_ar | import_ap', 1)) { + die 'no permission to unimport'; + } + my $file = $backend->get_filepath(dbfile => $params{dbfile}); + $main::lxdebug->message(LXDebug->DEBUG2(), "del file=" . $file . " to=" . $hist->snumbers); + File::Copy::copy($file, $hist->snumbers) if $file; + $hist->addition('UNIMPORT'); + $hist->save; + } + } + if ($backend->delete(%params)) { + my $do_delete = 0; + if ( $params{last} || $params{version} || $params{all_but_notlast} ) { + if ( $backend->get_version_count(%params) > 0 ) { + $params{dbfile}->mtime(DateTime->now_local); + $params{dbfile}->save; + } else { + $do_delete = 1; + } + } else { + $do_delete = 1; + } + $params{dbfile}->delete if $do_delete; + return 1; + } + return 0; +} + +sub save { + my ($self, %params) = @_; + + my $obj; + eval { + $obj = SL::DB->client->with_transaction(\&_save, $self, %params); + 1; + } or do { die $@ }; + return $obj; +} + +sub _save { + my ($self, %params) = @_; + my $file = $params{dbfile}; + my $exists = 0; + + if ($params{id}) { + $file = SL::DB::File->new(id => $params{id})->load; + die 'dbfile not exists' unless $file; + } elsif (!$file) { + $main::lxdebug->message(LXDebug->DEBUG2(), "obj_id=" .$params{object_id}); + die 'no object type set' unless $params{object_type}; + die 'no object id set' unless defined($params{object_id}); + + $exists = $self->get_all_count(%params); + die 'filename still exist' if $exists && $params{fail_if_exists}; + if ($exists) { + my ($obj1) = $self->get_all(%params); + $file = $obj1->db_file; + } else { + $file = SL::DB::File->new(); + $file->assign_attributes( + object_id => $params{object_id}, + object_type => $params{object_type}, + source => $params{source}, + file_type => $params{file_type}, + file_name => $params{file_name}, + mime_type => $params{mime_type}, + title => $params{title}, + description => $params{description}, + print_variant => $params{print_variant}, + ); + $file->itime($params{mtime}) if $params{mtime}; + $params{itime} = $params{mtime} if $params{mtime}; + } + } else { + $exists = 1; + } + if ($exists) { + #change attr on existing file + $file->file_name ($params{file_name}) if $params{file_name}; + $file->mime_type ($params{mime_type}) if $params{mime_type}; + $file->title ($params{title}) if $params{title}; + $file->description($params{description}) if $params{description}; + } + if ( !$file->backend ) { + $file->backend($self->_get_backend_by_file_type($file)); + # load itime for new file + $file->save->load; + } + + $file->mtime(DateTime->now_local) unless $params{mtime}; + $file->mtime($params{mtime} ) if $params{mtime}; + + my $backend = $self->_get_backend($file->backend); + $params{dbfile} = $file; + $backend->save(%params); + + $file->save; + #ShopImage + if($file->object_type eq "shop_image"){ + my $image_content = $params{file_contents}; + my $thumbnail = file_probe_type($image_content); + my $shopimage = SL::DB::ShopImage->new(); + $shopimage->assign_attributes( + file_id => $file->id, + thumbnail_content => $thumbnail->{thumbnail_img_content}, + org_file_height => $thumbnail->{file_image_height}, + org_file_width => $thumbnail->{file_image_width}, + thumbnail_content_type => $thumbnail->{thumbnail_img_content_type}, + object_id => $file->object_id, + ); + $shopimage->save; + } + if ($params{file_type} eq 'document' && $params{source} ne 'created') { + SL::DB::History->new( + addition => 'IMPORT', + trans_id => $params{object_id}, + snumbers => $params{file_path}, + employee_id => SL::DB::Manager::Employee->current->id, + what_done => $params{dbfile}->id + )->save(); + } + return $params{obj} if $params{dbfile} && $params{obj}; + return SL::File::Object->new(db_file => $file, id => $file->id, loaded => 1); +} + +sub rename { + my ($self, %params) = @_; + return RENAME_NOFILE unless $params{id} || $params{dbfile}; + my $file = $params{dbfile}; + $file = SL::DB::Manager::File->get_first(query => [id => $params{id}]) if !$file; + return RENAME_NOFILE unless $file; + + $main::lxdebug->message(LXDebug->DEBUG2(), "rename id=" . $file->id . " to=" . $params{to}); + if ($params{to}) { + return RENAME_SAME if $params{to} eq $file->file_name; + return RENAME_EXISTS if $self->get_all_count( object_id => $file->object_id, + object_type => $file->object_type, + mime_type => $file->mime_type, + source => $file->source, + file_type => $file->file_type, + file_name => $params{to} + ) > 0; + + my $backend = $self->_get_backend($file->backend); + $backend->rename(dbfile => $file) if $backend; + $file->file_name($params{to}); + $file->save; + } + return RENAME_OK; +} + +sub get_backend_class { + my ($self, $backendname) = @_; + die "no backend name set" unless $backendname; + $self->_get_backend($backendname); +} + +sub get_other_sources { + my ($self) = @_; + my $pref = SL::Helper::UserPreferences->new(namespace => 'file_sources'); + $pref->login("#default#"); + my @sources; + foreach my $tuple (@{ $pref->get_all() }) { + my %lkeys = %{ SL::JSON::from_json($tuple->{value}) }; + my $source = { + 'name' => $tuple->{key}, + 'description' => $lkeys{desc}, + 'directory' => $lkeys{dir} + }; + push @sources, $source; + } + return @sources; +} + +sub sync_from_backend { + my ($self, %params) = @_; + return unless $params{file_type}; + my $file = SL::DB::File->new; + $file->file_type($params{file_type}); + my $backend = $self->_get_backend($self->_get_backend_by_file_type($file)); + return unless $backend; + $backend->sync_from_backend(%params); +} + +# +# internal +# +sub _get_backend { + my ($self, $backend_name) = @_; + my $class = 'SL::File::Backend::' . $backend_name; + my $obj = undef; + die $::locale->text('no backend enabled') if $backend_name eq 'None'; + eval { + eval "require $class"; + $obj = $class->new; + die $::locale->text('backend "#1" not enabled',$backend_name) unless $obj->enabled; + 1; + } or do { + if ( $obj ) { + die $@; + } else { + die $::locale->text('backend "#1" not found',$backend_name); + } + }; + return $obj; +} + +sub _get_backend_by_file_type { + my ($self, $dbfile) = @_; + + $main::lxdebug->message(LXDebug->DEBUG2(), "_get_backend_by_file_type=" .$dbfile." type=".$dbfile->file_type); + return "Filesystem" unless $dbfile; + return $::instance_conf->get_doc_storage_for_documents if $dbfile->file_type eq 'document'; + return $::instance_conf->get_doc_storage_for_attachments if $dbfile->file_type eq 'attachment'; + return $::instance_conf->get_doc_storage_for_images if $dbfile->file_type eq 'image'; + return "Filesystem"; +} + +1; + +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::File - The intermediate Layer for handling files + +=head1 SYNOPSIS + + # In a controller or helper ( see SL::Controller::File or SL::Helper::File ) + # you can create, remove, delete etc. a file in a backend independent way + + my $file = SL::File->save( + object_id => $self->object_id, + object_type => $self->object_type, + mime_type => 'application/pdf', + file_type => 'document', + file_contents => 'this is no pdf'); + + my $file1 = SL::File->get(id => $id); + SL::File->delete(id => $id); + SL::File->delete(dbfile => $file1); + SL::File->delete_all(object_id => $object_id, + object_type => $object_type, + file_type => $filetype # may be optional + ); + SL::File->rename(id => $id,to => $newname); + my $files1 = SL::File->get_all(object_id => $object_id, + object_type => $object_type, + file_type => 'image', # may be optional + source => 'uploaded' # may be optional + ); + + # Alternativelly some operation can be done with the filemangement object wrapper + # and additional oparations see L + +=head1 OVERVIEW + +The Filemanagemt can handle files in a storage independent way. Internal the File +use the configured storage backend for the type of file. +These backends must be configured in L or an extra database table. + +There are three types of files: + +=over 2 + +=item - document, + +which can be generated files (for sales), scanned files or uploaded files (for purchase) for an ERP-object. +They can exist in different versions. The versioning is handled implicit. All versions of a file may be +deleted by the user if she/he is allowed to do this. + +=item - attachment, + +which have additional information for an ERP-objects. They are uploadable. If a filename still exists +on a ERP-Object the new uploaded file is a new version of this or it must be renamed by user. + +There are generic attachments for a specific document group (like sales_invoices). This attachments can be +combinide/merged with the document-file in the time of printing. +Today only PDF-Attachmnets can be merged with the generated document-PDF. + +=item - image, + +they are like attachments, but they may be have thumbnails for displaying. +So the must have an image format like png,jpg. The versioning is like attachments + +=back + +For each type of files the backend can configured in L. + +The files have also the parameter C: + +=over 2 + +=item - created, generated by LaTeX + +=item - uploaded + +=item - scanner, import from scanner + +( or scanner1, scanner2 if there are different scanner, be configurable via UserPreferences ) + +=item - email, received by email and imported by hand or automatic. + +=back + +The files from source 'scanner' or 'email' are not allowed to delete else they must be send back to the sources. +This means they are moved back into the correspondent source directories. + +The scanner and email import must be configured via Table UserPreferences: + +=begin text + + id | login | namespace | version | key | value +----+---------+--------------+---------+----------+------------------------------------------------------ + 1 | default | file_sources | 0.00000 | scanner1 | {"dir":"/var/tmp/scanner1","desc":"Scanner Einkauf" } + 2 | default | file_sources | 0.00000 | emails | {"dir":"/var/tmp/emails" ,"desc":"Empfangene Mails"} + +=end text + +. + +The Fileinformation is stored in the table L for saving the information. +The modul and object_id describe the link to the object. + +The interface SL::File:Object encapsulate SL::DB:File, see L + +The storage backends are extra classes which depends from L. +So additional backend classes can be added. + +The implementation of versioning is done in the different backends. + +=head1 METHODS + +=over 4 + +=item C + +Creates a new SL::DB:File object or save an existing object for a specific backend depends of the C +and config, like + +=begin text + + SL::File->save( + object_id => $self->object_id, + object_type => $self->object_type, + content_type => 'application/pdf' + ); + +=end text + +. + +The file data is stored in the backend. If the file_type is "document" and the source is not "created" the file is imported, +so in the history the import is documented also as a hint to can unimport the file later. + +Available C: + +=over 4 + +=item C + +The id of SL::DB::File for an existing file + +=item C + +The Id of the ERP-object for a new file. + +=item C + +The Type of the ERP-object like "sales_quotation" for a new file. A clear mapping to the class/model exists in the controller. + +=item C + +The type may be "document", "attachment" or "image" for a new file. + +=item C + +The type may be "created", "uploaded" or email sources or scanner sources for a new file. + +=item C + +The file_name of the file for a new file. This name is used in the WebGUI and as name for download. + +=item C + +The mime_type of a new file. This is used for downloading or for email attachments. + +=item C or C + +The description or title of a new file. This must be discussed if this attribute is needed. + +=back + +=item C<delete PARAMS> + +The file data is deleted in the backend. If the file comes from source 'scanner' or 'email' +they moved back to the source folders. This is documented in the history. + +Available C<PARAMS>: + +=over 4 + +=item C<id> + +The id of SL::DB::File + +=item C<dbfile> + +As alternative if the SL::DB::File as object is available. + +=back + +=item C<delete_all PARAMS> + +All file data of an ERP-object is deleted in the backend. + +=over 4 + +=item C<object_id> + +The Id of the ERP-object. + +=item C<object_type> + +The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller. + +=back + +=item C<rename PARAMS> + +The Filename of the file is changed + +Available C<PARAMS>: + +=over 4 + +=item C<id> + +The id of SL::DB::File + +=item C<to> + +The new filename + +=back + +=item C<get PARAMS> + +The actual file object is retrieved. The id of the object is needed. + +Available C<PARAMS>: + +=over 4 + +=item C<id> + +The id of SL::DB::File + +=back + +=item C<get_all PARAMS> + +All last versions of file data objects which are related to an ERP-Document,Part,Customer,Vendor,... are retrieved. + +Available C<PARAMS>: + +=over 4 + +=item C<object_id> + +The Id of the ERP-object. + +=item C<object_type> + +The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller. + +=item C<file_type> + +The type may be "document", "attachment" or "image". This parameter is optional. + +=item C<file_name> + +The name of the file . This parameter is optional. + +=item C<mime_type> + +The MIME type of the file . This parameter is optional. + +=item C<source> + +The type may be "created", "uploaded" or email sources or scanner soureces. This parameter is optional. + +=item C<sort_by> + +An optional parameter in which sorting the files are retrieved. Default is decrementing itime and ascending filename + +=back + +=item C<get_all_versions PARAMS> + +All versions of file data objects which are related to an ERP-Document,Part,Customer,Vendor,... are retrieved. +If only the versions of one file are wanted, additional parameter like file_name must be set. +If the param C<dbfile> set, only the versions of this file are returned. + +Available C<PARAMS> ar the same as L<get_all> + + + +=item C<get_all_count PARAMS> + +The count of available files is returned. +Available C<PARAMS> ar the same as L<get_all> + + +=item C<get_content PARAMS> + +The data of a file can retrieved. A reference to the data is returned. + +Available C<PARAMS>: + +=over 4 + +=item C<id> + +The id of SL::DB::File + +=item C<dbfile> + +If no Id exists the object SL::DB::File as param. + +=back + +=item C<get_file_path PARAMS> + +Sometimes it is more useful to have a path to the file not the contents. If the backend has not stored the content as file +it is in the responsibility of the backend to create a tempory session file. + +Available C<PARAMS>: + +=over 4 + +=item C<id> + +The id of SL::DB::File + +=item C<dbfile> + +If no Id exists the object SL::DB::File as param. + +=back + +=item C<get_other_sources> + +A helpful method to get the sources for scanner and email from UserPreferences. This method is need from SL::Controller::File + +=item C<sync_from_backend> + +For Backends which may be changed outside of kivitendo a synchronization of the database is done. +This sync must be triggered by a periodical task. + +Needed C<PARAMS>: + +=over 4 + +=item C<file_type> + +The synchronization is done file_type by file_type. + +=back + +=back + +=head1 AUTHOR + +Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt> + +=cut