Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / File / Backend / Webdav.pm
diff --git a/SL/File/Backend/Webdav.pm b/SL/File/Backend/Webdav.pm
new file mode 100644 (file)
index 0000000..bf8f9af
--- /dev/null
@@ -0,0 +1,341 @@
+package SL::File::Backend::Webdav;
+
+use strict;
+
+use parent qw(SL::File::Backend);
+use SL::DB::File;
+
+use SL::System::Process;
+use File::Copy;
+use File::Slurp;
+use File::Basename;
+use File::Path qw(make_path);
+use File::MimeInfo::Magic;
+use File::stat;
+
+#
+# public methods
+#
+
+sub delete {
+  my ($self, %params) = @_;
+  $main::lxdebug->message(LXDebug->DEBUG2(), "del in backend " . $self . "  file " . $params{dbfile});
+  $main::lxdebug->message(LXDebug->DEBUG2(), "file id=" . $params{dbfile}->id * 1);
+  return 0 unless $params{dbfile};
+  my ($file_path, undef, undef) = $self->webdav_path($params{dbfile});
+  unlink($file_path);
+  return 1;
+}
+
+sub rename {
+  my ($self, %params) = @_;
+  return 0 unless $params{dbfile};
+  my (undef, $oldwebdavname) = split(/:/, $params{dbfile}->location, 2);
+  my ($tofile, $basepath, $basename) = $self->webdav_path($params{dbfile});
+  my $fromfile = File::Spec->catfile($basepath, $oldwebdavname);
+  $main::lxdebug->message(LXDebug->DEBUG2(), "renamefrom=" . $fromfile . " to=" . $tofile);
+  move($fromfile, $tofile);
+}
+
+sub save {
+  my ($self, %params) = @_;
+  die 'dbfile not exists' unless $params{dbfile};
+  $main::lxdebug->message(LXDebug->DEBUG2(), "in backend " . $self . "  file " . $params{dbfile});
+  $main::lxdebug->message(LXDebug->DEBUG2(), "file id=" . $params{dbfile}->id);
+  my $dbfile = $params{dbfile};
+  die 'no file contents' unless $params{file_path} || $params{file_contents};
+
+  if ($params{dbfile}->id * 1 == 0) {
+
+    # new element: need id for file
+    $params{dbfile}->save;
+  }
+  my ($tofile, undef, $basename) = $self->webdav_path($params{dbfile});
+  if ($params{file_path} && -f $params{file_path}) {
+    copy($params{file_path}, $tofile);
+  }
+  elsif ($params{file_contents}) {
+    open(OUT, "> " . $tofile);
+    print OUT $params{file_contents};
+    close(OUT);
+  }
+  return 1;
+}
+
+sub get_version_count {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  ## TODO
+  return 1;
+}
+
+sub get_mtime {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  $main::lxdebug->message(LXDebug->DEBUG2(), "version=" .$params{version});
+  my ($path, undef, undef) = $self->webdav_path($params{dbfile});
+  die "No file found in Backend: " . $path unless -f $path;
+  my $dt = DateTime->from_epoch(epoch => stat($path)->mtime, time_zone => $::locale->get_local_time_zone()->name)->clone();
+  $main::lxdebug->message(LXDebug->DEBUG2(), "dt=" .$dt);
+  return $dt;
+}
+
+sub get_filepath {
+  my ($self, %params) = @_;
+  die "no dbfile" unless $params{dbfile};
+  my ($path, undef, undef) = $self->webdav_path($params{dbfile});
+  die "No file found in Backend: " . $path unless -f $path;
+  return $path;
+}
+
+sub get_content {
+  my ($self, %params) = @_;
+  my $path = $self->get_filepath(%params);
+  return "" unless $path;
+  my $contents = File::Slurp::read_file($path);
+  return \$contents;
+}
+
+sub sync_from_backend {
+  my ($self, %params) = @_;
+  return unless $params{file_type};
+
+  $self->sync_all_locations(%params);
+
+}
+
+sub enabled {
+  return $::instance_conf->get_doc_webdav;
+}
+
+#
+# internals
+#
+
+my %type_to_path = (
+  sales_quotation             => 'angebote',
+  sales_order                 => 'bestellungen',
+  request_quotation           => 'anfragen',
+  purchase_order              => 'lieferantenbestellungen',
+  sales_delivery_order        => 'verkaufslieferscheine',
+  purchase_delivery_order     => 'einkaufslieferscheine',
+  credit_note                 => 'gutschriften',
+  invoice                     => 'rechnungen',
+  invoice_for_advance_payment => 'rechnungen',
+  final_invoice               => 'rechnungen',
+  purchase_invoice            => 'einkaufsrechnungen',
+  part                        => 'waren',
+  service                     => 'dienstleistungen',
+  assembly                    => 'erzeugnisse',
+  letter                      => 'briefe',
+  general_ledger              => 'dialogbuchungen',
+  gl_transaction              => 'dialogbuchungen',
+  accounts_payable            => 'kreditorenbuchungen',
+  shop_image                  => 'shopbilder',
+  customer                    => 'kunden',
+  vendor                      => 'lieferanten',
+);
+
+my %type_to_model = (
+  sales_quotation             => 'Order',
+  sales_order                 => 'Order',
+  request_quotation           => 'Order',
+  purchase_order              => 'Order',
+  sales_delivery_order        => 'DeliveryOrder',
+  purchase_delivery_order     => 'DeliveryOrder',
+  credit_note                 => 'Invoice',
+  invoice                     => 'Invoice',
+  invoice_for_advance_payment => 'Invoice',
+  final_invoice               => 'Invoice',
+  purchase_invoice            => 'PurchaseInvoice',
+  part                        => 'Part',
+  service                     => 'Part',
+  assembly                    => 'Part',
+  letter                      => 'Letter',
+  general_ledger              => 'GLTransaction',
+  gl_transaction              => 'GLTransaction',
+  accounts_payable            => 'GLTransaction',
+  shop_image                  => 'Part',
+  customer                    => 'Customer',
+  vendor                      => 'Vendor',
+);
+
+my %model_to_number = (
+  Order           => 'ordnumber',
+  DeliveryOrder   => 'ordnumber',
+  Invoice         => 'invnumber',
+  PurchaseInvoice => 'invnumber',
+  Part            => 'partnumber',
+  Letter          => 'letternumber',
+  GLTransaction   => 'reference',
+  ShopImage       => 'partnumber',
+  Customer        => 'customernumber',
+  Vendor          => 'vendornumber',
+);
+
+sub webdav_path {
+  my ($self, $dbfile) = @_;
+
+  #die "No webdav backend enabled" unless $::instance_conf->get_webdav;
+
+  my $type = $type_to_path{ $dbfile->object_type };
+
+  die "Unknown type" unless $type;
+
+  my $number = $dbfile->backend_data;
+  if ($number eq '') {
+    $number = $self->_get_number_from_model($dbfile);
+    $dbfile->backend_data($number);
+    $dbfile->save;
+  }
+  $main::lxdebug->message(LXDebug->DEBUG2(), "file_name=" . $dbfile->file_name ." number=".$number);
+
+  my @fileparts = split(/_/, $dbfile->file_name);
+  my $number_ext = pop @fileparts;
+  my ($maynumber, $ext) = split(/\./, $number_ext, 2);
+  push @fileparts, $maynumber if $maynumber ne $number;
+
+  my $basename = join('_', @fileparts);
+
+  my $path = File::Spec->catdir($self->get_rootdir, "webdav", $::auth->client->{id}, $type, $number);
+  if (!-d $path) {
+    File::Path::make_path($path, { chmod => 0770 });
+  }
+  my $fname = $basename . '_' . $number . '_' . $dbfile->itime->strftime('%Y%m%d_%H%M%S');
+  $fname .= '.' . $ext if $ext;
+
+  $main::lxdebug->message(LXDebug->DEBUG2(), "webdav path=" . $path . " filename=" . $fname);
+
+  return (File::Spec->catfile($path, $fname), $path, $fname);
+}
+
+sub get_rootdir { SL::System::Process::exe_dir() }
+
+sub _get_number_from_model {
+  my ($self, $dbfile) = @_;
+
+  my $class = 'SL::DB::' . $type_to_model{ $dbfile->object_type };
+  eval "require $class";
+  my $obj = $class->new(id => $dbfile->object_id)->load;
+  die 'no object found' unless $obj;
+  my $numberattr = $model_to_number{ $type_to_model{ $dbfile->object_type } };
+  return $obj->$numberattr;
+}
+
+#
+# TODO not fully imlemented and tested
+#
+sub sync_all_locations {
+  my ($self, %params) = @_;
+
+  my %dateparms = (dateformat => 'yyyymmdd');
+
+  foreach my $type (keys %type_to_path) {
+
+    my @query = (
+      file_type => $params{file_type},
+      object_type    => $type
+    );
+    my @oldfiles = @{ SL::DB::Manager::File->get_all(
+        query => [
+          file_type => $params{file_type},
+          object_type    => $type
+        ]
+      )
+    };
+
+    my $path = File::Spec->catdir($self->get_rootdir, "webdav", $::auth->client->{id},$type_to_path{$type});
+
+    if (opendir my $dir, $path) {
+      foreach my $file (sort { lc $a cmp lc $b }
+        map { decode("UTF-8", $_) } readdir $dir)
+      {
+        next if (($file eq '.') || ($file eq '..'));
+
+        my $fname = $file;
+        $fname =~ s|.*/||;
+
+        my ($filename, $number, $date, $time_ext) = split(/_/, $fname);
+        my ($time, $ext) = split(/\./, $time_ext, 2);
+
+        $time = substr($time, 0, 2) . ':' . substr($time, 2, 2) . ':' . substr($time, 4, 2);
+
+        #my @found = grep { $_->backend_data eq $fname } @oldfiles;
+        #if (scalar(@found) > 0) {
+        #  @oldfiles = grep { $_ != @found[0] } @oldfiles;
+        #}
+        #else {
+          my $dbfile = SL::DB::File->new();
+          my $class  = 'SL::DB::Manager::' . $type_to_model{$type};
+          my $obj =
+            $class->find_by(
+            $model_to_number{ $type_to_model{$type} } => $number);
+          if ($obj) {
+
+            my $mime_type = File::MimeInfo::Magic::magic(File::Spec->catfile($path, $fname));
+            if (!$mime_type) {
+              # if filename has the suffix "pdf", but is really no pdf set mimetype for no suffix
+              $mime_type = File::MimeInfo::Magic::mimetype($fname);
+              $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
+            }
+
+            $dbfile->assign_attributes(
+              object_id   => $obj->id,
+              object_type => $type,
+              source      => $params{file_type} eq 'document' ? 'created' : 'uploaded',
+              file_type   => $params{file_type},
+              file_name   => $filename . '_' . $number . '_' . $ext,
+              mime_type   => $mime_type,
+              itime       => $::locale->parse_date_to_object($date . ' ' . $time, %dateparms),
+            );
+            $dbfile->save;
+          }
+        #}
+
+        closedir $dir;
+      }
+    }
+  }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::File::Backend::Filesystem  - Filesystem class for file storage backend
+
+=head1 SYNOPSIS
+
+See the synopsis of L<SL::File::Backend>.
+
+=head1 OVERVIEW
+
+This specific storage backend use a Filesystem which is only accessed by this interface.
+This is the big difference to the Webdav backend where the files can be accessed without the control of that backend.
+This backend use the database id of the SL::DB::File object as filename. The filesystem has up to 1000 subdirectories
+to store the files not to flat in the filesystem.
+
+
+=head1 METHODS
+
+See methods of L<SL::File::Backend>.
+
+=head1 SEE ALSO
+
+L<SL::File::Backend>
+
+=head1 TODO
+
+The synchronization must be tested and a periodical task is needed to synchronize in some time periods.
+
+=head1 AUTHOR
+
+Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
+
+=cut