mini-DMS: Filesystem-Backend: undef-Warnung vermeiden
[kivitendo-erp.git] / SL / File / Backend / Filesystem.pm
1 package SL::File::Backend::Filesystem;
2
3 use strict;
4
5 use parent qw(SL::File::Backend);
6 use SL::DB::File;
7 use File::Copy;
8 use File::Slurp;
9 use File::stat;
10 use File::Path qw(make_path);
11
12 #
13 # public methods
14 #
15
16 sub delete {
17   my ($self, %params) = @_;
18   die "no dbfile in backend delete" unless $params{dbfile};
19   my $last_version  = $params{dbfile}->backend_data;
20   my $first_version = 1;
21   $last_version     = 0                               if $params{last};
22   $last_version     = $params{dbfile}->backend_data-1 if $params{all_but_notlast};
23   $last_version     = $params{version}                if $params{version};
24   $first_version    = $params{version}                if $params{version};
25
26   if ($last_version > 0 ) {
27     for my $version ( $first_version..$last_version) {
28       my $file_path = $self->_filesystem_path($params{dbfile},$version);
29       unlink($file_path);
30     }
31     if ($params{version}) {
32       for my $version ( $last_version+1 .. $params{dbfile}->backend_data) {
33         my $from = $self->_filesystem_path($params{dbfile},$version);
34         my $to   = $self->_filesystem_path($params{dbfile},$version - 1);
35         die "file not exists in backend delete" unless -f $from;
36         rename($from,$to);
37       }
38       $params{dbfile}->backend_data($params{dbfile}->backend_data-1);
39     }
40     elsif ($params{all_but_notlast}) {
41       my $from = $self->_filesystem_path($params{dbfile},$params{dbfile}->backend_data);
42       my $to   = $self->_filesystem_path($params{dbfile},1);
43       die "file not exists in backend delete" unless -f $from;
44       rename($from,$to);
45       $params{dbfile}->backend_data(1);
46     } else {
47       $params{dbfile}->backend_data(0);
48     }
49     unless ($params{dbfile}->backend_data) {
50       my $dir_path = $self->_filesystem_path($params{dbfile});
51       rmdir($dir_path);
52     }
53   } else {
54     my $file_path = $self->_filesystem_path($params{dbfile},$params{dbfile}->backend_data);
55     die "file not exists in backend delete" unless -f $file_path;
56     unlink($file_path);
57     $params{dbfile}->backend_data($params{dbfile}->backend_data-1);
58   }
59   return 1;
60 }
61
62 sub rename {
63 }
64
65 sub save {
66   my ($self, %params) = @_;
67   die 'dbfile not exists' unless $params{dbfile};
68   my $dbfile = $params{dbfile};
69   die 'no file contents' unless $params{file_path} || $params{file_contents};
70
71   # Do not save and do not create a new version of the document if file size of last version is the same.
72   if ($dbfile->source eq 'created' && $self->get_version_count(dbfile => $dbfile)) {
73     my $new_file_size  = $params{file_path} ? stat($params{file_path})->size : length($params{file_contents});
74     my $last_file_size = stat($self->_filesystem_path($dbfile))->size;
75
76     return 1 if $last_file_size == $new_file_size;
77   }
78
79   $dbfile->backend_data(0) unless $dbfile->backend_data;
80   $dbfile->backend_data($dbfile->backend_data*1+1);
81   $dbfile->save->load;
82
83   my $tofile = $self->_filesystem_path($dbfile);
84   if ($params{file_path} && -f $params{file_path}) {
85     File::Copy::copy($params{file_path}, $tofile);
86   }
87   elsif ($params{file_contents}) {
88     open(OUT, "> " . $tofile);
89     print OUT $params{file_contents};
90     close(OUT);
91   }
92   if ($params{mtime}) {
93     utime($params{mtime}, $params{mtime}, $tofile);
94   }
95   return 1;
96 }
97
98 sub get_version_count {
99   my ($self, %params) = @_;
100   die "no dbfile" unless $params{dbfile};
101   return $params{dbfile}->backend_data//0 * 1;
102 }
103
104 sub get_mtime {
105   my ($self, %params) = @_;
106   die "no dbfile" unless $params{dbfile};
107   die "unknown version" if $params{version} &&
108                           ($params{version} < 0 || $params{version} > $params{dbfile}->backend_data);
109   my $path = $self->_filesystem_path($params{dbfile}, $params{version});
110
111   die "No file found at $path. Expected: $params{dbfile}{file_name}, file.id: $params{dbfile}{id}" if !-f $path;
112
113   my $dt = DateTime->from_epoch(epoch => stat($path)->mtime, time_zone => $::locale->get_local_time_zone()->name, locale => $::lx_office_conf{system}->{language})->clone();
114   return $dt;
115 }
116
117 sub get_filepath {
118   my ($self, %params) = @_;
119   die "no dbfile" unless $params{dbfile};
120   my $path = $self->_filesystem_path($params{dbfile},$params{version});
121
122   die "No file found at $path. Expected: $params{dbfile}{file_name}, file.id: $params{dbfile}{id}" if !-f $path;
123
124   return $path;
125 }
126
127 sub get_content {
128   my ($self, %params) = @_;
129   my $path = $self->get_filepath(%params);
130   return "" unless $path;
131   my $contents = File::Slurp::read_file($path);
132   return \$contents;
133 }
134
135 sub enabled {
136   return 0 unless $::instance_conf->get_doc_files;
137   return 0 unless $::lx_office_conf{paths}->{document_path};
138   return 0 unless -d $::lx_office_conf{paths}->{document_path};
139   return 1;
140 }
141
142 sub sync_from_backend {
143   my ($self, %params) = @_;
144   my @query = (file_type => $params{file_type});
145   push @query, (file_name => $params{file_name}) if $params{file_name};
146   push @query, (mime_type => $params{mime_type}) if $params{mime_type};
147   push @query, (source    => $params{source})    if $params{source};
148
149   my $sortby = $params{sort_by} || 'itime DESC,file_name ASC';
150
151   my @files = @{ SL::DB::Manager::File->get_all(query => [@query], sort_by => $sortby) };
152   for (@files) {
153     $main::lxdebug->message(LXDebug->DEBUG2(), "file id=" . $_->id." version=".$_->backend_data);
154     my $newversion = $_->backend_data;
155     for my $version ( reverse 1 .. $_->backend_data ) {
156       my $path = $self->_filesystem_path($_, $version);
157       $main::lxdebug->message(LXDebug->DEBUG2(), "path=".$path." exists=".( -f $path?1:0));
158       last if -f $path;
159       $newversion = $version - 1;
160     }
161     $main::lxdebug->message(LXDebug->DEBUG2(), "newversion=".$newversion." version=".$_->backend_data);
162     if ( $newversion < $_->backend_data ) {
163       $_->backend_data($newversion);
164       $_->save   if $newversion >  0;
165       $_->delete if $newversion <= 0;
166     }
167   }
168
169 }
170
171 #
172 # internals
173 #
174
175 sub _filesystem_path {
176   my ($self, $dbfile, $version) = @_;
177
178   die "No files backend enabled" unless $::instance_conf->get_doc_files || $::lx_office_conf{paths}->{document_path};
179
180   # use filesystem with depth 3
181   $version    = $dbfile->backend_data if !$version || $version < 1 || $version > $dbfile->backend_data;
182   my $iddir   = sprintf("%04d", $dbfile->id % 1000);
183   my $path    = File::Spec->catdir($::lx_office_conf{paths}->{document_path}, $::auth->client->{id}, $iddir, $dbfile->id);
184   if (!-d $path) {
185     File::Path::make_path($path, { chmod => 0770 });
186   }
187   return $path if !$version;
188   return File::Spec->catdir($path, $dbfile->id . '_' . $version);
189 }
190
191 1;
192
193 __END__
194
195 =pod
196
197 =encoding utf8
198
199 =head1 NAME
200
201 SL::File::Backend::Filesystem  - Filesystem class for file storage backend
202
203 =head1 SYNOPSIS
204
205 See the synopsis of L<SL::File::Backend>.
206
207 =head1 OVERVIEW
208
209 This specific storage backend use a Filesystem which is only accessed by this interface.
210 This is the big difference to the Webdav backend where the files can be accessed without the control of that backend.
211 This backend use the database id of the SL::DB::File object as filename. The filesystem has up to 1000 subdirectories
212 to store the files not to flat in the filesystem. In this Subdirectories for each file an additional subdirectory exists
213 for the versions of this file.
214
215 The Versioning is done via a Versionnumber which is incremented by one for each version.
216 So the Version 2 of the file with the database id 4 is stored as path {root}/0004/4/4_2.
217
218
219 =head1 METHODS
220
221 See methods of L<SL::File::Backend>.
222
223 =head1 SEE ALSO
224
225 L<SL::File::Backend>
226
227 =head1 AUTHOR
228
229 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
230
231 =cut