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