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