Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / File.pm
diff --git a/SL/File.pm b/SL/File.pm
new file mode 100644 (file)
index 0000000..ca065e0
--- /dev/null
@@ -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<SL::File::Object>
+
+=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<SL::Controller::ClientConfig> 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<SL::Controller::ClientConfig>.
+
+The files have also the parameter C<Source>:
+
+=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<SL::DB::File> 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<SL::DB::Object>
+
+The storage backends are extra classes which depends from L<SL::File::Backend>.
+So additional backend classes can be added.
+
+The implementation of versioning is done in the different backends.
+
+=head1 METHODS
+
+=over 4
+
+=item C<save>
+
+Creates a new SL::DB:File object or save an existing object for a specific backend depends of the C<file_type>
+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<PARAMS>:
+
+=over 4
+
+=item C<id>
+
+The id of SL::DB::File for an existing file
+
+=item C<object_id>
+
+The Id of the ERP-object for a new file.
+
+=item C<object_type>
+
+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<file_type>
+
+The type may be "document", "attachment" or "image" for a new file.
+
+=item C<source>
+
+The type may be "created", "uploaded" or email sources or scanner sources for a new file.
+
+=item C<file_name>
+
+The file_name of the file for a new file. This name is used in the WebGUI and as name for download.
+
+=item C<mime_type>
+
+The mime_type of a new file. This is used for downloading or for email attachments.
+
+=item C<description> or C<title>
+
+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