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.088';
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   15025 use Mojo::Base 'Mojolicious::Plugin';
  19         46  
  19         144  
55 19     19   3676 use Yancy::Util qw( currym is_type );
  19         45  
  19         1094  
56 19     19   5767 use Digest;
  19         6875  
  19         627  
57 19     19   152 use Mojo::Asset::File;
  19         50  
  19         178  
58 19     19   670 use Mojo::File qw( path );
  19         44  
  19         21509  
59              
60             has file_root =>;
61             has url_root =>;
62             has digest_type => 'SHA-1';
63             has moniker => 'file';
64              
65             sub register {
66 53     53 1 492 my ( $self, $app, $config ) = @_;
67 53 100       347 my $file_root = $config->{file_root} ? path( $config->{file_root} ) : $app->home->child( 'public/uploads' );
68 53         1507 $self->file_root( $file_root );
69 53   100     686 my $url_root = $config->{url_root} // '/uploads';
70 53         230 $self->url_root( $url_root );
71 53   50     558 my $moniker = $config->{moniker} // 'file';
72 53         194 $self->moniker( $moniker );
73 53     7   620 $app->helper( 'yancy.' . $moniker, sub { $self } );
  7         30324  
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 14 my ( $self, $name, $asset ) = @_;
89 4 100       20 if ( ref $name eq 'Mojo::Upload' ) {
90 3         17 $asset = $name->asset;
91 3         24 $name = $name->filename;
92             }
93 4         25 my $digest = $self->_digest_file( $asset );
94 4         45 my @path_parts = grep $_, split /(..)/, $digest, 3;
95 4         21 my $root = $self->file_root;
96 4         44 my $path = $root->child( @path_parts )->make_path;
97 4         1413 my $file_path = $path->child( $name );
98 4         104 $file_path->spurt( $asset->slurp );
99 4         895 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 4 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         3 my ( %files, %linked );
118              
119             # List all the files
120 1         5 for my $path ( $self->file_root->list_tree->each ) {
121 2         1036 $files{ $path }++;
122             }
123              
124             # Find all the linked files
125 1         12 for my $schema_name ( keys %$schema ) {
126 6         9 my @path_fields;
127 6         8 for my $property_name ( keys %{ $schema->{$schema_name}{properties} } ) {
  6         29  
128 28         50 my $prop = $schema->{$schema_name}{properties}{$property_name};
129             # ; use Data::Dumper;
130             # ; say "Checking prop $property_name: " . Dumper $prop;
131 28 100 100     65 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         4 my $per_page = 50;
142 1         3 my $i = 0;
143 1         6 my $file_root = $self->file_root;
144 1         9 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         4  
148 1         2 for my $field ( @path_fields ) {
149             # Add to linked records
150 1         3 my $path = $item->{ $field };
151 1         28 $path =~ s{^$url_root}{$file_root};
152 1         15 $linked{ $path }++;
153             }
154             }
155 1         3 $i += @{ $items->{items} };
  1         3  
156 1         7 $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         4 for my $path ( keys %files ) {
166 1         4 path( $path )->dirname->dirname->dirname->remove_tree;
167             }
168              
169 1         931 return;
170             }
171              
172             sub _digest_file {
173 4     4   13 my ( $self, $asset ) = @_;
174             # Using hex instead of base64 to support case-insensitive file
175             # systems
176 4         19 my $digest = Digest->new( $self->digest_type )->add( $asset->slurp )->hexdigest;
177 4         1550 return $digest;
178             }
179              
180             1;
181              
182             __END__