File Coverage

blib/lib/DBIx/Class/InflateColumn/FS.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package DBIx::Class::InflateColumn::FS;
2              
3 1     1   17694 use strict;
  1         2  
  1         31  
4 1     1   3 use warnings;
  1         2  
  1         23  
5 1     1   220 use DBIx::Class::UUIDColumns;
  0            
  0            
6             use File::Spec ();
7             use File::Path ();
8             use File::Copy ();
9             use Path::Class ();
10              
11             our $VERSION = '0.01007';
12              
13             =head1 NAME
14              
15             DBIx::Class::InflateColumn::FS - Inflate/deflate columns to Path::Class::File objects
16              
17             =head1 SYNOPSIS
18              
19             __PACKAGE__->load_components(qw/InflateColumn::FS Core/);
20             __PACKAGE__->add_columns(
21             id => {
22             data_type => 'INT',
23             is_auto_increment => 1,
24             },
25             file => {
26             data_type => 'TEXT',
27             is_fs_column => 1,
28             fs_column_path => '/var/lib/myapp/myfiles',
29             },
30             file_2 => {
31             data_type => 'TEXT',
32             is_fs_column => 1,
33             fs_column_path => '/var/lib/myapp/myfiles',
34             fs_new_on_update => 1
35             },
36             );
37             __PACKAGE__->set_primary_key('id');
38              
39             # in application code
40             $rs->create({ file => $file_handle });
41              
42             $row = $rs->find({ id => $id });
43             my $fh = $row->file->open('r');
44              
45             =head1 DESCRIPTION
46              
47             Provides inflation to a Path::Class::File object allowing file system storage
48             of BLOBS.
49              
50             The storage path is specified with C<fs_column_path>. Each file receives a
51             unique name, so the storage for all FS columns can share the same path.
52              
53             Within the path specified by C<fs_column_path>, files are stored in
54             sub-directories based on the first 2 characters of the unique file names. Up to
55             256 sub-directories will be created, as needed. Override C<_fs_column_dirs> in
56             a derived class to change this behavior.
57              
58             C<fs_new_on_update> will create a new file name if the file has been updated.
59              
60             =cut
61              
62             =head1 METHODS
63              
64             =cut
65              
66             =head2 inflate_result
67              
68             =cut
69              
70             sub inflate_result {
71             my ($class, $source, $me, $prefetch) = @_;
72              
73             my $new = $class->next::method($source, $me, $prefetch);
74            
75             while ( my($column, $data) = each %{$new->{_column_data}} ) {
76             if ( $source->has_column($column) && $source->column_info($column)->{is_fs_column} && defined $data ) {
77             $new->{_fs_column_filename}{$column} = $data;
78             }
79             }
80            
81             return $new;
82             }
83              
84              
85             =head2 register_column
86              
87             =cut
88              
89             sub register_column {
90             my ($self, $column, $info, @rest) = @_;
91             $self->next::method($column, $info, @rest);
92             return unless defined($info->{is_fs_column});
93              
94             $self->inflate_column($column => {
95             inflate => sub {
96             my ($value, $obj) = @_;
97             $obj->_inflate_fs_column($column, $value);
98             },
99             deflate => sub {
100             my ($value, $obj) = @_;
101             $obj->_deflate_fs_column($column, $value);
102             },
103             });
104             }
105              
106             =head2 fs_file_name
107              
108             Provides the file naming algorithm. Override this method to change it.
109              
110             This method is called with two parameters: The name of the column and the
111             C<< column_info >> object.
112              
113             =cut
114              
115             sub fs_file_name {
116             my ($self, $column, $column_info) = @_;
117             return DBIx::Class::UUIDColumns->get_uuid;
118             }
119              
120             sub _fs_column_storage {
121             my ( $self, $column ) = @_;
122              
123             my $column_info = $self->result_source->column_info($column);
124             $self->throw_exception("$column is not an fs_column")
125             unless $column_info->{is_fs_column};
126              
127             $self->{_fs_column_filename}{$column} ||= do {
128             my $filename = $self->fs_file_name($column, $column_info);
129             File::Spec->catfile($self->_fs_column_dirs($filename), $filename);
130             };
131              
132             return Path::Class::File->new($column_info->{fs_column_path}, $self->{_fs_column_filename}{$column});
133             }
134              
135             =head2 _fs_column_dirs
136              
137             Returns the sub-directory components for a given file name. Override it to
138             provide a deeper directory tree or change the algorithm.
139              
140             =cut
141              
142             sub _fs_column_dirs {
143             shift;
144             my $filename = shift;
145              
146             return $filename =~ /(..)/;
147             }
148              
149             =head2 copy
150              
151             Copies a row object, duplicating the files backing fs columns.
152              
153             =cut
154              
155             sub copy {
156             my ($self, $changes) = @_;
157              
158             $changes ||= {};
159             my $col_data = { %{$self->{_column_data}} };
160              
161             foreach my $col ( keys %$col_data ) {
162             my $column_info = $self->result_source->column_info($col);
163             if ( $column_info->{is_fs_column} && defined $col_data->{$col} ) { # nothing special required for NULLs
164             delete $col_data->{$col};
165            
166             # pass the original file to produce a copy on deflate
167             $changes->{$col} = $self->get_inflated_column($col);
168             }
169             }
170              
171             my $temp = bless { _column_data => $col_data }, ref $self;
172             $temp->result_source($self->result_source);
173              
174             return $temp->next::method($changes);
175             }
176              
177             =head2 delete
178              
179             Deletes the associated file system storage when a row is deleted.
180              
181             =cut
182              
183             sub delete {
184             my ( $self, @rest ) = @_;
185              
186             for my $column ( $self->columns ) {
187             my $column_info = $self->result_source->column_info($column);
188             if ( $column_info->{is_fs_column} ) {
189             my $accessor = $column_info->{accessor} || $column;
190             $self->$accessor && $self->$accessor->remove;
191             }
192             }
193              
194             return $self->next::method(@rest);
195             }
196              
197             =head2 set_column
198              
199             Deletes file storage when an fs_column is set to undef.
200              
201             =cut
202              
203             sub set_column {
204             my ($self, $column, $new_value) = @_;
205              
206             if ( !defined $new_value && $self->result_source->column_info($column)->{is_fs_column}
207             && $self->{_fs_column_filename}{$column} ) {
208             $self->_fs_column_storage($column)->remove;
209             delete $self->{_fs_column_filename}{$column};
210             }
211              
212             return $self->next::method($column, $new_value);
213             }
214              
215             =head2 set_inflated_column
216              
217             Re-inflates after setting an fs_column.
218              
219             =cut
220              
221             sub set_inflated_column {
222             my ($self, $column, $inflated) = @_;
223              
224             $self->next::method($column, $inflated);
225              
226             # reinflate
227             if ( defined $inflated && ref $inflated && ref $inflated ne 'SCALAR'
228             && $self->result_source->column_info($column)->{is_fs_column} ) {
229             $inflated = $self->{_inflated_column}{$column} = $self->_fs_column_storage($column);
230             }
231             return $inflated;
232             }
233              
234             =head2 _inflate_fs_column
235              
236             Inflates a file column to a Path::Class::File object.
237              
238             =cut
239              
240             sub _inflate_fs_column {
241             my ( $self, $column, $value ) = @_;
242             return unless defined $value;
243              
244             $self->{_fs_column_filename}{$column} = $value;
245             return $self->_fs_column_storage($column);
246             }
247              
248             =head2 _deflate_fs_column
249              
250             Deflates a file column to its storage path name, relative to C<fs_column_path>.
251             In the database, a file column is just a place holder for inflation/deflation.
252             The actual file lives in the file system.
253              
254             =cut
255              
256             sub _deflate_fs_column {
257             my ( $self, $column, $value ) = @_;
258              
259             my $column_info = $self->result_source->column_info($column);
260              
261             # kill the old storage, rather than overwrite, if fs_new_on_update
262             if ( $column_info->{fs_new_on_update} && $self->{_fs_column_filename}{$column} ) {
263             my $oldfile = $self->_fs_column_storage($column);
264             if ( $oldfile ne $value ) {
265             $oldfile->remove;
266             delete $self->{_fs_column_filename}{$column};
267             }
268             }
269            
270             my $file = $self->_fs_column_storage($column);
271             if ( $value ne $file ) {
272             File::Path::mkpath([$file->dir]);
273              
274             # get a filehandle if we were passed a Path::Class::File
275             my $fh1 = eval { $value->openr } || $value;
276             my $fh2 = $file->openw or die;
277             File::Copy::copy($fh1, $fh2);
278              
279             $self->{_inflated_column}{$column} = $file;
280              
281             # ensure the column will be marked dirty
282             $self->{_column_data}{$column} = undef;
283             }
284             return $self->{_fs_column_filename}{$column};
285             }
286              
287             sub DESTROY {
288             my $self = shift;
289              
290             return if $self->in_storage;
291              
292             # If fs columns were deflated, but the row was never stored, we need to delete the
293             # backing files.
294             while ( my ( $col, $data ) = each %{ $self->{_column_data} } ) {
295             my $column_info = $self->result_source->column_info($col);
296             if ( $column_info->{is_fs_column} && defined $data ) {
297             my $accessor = $column_info->{accessor} || $col;
298             $self->$accessor->remove;
299             }
300             }
301             }
302              
303             =head2 table
304              
305             Overridden to provide a hook for specifying the resultset_class. If
306             you provide your own resultset_class, inherit from
307             InflateColumn::FS::ResultSet.
308              
309             =cut
310              
311             sub table {
312             my $self = shift;
313              
314             my $ret = $self->next::method(@_);
315             if ( @_ && $self->result_source_instance->resultset_class
316             eq 'DBIx::Class::ResultSet' ) {
317             $self->result_source_instance
318             ->resultset_class('DBIx::Class::InflateColumn::FS::ResultSet');
319             }
320             return $ret;
321             }
322              
323             =head1 SUPPORT
324              
325             Community support can be found via:
326              
327             Mailing list: http://lists.scsys.co.uk/mailman/listinfo/dbix-class/
328              
329             IRC: irc.perl.org#dbix-class
330              
331             The author is C<semifor> on IRC and a member of the mailing list.
332              
333             =head1 AUTHOR
334              
335             semifor: Marc Mims <marc@questright.com>
336              
337             =head1 CONTRIBUTORS
338              
339             mst: Matt S. Trout <mst@shadowcatsystems.co.uk>
340              
341             mo: Moritz Onken <onken@netcubed.de>
342              
343             norbi: Norbert Buchmuller <norbi@nix.hu>
344              
345             =head1 LICENSE
346              
347             You may distribute this code under the same terms as Perl itself.
348              
349             =cut
350              
351             1;