| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Yancy::Plugin::File; | 
| 2 |  |  |  |  |  |  | our $VERSION = '1.086'; | 
| 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 |  | 13739 | use Mojo::Base 'Mojolicious::Plugin'; | 
|  | 19 |  |  |  |  | 49 |  | 
|  | 19 |  |  |  |  | 135 |  | 
| 55 | 19 |  |  | 19 |  | 3543 | use Yancy::Util qw( currym is_type ); | 
|  | 19 |  |  |  |  | 39 |  | 
|  | 19 |  |  |  |  | 1030 |  | 
| 56 | 19 |  |  | 19 |  | 5526 | use Digest; | 
|  | 19 |  |  |  |  | 6716 |  | 
|  | 19 |  |  |  |  | 571 |  | 
| 57 | 19 |  |  | 19 |  | 144 | use Mojo::Asset::File; | 
|  | 19 |  |  |  |  | 53 |  | 
|  | 19 |  |  |  |  | 187 |  | 
| 58 | 19 |  |  | 19 |  | 652 | use Mojo::File qw( path ); | 
|  | 19 |  |  |  |  | 42 |  | 
|  | 19 |  |  |  |  | 20865 |  | 
| 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 | 485 | my ( $self, $app, $config ) = @_; | 
| 67 | 52 | 100 |  |  |  | 317 | my $file_root = $config->{file_root} ? path( $config->{file_root} ) : $app->home->child( 'public/uploads' ); | 
| 68 | 52 |  |  |  |  | 1438 | $self->file_root( $file_root ); | 
| 69 | 52 |  | 100 |  |  | 661 | my $url_root = $config->{url_root} // '/uploads'; | 
| 70 | 52 |  |  |  |  | 227 | $self->url_root( $url_root ); | 
| 71 | 52 |  | 50 |  |  | 535 | my $moniker = $config->{moniker} // 'file'; | 
| 72 | 52 |  |  |  |  | 197 | $self->moniker( $moniker ); | 
| 73 | 52 |  |  | 7 |  | 580 | $app->helper( 'yancy.' . $moniker, sub { $self } ); | 
|  | 7 |  |  |  |  | 26326 |  | 
| 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 | 13 | my ( $self, $name, $asset ) = @_; | 
| 89 | 4 | 100 |  |  |  | 31 | if ( ref $name eq 'Mojo::Upload' ) { | 
| 90 | 3 |  |  |  |  | 14 | $asset = $name->asset; | 
| 91 | 3 |  |  |  |  | 25 | $name = $name->filename; | 
| 92 |  |  |  |  |  |  | } | 
| 93 | 4 |  |  |  |  | 21 | my $digest = $self->_digest_file( $asset ); | 
| 94 | 4 |  |  |  |  | 38 | my @path_parts = grep $_, split /(..)/, $digest, 3; | 
| 95 | 4 |  |  |  |  | 21 | my $root = $self->file_root; | 
| 96 | 4 |  |  |  |  | 33 | my $path = $root->child( @path_parts )->make_path; | 
| 97 | 4 |  |  |  |  | 1246 | my $file_path = $path->child( $name ); | 
| 98 | 4 |  |  |  |  | 112 | $file_path->spurt( $asset->slurp ); | 
| 99 | 4 |  |  |  |  | 807 | 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 | 5 | my ( $self, $backend, $schema ) = @_; | 
| 114 | 1 |  | 33 |  |  | 9 | $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 |  |  |  |  | 2 | my ( %files, %linked ); | 
| 118 |  |  |  |  |  |  |  | 
| 119 |  |  |  |  |  |  | # List all the files | 
| 120 | 1 |  |  |  |  | 6 | for my $path ( $self->file_root->list_tree->each ) { | 
| 121 | 2 |  |  |  |  | 990 | $files{ $path }++; | 
| 122 |  |  |  |  |  |  | } | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | # Find all the linked files | 
| 125 | 1 |  |  |  |  | 11 | for my $schema_name ( keys %$schema ) { | 
| 126 | 6 |  |  |  |  | 10 | my @path_fields; | 
| 127 | 6 |  |  |  |  | 7 | for my $property_name ( keys %{ $schema->{$schema_name}{properties} } ) { | 
|  | 6 |  |  |  |  | 27 |  | 
| 128 | 28 |  |  |  |  | 53 | my $prop = $schema->{$schema_name}{properties}{$property_name}; | 
| 129 |  |  |  |  |  |  | # ; use Data::Dumper; | 
| 130 |  |  |  |  |  |  | # ; say "Checking prop $property_name: " . Dumper $prop; | 
| 131 | 28 | 100 | 100 |  |  | 61 | 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 |  |  |  |  | 2 | my $i = 0; | 
| 143 | 1 |  |  |  |  | 4 | my $file_root = $self->file_root; | 
| 144 | 1 |  |  |  |  | 9 | my $url_root = $self->url_root; | 
| 145 | 1 |  |  |  |  | 12 | my $items = $backend->list( $schema_name, {}, { limit => $per_page } ); | 
| 146 | 1 |  |  |  |  | 6 | while ( $i < $items->{total} ) { | 
| 147 | 1 |  |  |  |  | 4 | 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 |  |  |  |  | 25 | $path =~ s{^$url_root}{$file_root}; | 
| 152 | 1 |  |  |  |  | 14 | $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 |  |  |  |  | 7 | 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 |  |  |  |  | 4 | path( $path )->dirname->dirname->dirname->remove_tree; | 
| 167 |  |  |  |  |  |  | } | 
| 168 |  |  |  |  |  |  |  | 
| 169 | 1 |  |  |  |  | 916 | 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 |  |  |  |  | 13 | my $digest = Digest->new( $self->digest_type )->add( $asset->slurp )->hexdigest; | 
| 177 | 4 |  |  |  |  | 804 | return $digest; | 
| 178 |  |  |  |  |  |  | } | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | 1; | 
| 181 |  |  |  |  |  |  |  | 
| 182 |  |  |  |  |  |  | __END__ |