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   66223 use Mojo::Base 'Mojolicious::Plugin';
  2         189831  
  2         16  
3 2     2   1824 use Mojo::File qw(path);
  2         30017  
  2         133  
4 2     2   512 use Mojolicious::Types;
  2         938  
  2         16  
5 2     2   530 use Mojo::Path;
  2         1810  
  2         17  
6 2     2   74 use Mojo::Util;# qw(decode);
  2         6  
  2         88  
7 2     2   13 use Scalar::Util 'weaken';
  2         6  
  2         5339  
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 98 my ($self, $app, $args) = @_;
44 1         5 $self->config($args);
45 1         12 $self->app($app);
46            
47 1         14 my $push_class = "$PKG\::Templates";
48 1         7 my $push_path = path(__FILE__)->sibling('StaticShare')->child('static');
49            
50             require Mojolicious::Plugin::StaticShare::Templates
51 1         12 and push @{$app->renderer->classes}, grep($_ eq $push_class, @{$app->renderer->classes}) ? () : $push_class
  1         22  
52 1 50 33     171 and push @{$app->static->paths}, grep($_ eq $push_path, @{$app->static->paths}) ? () : $push_path
  1 50 33     20  
  1 50 50     20  
      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         10 my $route = $self->root_url->clone->merge('*pth');#"$args->{root_url}/*pth";
60 1         334 my $names = $self->routes_names;
61 1         6 $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       5 $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     11 unless !$self->config->{root_dir} || -e $self->config->{root_dir};
78              
79 1         16 $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     185 unless $self->app->renderer->helpers->{'pod_to_html'} && ($self->render_pod // '') eq 0 ;
      33        
85            
86             # PATCH EMIT CHUNK
87 1 50       12 $self->_hook_chunk()
88             if defined $self->max_upload_size;
89            
90 1         18 return ($app, $self);
91             }
92              
93             sub _make_route {
94 4     4   187 my ($self) = shift;
95 4 50       24 my $arg = ref $_[0] ? shift : {@_};
96 4         16 weaken $self;
97 4         9 my $action = $arg->{action};
98 4         28 my $r = $arg->{routes}->$action($arg->{path});
99 4 100       1114 $r->to(namespace=>$PKG, controller=>"Controller", action=>$arg->{action}, defined $arg->{pth} ? (pth=>$arg->{pth}) : (), plugin=>$self, );
100 4         133 $r->name($arg->{name});
101             $r->over(host => $arg->{host})
102 4 50       41 if $arg->{host};
103 4         20 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 51037 my ($c, $str, $lang) = @_;
145             #~ $lang //= $c->stash('language');
146 15 50       50 return $str
147             unless $c->stash('language');
148 15         176 my $loc;
149 15         42 for ($c->stash('language')->languages) {
150 15 50       350 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 9 my ($self, $c) = @_;
162             return
163 1 50       24 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 route: [".$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             # eq $self->routes_names->[1] || $route->name eq $self->routes_names->[3]);#Mojo::Util::dumper($url);, length($chunk)
197 0 0 0       $tx->req->max_message_size($self->max_upload_size)
      0        
      0        
198             if (($route->name eq $self->routes_names->[2]) || ($route->name eq $self->routes_names->[3]))
199             && $host && $tx->req->headers->host =~ /$host/;
200             # TODO admin session
201            
202 0           });
203 0           });
204            
205             }
206              
207             sub _patch_emit_chunk {
208            
209             Mojo::Util::monkey_patch 'Mojo::Transaction::HTTP', 'server_read' => sub {
210 0     0     my ($self, $chunk) = @_;
211            
212 0           $self->emit('chunk before req parse'=>$chunk);### только одна строка патча этот эмит
213            
214             # Parse request
215 0           my $req = $self->req;
216 0 0         $req->parse($chunk) unless $req->error;
217            
218 0           $self->emit('chunk'=>$chunk);### только одна строка патча этот эмит
219            
220             # Generate response
221 0 0 0       $self->emit('request') if $req->is_finished && !$self->{handled}++;
222            
223 0     0     };
224            
225             }
226              
227             our $VERSION = '0.074';
228              
229             ##############################################
230             package __internal__::Markdown;
231             sub new {
232 0     0     my $class = shift;
233 0           my $pkg = shift;
234             return
235 0 0         unless eval "require $pkg; 1";#
236             #~ $pkg->import
237             #~ if $pkg->can('import');
238 0 0 0       return $pkg->new()
239             if $pkg->can('new') && $pkg->can('parse');
240             return
241 0 0         unless $pkg->can('markdown');
242 0           bless {pkg=>$pkg} => $class;
243             }
244              
245 2     2   19 sub parse { my $self = shift; no strict 'refs'; ($self->{pkg}.'::markdown')->(@_); }
  2     0   5  
  2         251  
  0            
  0            
246              
247              
248             =pod
249              
250             =encoding utf8
251              
252             Доброго всем
253              
254             =head1 Mojolicious::Plugin::StaticShare
255              
256             ¡ ¡ ¡ ALL GLORY TO GLORIA ! ! !
257              
258             =head1 NAME
259              
260             Mojolicious::Plugin::StaticShare - browse, upload, copy, move, delete, edit, rename static files and dirs.
261              
262             =head1 VERSION
263              
264             0.074
265              
266             =head1 SYNOPSIS
267              
268             # Mojolicious
269             $app->plugin('StaticShare', );
270              
271             # Mojolicious::Lite
272             plugin 'StaticShare', ;
273            
274             # oneliner
275             $ perl -MMojolicious::Lite -E 'plugin("StaticShare", root_url=>"/my/share",)->secrets([rand])->start' daemon
276              
277             L also.
278              
279             =head1 DESCRIPTION
280              
281             This plugin allow to share static files/dirs/markdown and has public and admin functionality:
282              
283             =head2 Public interface
284              
285             Can browse and upload files if name not exists.
286              
287             =head2 Admin interface
288              
289             Can copy, move, delete, rename and edit content of files/dirs.
290              
291             Append param C<< admin= option >> to any url inside B requests (see below).
292              
293             =head1 OPTIONS
294              
295             =head2 root_dir
296              
297             Absolute or relative file system path root directory. Defaults to '.'.
298              
299             root_dir => '/mnt/usb',
300             root_dir => 'foo',
301              
302             =head2 root_url
303              
304             This prefix to url path. Defaults to '/'.
305              
306             root_url => '/', # mean route '/*pth'
307             root_url => '', # mean also route '/*pth'
308             root_url => '/my/share', # mean route '/my/share/*pth'
309              
310             See L.
311              
312             =head2 admin_pass
313              
314             Admin password (be sure https) for admin tasks. None defaults.
315              
316             admin_pass => '$%^!!9nes--', #
317              
318             Signin to admin interface C< https://myhost/my/share/foo/bar?admin=$%^!!9nes-- >
319              
320             =head2 render_dir
321              
322             Template path, format, handler, etc which render directory index. Defaults to builtin things.
323              
324             render_dir => 'foo/dir_index',
325             render_dir => {template => 'foo/my_directory_index', foo=>...},
326             # Disable directory index rendering
327             render_dir => 0,
328              
329             =head3 Usefull stash variables
330              
331             Plugin make any stash variables for rendering: C, C, C, C, C, C, C
332              
333             =head4 pth
334              
335             Path of request exept C option, as L object.
336              
337             =head4 url_path
338              
339             Path of request with C option, as L object.
340              
341             =head4 language
342              
343             Req header AcceptLanguage as L object.
344              
345             =head4 dirs
346              
347             List of scalars dirnames. Not sorted.
348              
349             =head4 files
350              
351             List of hashrefs (C keys) files. Not sorted.
352              
353             =head4 index
354              
355             Filename for markdown or pod rendering in page below the column dirs and column files.
356              
357             =head2 templates_dir
358              
359             String or arrayref strings. Simply C<< push @{$app->renderer->paths}, ; >>. None defaults.
360              
361             Mainly needs for layouting markdown. When you set this option then you can define layout inside markdown/pod files like syntax:
362              
363             % layouts/foo.html.ep
364             # Foo header
365              
366             =head2 render_markdown
367              
368             Same as B but for markdown files. Defaults to builtin things.
369              
370             render_markdown => 'foo/markdown',
371             render_markdown => {template => 'foo/markdown', foo=>...},
372             # Disable markdown rendering
373             render_markdown => 0,
374              
375             =head2 markdown_pkg
376              
377             Module name for render markdown. Must contains sub C or method C. Defaults to L.
378              
379             markdown_pkg => 'Foo::Markup';
380              
381             Does not need to install if C<< render_markdown => 0 >> or never render md files.
382              
383             =head2 render_pod
384              
385             Template path, format, handler, etc which render pod files. Defaults to builtin things.
386              
387             render_pod=>'foo/pod',
388             render_pod => {template => 'foo/pod', layout=>'pod', foo=>...},
389             # Disable pod rendering
390             render_pod => 0,
391              
392             =head2 dir_index
393              
394             Arrayref to match files to include to directory index page. Defaults to C<< [qw(README.md INDEX.md README.pod INDEX.pod)] >>.
395              
396             dir_index => [qw(DIR.md)],
397             dir_index => 0, # disable include markdown to index dir page
398              
399             =head2 public_uploads
400              
401             Boolean to disable/enable uploads for public users. Defaults to undef (disable).
402              
403             public_uploads=>1, # enable
404              
405             =head2 max_upload_size
406              
407             max_upload_size=>0, # unlimited POST
408              
409             Numeric value limiting uploads size for route.
410             WARN-EXPIRIMENTAL patching of L
411             for emit chunk event.
412              
413             See also L, L.
414              
415             =head1 Extended markdown & pod
416              
417             You can place attributes like:
418              
419             =head2 id (# as prefix)
420              
421             =head2 classnames (dot as prefix and separator)
422              
423             =head2 css-style rules (key:value; colon separator and semicolon terminator)
424              
425             to markup elements as below.
426              
427             In markdown:
428              
429             # {#foo123 .class1 .class2 padding: 0 0.5rem;} Header 1
430             {.brown-text} brown paragraph text ...
431              
432             In pod:
433              
434             =head2 {.class1.blue-text border-bottom: 1px dotted;} Header 2
435            
436             {.red-text} red color text...
437              
438             =head1 METHODS
439              
440             L inherits all methods from
441             L and implements the following new ones.
442              
443             =head2 register
444              
445             $plugin->register(Mojolicious->new);
446              
447             Register plugin in L application.
448              
449             =head1 MULTI PLUGIN
450              
451             A possible:
452              
453             # Mojolicious
454             $app->plugin('StaticShare', )
455             ->plugin('StaticShare', ); # and so on ...
456            
457             # Mojolicious::Lite
458             app->config(...)
459             ->plugin('StaticShare', )
460             ->plugin('StaticShare', ) # and so on ...
461             ...
462              
463             =head1 UTF-8
464              
465             Everywhere and everything: module, files, content.
466              
467             =head1 WINDOWS OS
468              
469             It was not tested but I hope you dont worry and have happy.
470              
471             =head1 SEE ALSO
472              
473             L
474              
475             L, L, L.
476              
477             =head1 AUTHOR
478              
479             Михаил Че (Mikhail Che), C<< >>
480              
481             =head1 BUGS / CONTRIBUTING
482              
483             Please report any bugs or feature requests at L. Pull requests also welcome.
484              
485             =head1 COPYRIGHT
486              
487             Copyright 2017+ Mikhail Che.
488              
489             This library is free software; you can redistribute it and/or modify
490             it under the same terms as Perl itself.
491              
492             =cut