File Coverage

blib/lib/Dancer/Plugin/Dropbox.pm
Criterion Covered Total %
statement 117 134 87.3
branch 49 66 74.2
condition 17 33 51.5
subroutine 16 16 100.0
pod n/a
total 199 249 79.9


line stmt bran cond sub pod time code
1             package Dancer::Plugin::Dropbox;
2              
3 3     3   756819 use 5.010001;
  3         10  
  3         135  
4 3     3   16 use strict;
  3         6  
  3         87  
5 3     3   15 use warnings;
  3         16  
  3         85  
6 3     3   3616 use Dancer ':syntax';
  3         340999  
  3         15  
7 3     3   3847 use Dancer::Plugin;
  3         4442  
  3         244  
8 3     3   673 use File::Spec::Functions qw/catfile catdir splitdir/;
  3         742  
  3         201  
9 3     3   1823 use Dancer::Plugin::Dropbox::AutoIndex qw/autoindex/;
  3         11  
  3         5947  
10              
11             =head1 NAME
12              
13             Dancer::Plugin::Dropbox - Dancer plugin for a dropbox-like applications.
14              
15             =head1 VERSION
16              
17             Version 0.00002
18              
19             B
20             development and testing>. You have been warned.
21              
22              
23             =cut
24              
25             our $VERSION = '0.00002';
26              
27              
28             =head1 SYNOPSIS
29              
30             In the config:
31              
32             plugins:
33             Dropbox:
34             basedir: 'dropbox-data'
35             template: 'dropbox-listing'
36             token: index
37             autocreate_root: 1
38            
39             In your route:
40              
41             get '/dropbox/*/' => sub {
42             my ($user) = splat;
43             return dropbox_send_file($user, "/");
44             };
45            
46             get '/dropbox/*/**' => sub {
47             my ($user, $filepath) = splat;
48             return dropbox_send_file($user, $filepath);
49             };
50            
51             post '/dropbox/*/**' => \&manage_uploads;
52             post '/dropbox/*/' => \&manage_uploads;
53            
54             sub manage_uploads {
55             my ($user, $filepath) = splat;
56             if (my $uploaded = upload('upload_file')) {
57             warning dropbox_upload_file($user, $filepath, $uploaded);
58            
59             }
60             elsif (my $dirname = param("newdirname")) {
61             dropbox_create_directory($user, $filepath, $dirname);
62             }
63             elsif (my $deletion = param("filedelete")) {
64             dropbox_delete_file($user, $filepath, $deletion);
65             }
66             return redirect request->path;
67             }
68            
69            
70             =head2 Configuration
71              
72             The configuration keys are as follows:
73              
74             =over 4
75              
76             =item basedir
77              
78             The directory which will be the root of the dropbox users. The
79             directory must already exist. Defaults to "dropbox-datadir" in the
80             application directory.
81              
82             =item template
83              
84             The template to use. If not present, a minimal embedded template will
85             be used.
86              
87             =item layout
88              
89             The layout to use (defaults to C
).
90              
91             =item token
92              
93             The token of your template to use (defaults to C) for the
94             directory listing.
95              
96             =item autocreate_root
97              
98             If set to a true value, the root for the each user will be created on
99             the first "GET" request, e.g. C
100              
101             Please note that the dropbox file will be left in a subdirectory of
102             the basedir named with the username, so if you permit usernames with
103             "/" or "\" or ".." inside the name, the user will never reach its
104             files, effectively cutting it out.
105              
106             =back
107              
108              
109             =head2 Exported keywords
110              
111             =head3 dropbox_send_file ($user, $filepath, \%template_tokens, \%listing_params)
112              
113             This keyword accepts a list of positional arguments or a single hash
114             reference. If the given filename exists, it sends it to the client. If
115             it's a directory, a directory listing is returned.
116              
117             The first argument is the dropbox user, which is also the subdirectory
118             of the dropbox directory.
119              
120             The second argument is the path of the file, as a single string or as
121             a arrayref, the same you could get from a Dancer's megasplat (C<**>).
122             If not provided, it will return the root of the user.
123              
124             The third argument is an hashref with the template tokens for the
125             directory listing. This will be used only if the path points to a
126             directory and ignored otherwise. The configuration file should specify
127             at least the template to use.
128              
129             plugins:
130             Dropbox:
131             basedir: 'dropbox-data'
132             template: 'dropbox-listing'
133             token: index
134            
135              
136             The fourth argument is an hashref for the autoindex function. See
137             L for details.
138              
139             The directory listing will set the template token specified in the
140             configuration file under C.
141              
142             The alternate syntax using a hashref is the following:
143              
144             dropbox_send_file {
145             user => $username,
146             filepath => $filepath,
147             template_tokens => \%template_tokens,
148             listing_params => \%listing_params,
149             };
150              
151             =head3 dropbox_ajax_listing ( $user, $path )
152              
153             Return a hashref with a single key, the real system path file, and
154             with the value set to the L
155             arrayref for the directory $path and user $user.
156              
157             Retur , or undef if it doesn't exist or it is not a directory.
158              
159             =cut
160              
161             sub dropbox_ajax_listing {
162 3     3   25309 my ($self, @args) = plugin_args(@_);
163 3         23 my ($user, $filepath) = @args;
164 3 50       18 if (!defined $filepath) {
165 0         0 $filepath = "/";
166             }
167 3         13 my $file = _dropbox_get_filename($user, $filepath);
168 3 50       10 return unless $file;
169 3 100       90 return unless -d $file;
170 2         12 return { $file => autoindex($file) };
171             }
172              
173              
174             sub dropbox_send_file {
175 12     12   139534 my ($self, @args) = plugin_args(@_);
176             # Dancer::Logger::debug(to_dumper(\@args));
177              
178 12         89 my ($user, $filepath, $template_tokens, $listing_params);
179             # only one parameter and it's an hashref
180 12 50 33     80 if (@args == 1 and (ref($args[0]) eq 'HASH')) {
181 0         0 my $argsref = shift @args;
182 0         0 $user = $argsref->{user};
183 0         0 $filepath = $argsref->{filepath};
184 0         0 $template_tokens = $argsref->{template_tokens};
185 0         0 $listing_params = $argsref->{listing_params};
186             }
187             else {
188 12         32 ($user, $filepath, $template_tokens, $listing_params) = @args;
189             }
190 12   50     95 $template_tokens ||= {};
191 12   50     47 $filepath ||= '/';
192 12   50     63 $listing_params ||= {};
193              
194             # be sure to have the root directory created. $user is sane, as
195             # it's been passed by the route with authentication.
196              
197 12         51 my $file = _dropbox_get_filename($user, $filepath);
198              
199 12 100       38 unless ($file) {
200 2         15 send_error("Bad request", 403);
201             }
202              
203 10         68 Dancer::Logger::debug("Trying to serve $file");
204            
205             # check if exists
206 10 100       1600 unless (-e $file) {
207 3         195 return send_error("File not found", 404);
208             }
209              
210             # check if it's a file and send it
211 7 100       111 if (-f $file) {
212 2         12 return send_file($file, system_path => 1);
213             }
214              
215             # is it a directory?
216 5 50       95 if (-d $file) {
217             # for now just return the html
218 5         24 Dancer::Logger::debug("Creating autoindex for $file");
219 5         247 my $listing = autoindex($file, %$listing_params);
220             # Dancer::Logger::debug(to_dumper($listing));
221 5         22 my $template = plugin_setting->{template};
222 5   50     114 my $layout = plugin_setting->{layout} || "main";
223 5   50     98 my $token = plugin_setting->{token} || "listing";
224 5 50       90 if ($template) {
225 0         0 return template $template => {
226             $token => $listing,
227             %$template_tokens,
228             }, { layout => $layout };
229             }
230             else {
231 5         18 return _render_index($listing);
232             }
233             }
234             # if it's not a dir, return 404
235 0         0 return send_error("File not found", 404);
236             }
237              
238             =head3 dropbox_upload_file($user, $filepath, $fileuploaded)
239              
240             This keyword manage the uploading of a file.
241              
242             The first argument is the dropbox user, used as root directory.
243              
244             The second argument is the desired path, a directory which must
245             exists.
246              
247             The third argument is the L object which you
248             can get with C.
249              
250             It returns true in case of success, false otherwise.
251              
252             =cut
253              
254             sub dropbox_upload_file {
255 2     2   32134 my ($self, $user, $filepath, $uploaded) = plugin_args(@_);
256 2         16 my $target = _dropbox_get_filename($user, $filepath);
257 2 50 33     48 unless ($target and -d $target) {
258 0         0 Dancer::Logger::warning "$target is not a directory";
259 0         0 return;
260             }
261 2 50       7 return unless $uploaded;
262              
263 2         13 my $basename = $uploaded->basename;
264 2         154 Dancer::Logger::debug("Uploading $basename");
265              
266             # we use _check_root to be sure it's a decent filename, with no \ or /
267 2 50       113 unless (_check_root($basename)) {
268 0         0 Dancer::Logger::warning("bad filename");
269 0         0 return;
270             }
271              
272             # find the target file
273 2         10 my $targetfile = catfile($target, $basename);
274 2         14 Dancer::Logger::info("copying the file to $targetfile");
275              
276             # copy and return the return value
277 2         100 return $uploaded->copy_to($targetfile)
278             }
279              
280             =head3 dropbox_create_directory($user, $filepath, $dirname);
281              
282             The keyword creates a new directory on the top of an existing dropbox
283             directory.
284              
285             The first argument is the user the directory belongs to in the dropbox
286             application.
287              
288             The second argument is the path where the directory should be created.
289             This is usually retrieved from the route against which the user posts
290             the request. The directory must already exist.
291              
292             The third argument is the desired new name. It should constitute a
293             single directory, so no directory separator is allowed.
294              
295             It returns true on success, false otherwise.
296              
297             =cut
298              
299             sub dropbox_create_directory {
300 2     2   7951 my ($self, $user, $filepath, $dirname) = plugin_args(@_);
301 2         19 my $target = _dropbox_get_filename($user, $filepath);
302              
303             # the post must happen against a directory
304 2 50 33     38 return unless ($target and -d $target);
305              
306             # we can't create a directory over an existing file
307 2 50       20 return if (-e $dirname);
308 2 100       5 return unless _check_root($dirname);
309 1         9 Dancer::Logger::info("Trying to create $dirname in $target");
310 1         54 my $dir_to_create = catdir($target, $dirname);
311 1         13656 return mkdir($dir_to_create, 0700);
312             }
313              
314              
315             =head3 dropbox_delete_file($user, $filepath, $filename);
316              
317             The keyword deletes a file or an empty directory belonging to an
318             existing dropbox directory.
319              
320             The first argument is the dropbox user.
321              
322             The second argument is the parent directory of the target file. This
323             is usually retrieved from the route against which the user posts the
324             request.
325              
326             The third argument is the target to delete. No directory separator is
327             allowed here.
328              
329             It returns true on success, false otherwise.
330              
331             Internally, it uses C on files and C on directories.
332              
333              
334             =cut
335              
336              
337             sub dropbox_delete_file {
338 8     8   50016 my ($self, $user, $filepath, $filename) = plugin_args(@_);
339 8         65 my $target = _dropbox_get_filename($user, $filepath);
340 8 100 66     186 return unless ($target and -e $target);
341 7 100       17 return unless _check_root($filename);
342 6         60 Dancer::Logger::info("Requested deletion:" . catfile($target, $filename));
343 6         433 my $file_to_delete = catfile($target, $filename);
344 6 100       215 if (-f $file_to_delete) {
    100          
345 3         399 return unlink($file_to_delete);
346             }
347             elsif (-d $file_to_delete) {
348 2         178 return rmdir($file_to_delete);
349             }
350             return
351 1         5 }
352              
353             sub _dropbox_get_filename {
354 27     27   68 my ($user, $filepath) = @_;
355              
356             # if the filepath is not provided, use the root
357 27   100     129 $filepath ||= "/";
358 27   33     109 my $basedir = plugin_setting->{basedir} ||
359             catdir(config->{appdir}, "dropbox-datadir");
360              
361             # if the app runs without a $basedir, die
362 27 50       1307 die "$basedir doesn't exist or is not a directory\n" unless -d $basedir;
363              
364 27 100 66     164 unless ($user && _check_root($user)) {
365 2         7 return undef;
366             }
367              
368 25         56 my $user_root = catdir($basedir, _get_sane_path($user));
369 25 100       763 unless (-d $user_root) {
370 2 50       10 if (plugin_setting->{autocreate_root}) {
371 0         0 Dancer::Logger::info("Autocreating root dir for $user: " .
372             "$user_root");
373 0 0       0 mkdir($user_root, 0700) or die "Couldn't create $user_root $!";
374             }
375             else {
376 2         56 Dancer::Logger::warning("Directory for $user does not exist and " .
377             "settings prevent its creation.");
378             }
379             }
380              
381             # if the app required this path
382              
383             # get the desired path
384 25         135 my @path;
385 25 100       102 if (ref($filepath) eq 'ARRAY') {
    50          
386 9         30 @path = @$filepath;
387             }
388             elsif (ref($filepath) eq '') {
389             # it's a single piece, so use that
390 16         101 @path = split(/[\/\\]/, $filepath);
391             }
392             else {
393 0         0 die "Wrong usage! the second argument should be an arrayref or a string\n";
394             }
395              
396 25         74 my $file = catfile($basedir, _get_sane_path($user, @path));
397 25         121 return $file;
398             }
399              
400              
401              
402             sub _get_sane_path {
403 88     88   217 my @pathdirs = @_;
404 88         103 my @realdir;
405              
406             # loop over the dirs and search ".."
407 88         152 foreach my $dir (@pathdirs) {
408 118 100       1257 next if $dir =~ m![\\/\0]!; # just to avoid bad names
409              
410 116 100       584 if ($dir eq ".") {
    100          
    50          
411             # do nothing
412             }
413              
414             # the tricky case
415             elsif ($dir eq "..") {
416 17 100       46 if (@realdir) {
417 1         3 pop @realdir;
418             }
419             }
420              
421             # we check with splitdir if the directory can be splat further
422             # with the hosting OS logic
423             elsif (splitdir($dir) == 1) {
424 98         774 push @realdir, $dir;
425             }
426             else {
427             # bad chunk, ignore
428 0         0 next;
429             }
430             }
431 88         488 return @realdir;
432             }
433              
434             # given that the username is the root directory, we want to be on the
435             # safe side. See if _get_sane_path returns exactly the argument passed.
436              
437              
438             sub _check_root {
439 38     38   75 my $username = shift;
440 38         95 my ($root) = _get_sane_path($username);
441 38 100 66     193 if ($root and $root eq $username) {
442 34         149 return 1
443             } else {
444 4         19 return 0
445             }
446             }
447              
448              
449             # if a template able to handle the arrayref with the listing, we just
450             # provide a really simple one.
451              
452             sub _render_index {
453 5     5   9 my $listing = shift;
454 5         14 my @out = (qq{Directory Listing}); }; }; ";
NameLast ModifiedSize
455 5         13 foreach my $f (@$listing) {
456 11         50 push @out, qq{
$f->{name}$f->{mod_time}$f->{size}
457 11 50       135 if ($f->{error}) {
458 0         0 push @out, qq{$f->{error}
459             }
460 11         35 push @out, "
461             }
462 5         10 push @out, "
";
463 5         83 return join("", @out);
464             }
465              
466              
467             register dropbox_send_file => \&dropbox_send_file;
468             register dropbox_ajax_listing => \&dropbox_ajax_listing;
469             register dropbox_upload_file => \&dropbox_upload_file;
470             register dropbox_create_directory => \&dropbox_create_directory;
471             register dropbox_delete_file => \&dropbox_delete_file;
472             register_plugin;
473              
474             =head1 AUTHOR
475              
476             Marco Pessotto, C<< >>
477              
478             =head1 BUGS
479              
480             Please report any bugs or feature requests to C, or through
481             the web interface at L. I will be notified, and then you'll
482             automatically be notified of progress on your bug as I make changes.
483              
484             =head1 SUPPORT
485              
486             You can find documentation for this module with the perldoc command.
487              
488             perldoc Dancer::Plugin::Dropbox
489              
490              
491             You can also look for information at:
492              
493             =over 4
494              
495             =item * RT: CPAN's request tracker (report bugs here)
496              
497             L
498              
499             =item * AnnoCPAN: Annotated CPAN documentation
500              
501             L
502              
503             =item * CPAN Ratings
504              
505             L
506              
507             =item * Search CPAN
508              
509             L
510              
511             =back
512              
513              
514             =head1 ACKNOWLEDGEMENTS
515              
516             Thanks to Stefan Hornburg (Racke) C for the initial
517             code, ideas and support.
518              
519             =head1 LICENSE AND COPYRIGHT
520              
521             Copyright 2013 Marco Pessotto.
522              
523             This program is free software; you can redistribute it and/or modify it
524             under the terms of the the Artistic License (2.0). You may obtain a
525             copy of the full license at:
526              
527             L
528              
529             Any use, modification, and distribution of the Standard or Modified
530             Versions is governed by this Artistic License. By using, modifying or
531             distributing the Package, you accept this license. Do not use, modify,
532             or distribute the Package, if you do not accept this license.
533              
534             If your Modified Version has been derived from a Modified Version made
535             by someone other than you, you are nevertheless required to ensure that
536             your Modified Version complies with the requirements of this license.
537              
538             This license does not grant you the right to use any trademark, service
539             mark, tradename, or logo of the Copyright Holder.
540              
541             This license includes the non-exclusive, worldwide, free-of-charge
542             patent license to make, have made, use, offer to sell, sell, import and
543             otherwise transfer the Package with respect to any patent claims
544             licensable by the Copyright Holder that are necessarily infringed by the
545             Package. If you institute patent litigation (including a cross-claim or
546             counterclaim) against any party alleging that the Package constitutes
547             direct or contributory patent infringement, then this Artistic License
548             to you shall terminate on the date that such litigation is filed.
549              
550             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
551             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
552             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
553             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
554             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
555             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
556             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
557             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
558              
559              
560             =cut
561              
562             1; # End of Dancer::Plugin::Dropbox
563              
564             # Local Variables:
565             # tab-width: 8
566             # End:
567