File Coverage

blib/lib/Yancy/Plugin/File.pm
Criterion Covered Total %
statement 70 70 100.0
branch 8 8 100.0
condition 10 13 76.9
subroutine 10 10 100.0
pod 3 3 100.0
total 101 104 97.1


line stmt bran cond sub pod time code
1             package Yancy::Plugin::File;
2             our $VERSION = '1.087';
3             # ABSTRACT: Manage file uploads, attachments, and other assets
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod # Write a file
8             #pod $c->yancy->file->write( $c->param( 'upload' ) );
9             #pod
10             #pod =head1 DESCRIPTION
11             #pod
12             #pod B This module is C and its API may change before
13             #pod Yancy v2.000 is released.
14             #pod
15             #pod This plugin manages file uploads. Files are stored in the C by
16             #pod
17             #pod This plugin API is meant to be subclassed by other asset storage
18             #pod mechanisms such as Hadoop or Amazon S3.
19             #pod
20             #pod =head2 Cleanup
21             #pod
22             #pod Files are B immediately deleted after they are no longer needed.
23             #pod Instead, a L method exists to periodically clean up any files
24             #pod that are not referenced. You should schedule this to run daily or weekly
25             #pod in cron:
26             #pod
27             #pod # Clean up files every week
28             #pod 0 0 * * 0 ./myapp.pl eval 'app->yancy->file->cleanup( app->yancy->backend, app->yancy->schema )'
29             #pod
30             #pod =head1 CONFIGURATION
31             #pod
32             #pod This plugin has the following configuration options.
33             #pod
34             #pod =head2 file_root
35             #pod
36             #pod The root path to store files. Defaults to C in the application's home
37             #pod directory.
38             #pod
39             #pod =head2 url_root
40             #pod
41             #pod The URL used to reach the C. Defaults to C.
42             #pod
43             #pod =head2 moniker
44             #pod
45             #pod The name to use for the helper. Defaults to C (creating a C helper).
46             #pod Change this to add multiple file plugins.
47             #pod
48             #pod =head1 SEE ALSO
49             #pod
50             #pod L
51             #pod
52             #pod =cut
53              
54 19     19   14350 use Mojo::Base 'Mojolicious::Plugin';
  19         50  
  19         141  
55 19     19   3795 use Yancy::Util qw( currym is_type );
  19         42  
  19         1090  
56 19     19   6561 use Digest;
  19         7325  
  19         659  
57 19     19   131 use Mojo::Asset::File;
  19         45  
  19         186  
58 19     19   662 use Mojo::File qw( path );
  19         43  
  19         21626  
59              
60             has file_root =>;
61             has url_root =>;
62             has digest_type => 'SHA-1';
63             has moniker => 'file';
64              
65             sub register {
66 52     52 1 474 my ( $self, $app, $config ) = @_;
67 52 100       381 my $file_root = $config->{file_root} ? path( $config->{file_root} ) : $app->home->child( 'public/uploads' );
68 52         1492 $self->file_root( $file_root );
69 52   100     679 my $url_root = $config->{url_root} // '/uploads';
70 52         231 $self->url_root( $url_root );
71 52   50     506 my $moniker = $config->{moniker} // 'file';
72 52         213 $self->moniker( $moniker );
73 52     7   601 $app->helper( 'yancy.' . $moniker, sub { $self } );
  7         29410  
74             }
75              
76             #pod =method write
77             #pod
78             #pod $url_path = $c->yancy->file->write( $upload );
79             #pod $url_path = $c->yancy->file->write( $name, $asset );
80             #pod
81             #pod Write a file into storage. C<$upload> is a L object. C<$name>
82             #pod is a filename and C<$asset> is a L object. Returns the URL
83             #pod of the uploaded file.
84             #pod
85             #pod =cut
86              
87             sub write {
88 4     4 1 15 my ( $self, $name, $asset ) = @_;
89 4 100       19 if ( ref $name eq 'Mojo::Upload' ) {
90 3         14 $asset = $name->asset;
91 3         24 $name = $name->filename;
92             }
93 4         23 my $digest = $self->_digest_file( $asset );
94 4         45 my @path_parts = grep $_, split /(..)/, $digest, 3;
95 4         20 my $root = $self->file_root;
96 4         36 my $path = $root->child( @path_parts )->make_path;
97 4         1280 my $file_path = $path->child( $name );
98 4         105 $file_path->spurt( $asset->slurp );
99 4         828 return join '/', $self->url_root, $file_path->to_rel( $root );
100             }
101              
102             #pod =method cleanup
103             #pod
104             #pod $app->yancy->file->cleanup( $app->yancy->backend );
105             #pod $app->yancy->file->cleanup( $app->yancy->backend, $app->yancy->schema );
106             #pod
107             #pod Clean up any files that do not exist in the given backend. Call this daily
108             #pod or weekly to remove files that aren't needed anymore.
109             #pod
110             #pod =cut
111              
112             sub cleanup {
113 1     1 1 15 my ( $self, $backend, $schema ) = @_;
114 1   33     12 $schema ||= $backend->schema;
115             # Clean up any unlinked files by scanning the entire database for
116             # files and then leaving only those files.
117 1         4 my ( %files, %linked );
118              
119             # List all the files
120 1         5 for my $path ( $self->file_root->list_tree->each ) {
121 2         1158 $files{ $path }++;
122             }
123              
124             # Find all the linked files
125 1         13 for my $schema_name ( keys %$schema ) {
126 6         10 my @path_fields;
127 6         9 for my $property_name ( keys %{ $schema->{$schema_name}{properties} } ) {
  6         44  
128 28         55 my $prop = $schema->{$schema_name}{properties}{$property_name};
129             # ; use Data::Dumper;
130             # ; say "Checking prop $property_name: " . Dumper $prop;
131 28 100 100     81 if ( is_type( $prop->{type}, 'string' ) && $prop->{format} && $prop->{format} eq 'filepath' ) {
      100        
132 1         4 push @path_fields, $property_name;
133             }
134             }
135              
136             # ; say "Got path fields: @path_fields";
137 6 100       18 next if !@path_fields;
138              
139             # Fetch the rows with values in the path, slowly so that we
140             # don't try to take up all the memory in the database
141 1         2 my $per_page = 50;
142 1         3 my $i = 0;
143 1         5 my $file_root = $self->file_root;
144 1         10 my $url_root = $self->url_root;
145 1         13 my $items = $backend->list( $schema_name, {}, { limit => $per_page } );
146 1         7 while ( $i < $items->{total} ) {
147 1         3 for my $item ( @{ $items->{items} } ) {
  1         3  
148 1         3 for my $field ( @path_fields ) {
149             # Add to linked records
150 1         2 my $path = $item->{ $field };
151 1         27 $path =~ s{^$url_root}{$file_root};
152 1         14 $linked{ $path }++;
153             }
154             }
155 1         2 $i += @{ $items->{items} };
  1         3  
156 1         6 $items = $backend->list( $schema_name, {}, { offset => $i, limit => $per_page } );
157             }
158             }
159              
160             # Any file that does not have a link must be deleted
161 1         8 delete $files{ $_ } for keys %linked;
162             # ; use Data::Dumper;
163             # ; say "Linked: " . Dumper [ keys %linked ];
164             # ; say "Deleting: " . Dumper [ keys %files ];
165 1         5 for my $path ( keys %files ) {
166 1         5 path( $path )->dirname->dirname->dirname->remove_tree;
167             }
168              
169 1         1048 return;
170             }
171              
172             sub _digest_file {
173 4     4   11 my ( $self, $asset ) = @_;
174             # Using hex instead of base64 to support case-insensitive file
175             # systems
176 4         15 my $digest = Digest->new( $self->digest_type )->add( $asset->slurp )->hexdigest;
177 4         1392 return $digest;
178             }
179              
180             1;
181              
182             __END__