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__ |