File Coverage

blib/lib/Mojolicious/Plugin/StaticShare.pm
Criterion Covered Total %
statement 57 95 60.0
branch 15 56 26.7
condition 6 52 11.5
subroutine 11 17 64.7
pod 1 3 33.3
total 90 223 40.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::StaticShare;
2 2     2   70131 use Mojo::Base 'Mojolicious::Plugin';
  2         194597  
  2         14  
3 2     2   1761 use Mojo::File qw(path);
  2         31631  
  2         118  
4 2     2   528 use Mojolicious::Types;
  2         1073  
  2         18  
5 2     2   555 use Mojo::Path;
  2         2066  
  2         18  
6 2     2   60 use Mojo::Util;# qw(decode);
  2         6  
  2         89  
7 2     2   13 use Scalar::Util 'weaken';
  2         5  
  2         5805  
8              
9             my $PKG = __PACKAGE__;
10              
11             has qw(app) => undef, weak => 1;
12             has qw(config);# => undef, weak => 1;
13             has root_url => sub { Mojo::Path->new(shift->config->{root_url})->leading_slash(1)->trailing_slash(1) };
14             has root_dir => sub { Mojo::Path->new(shift->config->{root_dir} // '.')->trailing_slash(1) };
15             has host => sub { shift->config->{host} };
16             has admin_pass => sub { shift->config->{admin_pass} };
17             has access => sub { shift->config->{access} };
18             has public_uploads => sub { !! shift->config->{public_uploads} };
19             has render_dir => sub { shift->config->{render_dir} };
20             has dir_index => sub { shift->config->{dir_index} // [qw(README.md INDEX.md README.pod INDEX.pod)] };
21             has render_pod => sub { shift->config->{render_pod} };
22             has render_markdown => sub { shift->config->{render_markdown} };
23             has markdown_pkg => sub { shift->config->{markdown_pkg} // 'Text::Markdown::Hoedown' };
24             has templates_dir => sub { shift->config->{templates_dir} };
25             has markdown => sub {# parser object
26             __internal__::Markdown->new(shift->markdown_pkg);
27             };
28             has re_markdown => sub { qr{[.]m(?:d(?:own)?|kdn?|arkdown)$}i };
29             has re_pod => sub { qr{[.]p(?:od|m|l)$} };
30             has re_html => sub { qr{[.]html?$} };
31             has mime => sub { Mojolicious::Types->new };
32             has max_upload_size => sub { shift->config->{max_upload_size} };
33             #separate routes store for matching without conflicts
34             has routes => sub { Mojolicious::Routes->new }; # для max_upload_size
35             has routes_names => sub {# тоже для max_upload_size
36             my $self = shift;
37             return [$self." ROOT GET (#1)", $self." PATH GET (#2)", $self." ROOT POST (#3)", $self." PATH POST (#4)"];
38             };
39             has debug => sub { shift->config->{debug} // $ENV{StaticShare_DEBUG} };
40              
41              
42             sub register {# none magic
43 1     1 1 91 my ($self, $app, $args) = @_;
44 1         4 $self->config($args);
45 1         12 $self->app($app);
46            
47 1         13 my $push_class = "$PKG\::Templates";
48 1         5 my $push_path = path(__FILE__)->sibling('StaticShare')->child('static');
49            
50             require Mojolicious::Plugin::StaticShare::Templates
51 1         9 and push @{$app->renderer->classes}, grep($_ eq $push_class, @{$app->renderer->classes}) ? () : $push_class
  1         23  
52 1 50 33     163 and push @{$app->static->paths}, grep($_ eq $push_path, @{$app->static->paths}) ? () : $push_path
  1 50 33     20  
  1 50 50     17  
      0        
      33        
53             unless ($self->render_dir // '') eq 0
54             && ($self->render_markdown // '') eq 0;
55 1 0       18 push @{$app->renderer->paths}, ref $self->templates_dir ? @{$self->templates_dir} : $self->templates_dir
  0 50       0  
  0         0  
56             if $self->templates_dir;
57            
58             # ROUTING 4 routes
59 1         9 my $route = $self->root_url->clone->merge('*pth');#"$args->{root_url}/*pth";
60 1         303 my $names = $self->routes_names;
61 1         8 $self->_make_route(routes => $app->routes, action => 'get', path => $self->root_url->to_route, name => $names->[0], pth => '', host => $self->host, )
62             ->_make_route(routes => $app->routes, action => 'get', path => $route->to_route, name => $names->[1], host => $self->host,)
63             ->_make_route(routes => $app->routes, action => 'post', path => $self->root_url->to_route, name => $names->[2], pth => '', host => $self->host,)
64             ->_make_route(routes => $app->routes, action => 'post', path => $route->to_route, name => $names->[3], host => $self->host,);
65             #~ $r->get($self->root_url->to_route)->to(namespace=>$PKG, controller=>"Controller", action=>'get', pth=>'', plugin=>$self, )->name($names->[0]);#$PKG
66             #~ $r->get($route->to_route)->to(namespace=>$PKG, controller=>"Controller", action=>'get', plugin=>$self, )->name($names->[1]);#;
67             #~ $r->post($self->root_url->to_route)->to(namespace=>$PKG, controller=>"Controller", action=>'post', pth=>'', plugin=>$self, )->name($names->[2]);#->name("$PKG ROOT POST");
68             #~ $r->post($route->to_route)->to(namespace=>$PKG, controller=>"Controller", action=>'post', plugin=>$self, )->name($names->[3]);#;
69            
70            
71             # only POST uploads
72 1 50       4 $self->_make_route(routes => $self->routes, action => 'post', path => $self->root_url->to_route, name => $names->[2], pth => '')# без host!
73             ->_make_route(routes => $self->routes, action => 'post', path => $route->to_route, name => $names->[3])
74             if defined $self->max_upload_size;
75            
76             path($self->config->{root_dir})->make_path
77 1 50 33     9 unless !$self->config->{root_dir} || -e $self->config->{root_dir};
78              
79 1         12 $app->helper(i18n => \&i18n);
80             #~ $app->helper(StaticShareIsAdmin => sub { $self->is_admin(@_) });
81            
82             #POD
83             $self->app->plugin(PODRenderer => {no_perldoc => 1})
84 1 50 0     160 unless $self->app->renderer->helpers->{'pod_to_html'} && ($self->render_pod // '') eq 0 ;
      33        
85            
86             # PATCH EMIT CHUNK
87 1 50       7 $self->_hook_chunk()
88             if defined $self->max_upload_size;
89            
90 1         13 return ($app, $self);
91             }
92              
93             sub _make_route {
94 4     4   156 my ($self) = shift;
95 4 50       20 my $arg = ref $_[0] ? shift : {@_};
96 4         15 weaken $self;
97 4         8 my $action = $arg->{action};
98 4         32 my $r = $arg->{routes}->$action($arg->{path});
99 4 100       1003 $r->to(namespace=>$PKG, controller=>"Controller", action=>$arg->{action}, defined $arg->{pth} ? (pth=>$arg->{pth}) : (), plugin=>$self, );
100 4         137 $r->name($arg->{name});
101             $r->requires(host => $arg->{host})
102 4 50       41 if $arg->{host};
103 4         18 return $self;
104             }
105              
106             my %loc = (
107             'ru-ru'=>{
108             'Not found'=>"Не найдено",
109             'Error on path'=>"Ошибка в",
110             'Error'=>"Ошибка",
111             'Permission denied'=>"Нет доступа",
112             'Cant open directory'=>"Нет доступа в папку",
113             'Share'=>'Обзор',
114             'Edit'=>'Редактировать',
115             'Index of'=>'Содержание',
116             'Dirs'=>'Папки',
117             'Files'=>'Файлы',
118             'Name'=>'Название файла',
119             'Size'=>'Размер',
120             'Last Modified'=>'Дата изменения',
121             'Up'=>'Выше',
122             'Down'=>'Ниже',
123             'Add uploads'=>'Добавить файлы',
124             'Add dir'=>'Добавить папку',
125             'root'=>"корень",
126             'Uploading'=>'Загружается',
127             'file is too big'=>'слишком большой файл',
128             'path is not directory'=>"нет такого каталога/папки",
129             'file already exists' => "такой файл уже есть",
130             'new dir name'=>"имя новой папки",
131             'Confirm to delete these files'=>"Подтвердите удаление этих файлов",
132             'Confirm to delete these dirs'=>"Подтвердите удаление этих папок",
133             'I AM SURE'=>"ДА",
134             'Save'=> 'Сохранить',
135             'Success saved' => "Успешно сохранено",
136             'you cant delete'=>"Удаление запрещено, обратитесь к админу",
137             'you cant upload'=>"Запрещено, обратитесь к админу",
138             'you cant create dir'=>"Создание папки запрещено, обратитесь к админу",
139             'you cant rename'=>"Переименование запрещено, обратитесь к админу",
140             'you cant edit' => "Редактирование запрещено, обратитесь к админу",
141             },
142             );
143             sub i18n {# helper
144 15     15 0 49999 my ($c, $str, $lang) = @_;
145             #~ $lang //= $c->stash('language');
146 15 50       60 return $str
147             unless $c->stash('language');
148 15         161 my $loc;
149 15         36 for ($c->stash('language')->languages) {
150 15 50       316 return $str
151             if /en/;
152 0 0 0     0 $loc = $loc{$_} || $loc{lc $_} || $loc{lc "$_-$_"}
153             and last;
154             }
155 0 0 0     0 return $loc->{$str} || $loc->{lc $str} || $str
156             if $loc;
157 0         0 return $str;
158             }
159              
160             sub is_admin {# as helper
161 1     1 0 8 my ($self, $c) = @_;
162             return
163 1 50       20 unless my $pass = $self->admin_pass;
164 0           my $sess = $c->session;
165 0 0 0       $sess->{StaticShare}{admin} = 1
166             if $c->param('admin') && $c->param('admin') eq $pass;
167 0   0       return $sess->{StaticShare} && $sess->{StaticShare}{admin};
168             }
169              
170             my $patched;
171             sub _hook_chunk {
172 0     0     my $self = shift;
173            
174 0 0         _patch_emit_chunk() unless $patched++;
175            
176 0           weaken $self;
177             $self->app->hook(after_build_tx => sub {
178 0     0     my ($tx, $app) = @_;
179             $tx->once('chunk'=>sub {
180 0           my ($tx, $chunk) = @_;
181 0 0         return unless $tx->req->method =~ /PUT|POST/;
182 0           my $url = $tx->req->url->to_abs;
183 0           my $match = Mojolicious::Routes::Match->new(root => $self->routes);
184             #~ weaken $tx;
185             #~ weaken $app;
186             #~ Mojolicious::Controller->new(app=>$app, tx=>$tx)
187 0           my $host = $self->host;
188 0           $match->find(undef, {method => $tx->req->method, path => $url->path->to_route});# websocket => $ws
189 0 0 0       $app->log->debug("TX ONCE CHUNK check path: [".$url->path->to_route."]",
    0          
190             "match route: ". (($match->endpoint && $match->endpoint->name) || 'none'),
191             $host ? ("match host: ". $tx->req->headers->host =~ /$host/) : () #, Mojo::Util::dumper($url)
192             ) if $self->debug;
193 0   0       my $route = $match->endpoint
194             || return;
195            
196 0 0 0       $tx->req->max_message_size($self->max_upload_size)
      0        
      0        
197             if (($route->name eq $self->routes_names->[2]) || ($route->name eq $self->routes_names->[3]))
198             && $host && $tx->req->headers->host =~ /$host/; # Хост ловится только так, в $url нет(
199             # TODO admin session
200            
201 0           });
202 0           });
203            
204             }
205              
206             sub _patch_emit_chunk {
207            
208             Mojo::Util::monkey_patch 'Mojo::Transaction::HTTP', 'server_read' => sub {
209 0     0     my ($self, $chunk) = @_;
210            
211 0           $self->emit('chunk before req parse'=>$chunk);### только одна строка патча этот эмит
212            
213             # Parse request
214 0           my $req = $self->req;
215 0 0         $req->parse($chunk) unless $req->error;
216            
217 0           $self->emit('chunk'=>$chunk);### только одна строка патча этот эмит
218            
219             # Generate response
220 0 0 0       $self->emit('request') if $req->is_finished && !$self->{handled}++;
221            
222 0     0     };
223            
224             }
225              
226             our $VERSION = '0.075';
227              
228             ##############################################
229             package __internal__::Markdown;
230             sub new {
231 0     0     my $class = shift;
232 0           my $pkg = shift;
233             return
234 0 0         unless eval "require $pkg; 1";#
235             #~ $pkg->import
236             #~ if $pkg->can('import');
237 0 0 0       return $pkg->new()
238             if $pkg->can('new') && $pkg->can('parse');
239             return
240 0 0         unless $pkg->can('markdown');
241 0           bless {pkg=>$pkg} => $class;
242             }
243              
244 2     2   18 sub parse { my $self = shift; no strict 'refs'; ($self->{pkg}.'::markdown')->(@_); }
  2     0   6  
  2         255  
  0            
  0            
245              
246              
247             =pod
248              
249             =encoding utf8
250              
251             Доброго всем
252              
253             =head1 Mojolicious::Plugin::StaticShare
254              
255             ¡ ¡ ¡ ALL GLORY TO GLORIA ! ! !
256              
257             =head1 NAME
258              
259             Mojolicious::Plugin::StaticShare - browse, upload, copy, move, delete, edit, rename static files and dirs.
260              
261             =head1 VERSION
262              
263             0.075
264              
265             =head1 SYNOPSIS
266              
267             # Mojolicious
268             $app->plugin('StaticShare', );
269              
270             # Mojolicious::Lite
271             plugin 'StaticShare', ;
272            
273             # oneliner
274             $ perl -MMojolicious::Lite -E 'plugin("StaticShare", root_url=>"/my/share",)->secrets([rand])->start' daemon
275              
276             L also.
277              
278             =head1 DESCRIPTION
279              
280             This plugin allow to share static files/dirs/markdown and has public and admin functionality:
281              
282             =head2 Public interface
283              
284             Can browse and upload files if name not exists.
285              
286             =head2 Admin interface
287              
288             Can copy, move, delete, rename and edit content of files/dirs.
289              
290             Append param C<< admin= option >> to any url inside B requests (see below).
291              
292             =head1 OPTIONS
293              
294             =head2 host
295              
296             host => 'local', # mean =~ /local/
297             host => qr/fobar\.com$/,
298              
299             String or regexp C< qr// >. Allow routing for given host.
300              
301             =head2 root_dir
302              
303             Absolute or relative file system path root directory. Defaults to '.'.
304              
305             root_dir => '/mnt/usb',
306             root_dir => 'foo',
307              
308             =head2 root_url
309              
310             This prefix to url path. Defaults to '/'.
311              
312             root_url => '/', # mean route '/*pth'
313             root_url => '', # mean also route '/*pth'
314             root_url => '/my/share', # mean route '/my/share/*pth'
315              
316             See L.
317              
318             =head2 admin_pass
319              
320             Admin password (be sure https) for admin tasks. None defaults.
321              
322             admin_pass => '$%^!!9nes--', #
323              
324             Sign into admin interface by pass param B to shares paths C< https://<...>?admin=$%^!!9nes-- >
325              
326             =head2 render_dir
327              
328             Template path, format, handler, etc which render directory index. Defaults to builtin things.
329              
330             render_dir => 'foo/dir_index',
331             render_dir => {template => 'foo/my_directory_index', foo=>...},
332             # Disable directory index rendering
333             render_dir => 0,
334              
335             =head3 Usefull stash variables
336              
337             Plugin make any stash variables for rendering: C, C, C, C, C, C, C
338              
339             =head4 pth
340              
341             Path of request exept C option, as L object.
342              
343             =head4 url_path
344              
345             Path of request with C option, as L object.
346              
347             =head4 language
348              
349             Req header AcceptLanguage as L object.
350              
351             =head4 dirs
352              
353             List of scalars dirnames. Not sorted.
354              
355             =head4 files
356              
357             List of hashrefs (C keys) files. Not sorted.
358              
359             =head4 index
360              
361             Filename for markdown or pod rendering in page below the column dirs and column files.
362              
363             =head2 templates_dir
364              
365             String or arrayref strings. Simply C<< push @{$app->renderer->paths}, ; >>. None defaults.
366              
367             Mainly needs for layouting markdown. When you set this option then you can define layout inside markdown/pod files like syntax:
368              
369             % layouts/foo.html.ep
370             # Foo header
371              
372             =head2 render_markdown
373              
374             Same as B but for markdown files. Defaults to builtin things.
375              
376             render_markdown => 'foo/markdown',
377             render_markdown => {template => 'foo/markdown', foo=>...},
378             # Disable markdown rendering
379             render_markdown => 0,
380              
381             =head2 markdown_pkg
382              
383             Module name for render markdown. Must contains sub C or method C. Defaults to L.
384              
385             markdown_pkg => 'Foo::Markup';
386              
387             Does not need to install if C<< render_markdown => 0 >> or never render md files.
388              
389             =head2 render_pod
390              
391             Template path, format, handler, etc which render pod files. Defaults to builtin things.
392              
393             render_pod=>'foo/pod',
394             render_pod => {template => 'foo/pod', layout=>'pod', foo=>...},
395             # Disable pod rendering
396             render_pod => 0,
397              
398             =head2 dir_index
399              
400             Arrayref to match files to include to directory index page. Defaults to C<< [qw(README.md INDEX.md README.pod INDEX.pod)] >>.
401              
402             dir_index => [qw(DIR.md)],
403             dir_index => 0, # disable include markdown to index dir page
404              
405             =head2 public_uploads
406              
407             Boolean to disable/enable uploads for public users. Defaults to undef (disable).
408              
409             public_uploads=>1, # enable
410              
411             =head2 max_upload_size
412              
413             max_upload_size=>0, # unlimited POST
414              
415             Numeric value limiting uploads size for route. Defaults to Mojolicious setting.
416             EXPIRIMENTAL patching of the L
417             for emit B event.
418              
419             See also L, L.
420              
421             =head1 Extended MARKDOWN & POD
422              
423             You can place attributes like:
424              
425             =head2 id (# as prefix)
426              
427             =head2 classnames (dot as prefix and separator)
428              
429             =head2 css-style rules (key:value; colon separator and semicolon terminator)
430              
431             to markup elements as below.
432              
433             In MARKDOWN:
434              
435             # {#foo123 .class1 .class2 padding: 0 0.5rem;} Header 1
436             {.brown-text} brown paragraph text ...
437              
438             In POD:
439              
440             =head2 {.class1.blue-text border-bottom: 1px dotted;} Header 2
441            
442             {.red-text} red color text...
443              
444             =head1 METHODS
445              
446             L inherits all methods from
447             parent L and implements the following new ones.
448              
449             =head2 register
450              
451             $plugin->register(Mojolicious->new);
452              
453             Register plugin in L application.
454              
455             =head1 MULTI PLUGIN
456              
457             Yep, possible:
458              
459             # Mojolicious
460             $app->config(...)
461             ->plugin('StaticShare', ...)
462             ->plugin('StaticShare', ...); # and so on ...
463            
464             # Mojolicious::Lite
465             app->config(...)
466             ->plugin('StaticShare', ...)
467             ->plugin('StaticShare', ...) # and so on ...
468              
469              
470             =head1 UTF-8
471              
472             Everywhere and everything: module, files, content.
473              
474             =head1 WINDOWS OS
475              
476             It was not tested but I hope you dont worry and have happy.
477              
478             =head1 SEE ALSO
479              
480             L
481              
482             L, L, L.
483              
484             =head1 AUTHOR
485              
486             Михаил Че (Mikhail Che), C<< >>
487              
488             =head1 BUGS / CONTRIBUTING
489              
490             Please report any bugs or feature requests at L. Pull requests also welcome.
491              
492             =head1 COPYRIGHT
493              
494             Copyright 2017+ Mikhail Che.
495              
496             This library is free software; you can redistribute it and/or modify
497             it under the same terms as Perl itself.
498              
499             =cut