File Coverage

blib/lib/AnyEvent/FTP/Server/Context/Memory.pm
Criterion Covered Total %
statement 178 189 94.1
branch 61 70 87.1
condition 10 18 55.5
subroutine 34 34 100.0
pod 3 25 12.0
total 286 336 85.1


line stmt bran cond sub pod time code
1             package AnyEvent::FTP::Server::Context::Memory;
2              
3 16     16   7998 use strict;
  16         41  
  16         618  
4 16     16   90 use warnings;
  16         41  
  16         465  
5 16     16   430 use 5.010;
  16         60  
6 16     16   134 use Moo;
  16         37  
  16         134  
7 16     16   6637 use Path::Class::File;
  16         17707  
  16         742  
8 16     16   144 use Path::Class::Dir;
  16         43  
  16         41426  
9              
10             extends 'AnyEvent::FTP::Server::Context';
11              
12             # ABSTRACT: FTP Server client context class with full read/write access
13             our $VERSION = '0.17'; # VERSION
14              
15              
16             with 'AnyEvent::FTP::Server::Role::Auth';
17             with 'AnyEvent::FTP::Server::Role::Help';
18             with 'AnyEvent::FTP::Server::Role::Old';
19             with 'AnyEvent::FTP::Server::Role::Type';
20             with 'AnyEvent::FTP::Server::Role::TransferPrep';
21              
22              
23             sub store
24             {
25             # The store for this class is global.
26             # if you wanted each connection or user
27             # to have their own store you could subclass
28             # and redefine the store method as apropriate
29 58     58 1 1098 state $store = {};
30 58         137 $store;
31             }
32              
33              
34             has cwd => (
35             is => 'rw',
36             default => sub {
37             Path::Class::Dir->new_foreign('Unix', '/');
38             },
39             );
40              
41              
42             sub _first_index (&@)
43             {
44 41     41   80 my $f = shift;
45 41         141 foreach my $i ( 0 .. $#_ )
46             {
47 122         310 local *_ = \$_[$i];
48 122 100       223 return $i if $f->();
49             }
50 33         79 return -1;
51             }
52              
53             sub find
54             {
55 49     49 1 1970 my($self, $path) = @_;
56 49 100       223 $path = Path::Class::Dir->new_foreign('Unix', $path) unless ref $path;
57 49 100       2483 $path = Path::Class::Dir->new_foreign('Unix', $self->cwd, $path)
58             unless $path->is_absolute;
59              
60 49         4228 my $store = $self->store;
61              
62 49 100       192 return $store if $path eq '/';
63              
64 27         592 my @list = $path->components;
65              
66 27         374 while(1)
67             {
68 28     75   171 my $i = _first_index { $_ eq '..' } @list;
  75         345  
69 28 100       149 last if $i == -1;
70 1 50       6 if($i > 1)
71             {
72 0         0 splice @list, $i-1, 2;
73             }
74             else
75             {
76 1         4 splice @list, $i, 1;
77             }
78             }
79              
80 27         57 shift @list; # shift off the root
81 27         61 my $top = pop @list;
82              
83 27         71 foreach my $part (@list)
84             {
85 19 50 33     123 if(exists($store->{$part}) && ref($store->{$part}) eq 'HASH')
86             {
87 19         56 $store = $store->{$part};
88             }
89             else
90             {
91 0         0 return;
92             }
93             }
94              
95 27 100       109 if(exists $store->{$top})
96 18         89 { return $store->{$top} }
97             else
98 9         54 { return }
99             }
100              
101              
102             sub rename_from
103             {
104 9     9 1 26 my($self, $value) = @_;
105 9 100       24 $self->{rename_from} = $value if defined $value;
106 9         19 $self->{rename_from};
107             }
108              
109              
110 2     2 0 9 sub help_cwd { 'CWD pathname' }
111              
112             sub cmd_cwd
113             {
114 6     6 0 13 my($self, $con, $req) = @_;
115              
116 6         16 my $dir = Path::Class::Dir->new_foreign('Unix', $req->args)->cleanup;
117 6 100       1241 $dir = $dir->absolute($self->cwd) unless $dir->is_absolute;
118              
119 6         612 my @list = grep !/^\.$/, $dir->components;
120              
121 6         90 while(1)
122             {
123 13     47   47 my $i = _first_index { $_ eq '..' } @list;
  47         123  
124 13 100       38 last if $i == -1;
125 7 100       18 if($i > 1)
126             {
127 4         10 splice @list, $i-1, 2;
128             }
129             else
130             {
131 3         6 splice @list, $i, 1;
132             }
133             }
134              
135              
136 6         25 $dir = Path::Class::Dir->new_foreign('Unix', @list);
137              
138 6 100       911 if(ref($self->find($dir)) eq 'HASH')
139             {
140 4         20 $self->cwd($dir);
141 4         15 $con->send_response(250 => 'CWD command successful');
142             }
143             else
144             {
145 2         9 $con->send_response(550 => 'CWD error');
146             }
147              
148 6         24 $self->done;
149             }
150              
151              
152 2     2 0 11 sub help_cdup { 'CDUP' }
153              
154             sub cmd_cdup
155             {
156 3     3 0 9 my($self, $con, $req) = @_;
157              
158 3         18 my $dir = $self->cwd->parent;
159              
160 3 100       317 if(ref($self->find($dir)) eq 'HASH')
161             {
162 2         41 $self->cwd($dir);
163 2         9 $con->send_response(250 => 'CDUP command successful');
164             }
165             else
166             {
167 1         5 $con->send_response(550 => 'CDUP error');
168             }
169              
170 3         18 $self->done;
171             }
172              
173              
174 2     2 0 9 sub help_pwd { 'PWD' }
175              
176             sub cmd_pwd
177             {
178 2     2 0 5 my($self, $con, $req) = @_;
179              
180 2         8 my $cwd = $self->cwd;
181 2         12 $con->send_response(257 => "\"$cwd\" is the current directory");
182 2         10 $self->done;
183             }
184              
185              
186 2     2 0 10 sub help_size { 'SIZE pathname' }
187              
188             sub cmd_size
189             {
190 3     3 0 8 my($self, $con, $req) = @_;
191              
192 3         28 my $file = $self->find(Path::Class::File->new_foreign('Unix', $req->args));
193              
194 3 100 100     39 if(defined($file) && !ref($file))
    100          
195             {
196 1         6 $con->send_response(213 => length $file);
197             }
198             elsif(defined $file)
199             {
200 1         9 $con->send_response(550 => $req->args . ": not a regular file");
201             }
202             else
203             {
204 1         6 $con->send_response(550 => $req->args . ": No such file or directory");
205             }
206              
207 3         14 $self->done;
208             }
209              
210              
211 2     2 0 114 sub help_mkd { 'MKD pathname' }
212              
213             sub cmd_mkd
214             {
215 6     6 0 14 my($self, $con, $req) = @_;
216              
217 6         15 my $path = Path::Class::Dir->new_foreign('Unix', $req->args);
218 6         815 my $file = $self->find($path->parent);
219 6 100 66     120 if($path->basename ne '' && defined($file) && ref($file) eq 'HASH')
      66        
220             {
221 5 100       51 if(exists $file->{$path->basename})
222             {
223 3         17 $con->send_response(521 => "\"$path\" directory exists");
224             }
225             else
226             {
227 2         30 $file->{$path->basename} = {};
228 2         14 $con->send_response(257 => "\"$path\" new directory created");
229             }
230             }
231             else
232             {
233 1         9 $con->send_response(550 => "MKD error");
234             }
235 6         26 $self->done;
236             }
237              
238              
239 2     2 0 9 sub help_rmd { 'RMD pathname' }
240              
241             sub cmd_rmd
242             {
243 4     4 0 81 my($self, $con, $req) = @_;
244              
245             # TODO: be more picky about rmd and file or dele a directory
246 4         17 my $path = Path::Class::Dir->new_foreign('Unix', $req->args);
247 4         849 my $file = $self->find($path->parent);
248 4 50 33     102 if(defined($file) && ref($file) eq 'HASH')
249             {
250 4 100       17 if(exists $file->{$path->basename})
251             {
252 2         16 delete $file->{$path->basename};
253 2         39 $con->send_response(250 => "RMD command successful");
254             }
255             else
256             {
257 2         14 $con->send_response(550 => "$path: No such file or directory");
258             }
259             }
260             else
261             {
262 0         0 $con->send_response(550 => "$path: No such file or directory");
263             }
264 4         27 $self->done;
265              
266             }
267              
268              
269 2     2 0 10 sub help_dele { 'DELE pathname' }
270              
271             sub cmd_dele
272             {
273 4     4 0 10 my($self, $con, $req) = @_;
274              
275 4         12 my $path = Path::Class::File->new_foreign('Unix', $req->args);
276 4         646 my $file = $self->find($path->parent);
277 4 50 33     99 if(defined($file) && ref($file) eq 'HASH')
278             {
279 4 100       16 if(exists $file->{$path->basename})
280             {
281 2         16 delete $file->{$path->basename};
282 2         14 $con->send_response(250 => "File removed");
283             }
284             else
285             {
286 2         16 $con->send_response(550 => "$path: No such file or directory");
287             }
288             }
289             else
290             {
291 0         0 $con->send_response(550 => "$path: No such file or directory");
292             }
293 4         19 $self->done;
294             }
295              
296              
297 2     2 0 9 sub help_rnfr { 'RNFR pathname' }
298              
299             sub cmd_rnfr
300             {
301 8     8 0 27 my($self, $con, $req) = @_;
302              
303 8         22 my $path = Path::Class::File->new_foreign('Unix', $req->args);
304 8         1533 my $dir = $self->find($path->parent);
305 8 100       52 if(ref($dir) eq 'HASH')
306             {
307 7 100       23 if(exists $dir->{$path->basename})
308             {
309 4         23 $self->rename_from([$dir,$path->basename]);
310 4         15 $con->send_response(350 => 'File or directory exists, ready for destination name');
311             }
312             else
313             {
314 3         23 $con->send_response(550 => 'No such file or directory');
315             }
316             }
317             else
318             {
319 1         4 $con->send_response(550 => 'No such file or directory');
320             }
321              
322 8         30 $self->done;
323             }
324              
325              
326 2     2 0 9 sub help_rnto { 'RNTO pathname' }
327              
328             sub cmd_rnto
329             {
330 5     5 0 11 my($self, $con, $req) = @_;
331              
332 5         14 my $from = $self->rename_from;
333              
334 5 100       12 unless(defined $from)
335             {
336 1         4 $con->send_response(503 => 'Bad sequence of commands');
337 1         6 $self->done;
338 1         3 return;
339             }
340              
341 4         13 my $path = Path::Class::File->new_foreign('Unix', $req->args);
342 4         718 my $dir = $self->find($path->parent);
343              
344 4 100       28 if(ref($dir) eq 'HASH')
345             {
346 3 100       11 if(exists $dir->{$path->basename})
347             {
348 1         8 $con->send_response(550 => 'File already exists');
349             }
350             else
351             {
352 2         16 $dir->{$path->basename} = delete $from->[0]->{$from->[1]};
353 2         13 $con->send_response(250 => 'Rename successful');
354             }
355             }
356             else
357             {
358 1         5 $con->send_response(550 => 'Rename failed');
359             }
360 4         17 $self->done;
361             }
362              
363              
364 2     2 0 11 sub help_stat { 'STAT [ pathname]' }
365              
366             sub cmd_stat
367             {
368 6     6 0 15 my($self, $con, $req) = @_;
369              
370 6         17 my $file = $self->find($req->args);
371              
372 6 100       53 if(defined $file)
373             {
374 4 100       14 if(ref($file) eq 'HASH')
375             {
376 3         14 $con->send_response(211 => "It's a directory");
377             }
378             else
379             {
380 1         6 $con->send_response(211 => "It's a file");
381             }
382             }
383             else
384             {
385 2         10 $con->send_response(450 => 'No such file or directory');
386             }
387 6         31 $self->done;
388             }
389              
390              
391 2     2 0 9 sub help_nlst { 'NLST [ (pathname)]' }
392              
393             sub cmd_nlst
394             {
395 5     5 0 14 my($self, $con, $req) = @_;
396              
397 5         17 my $dir = $req->args;
398              
399 5 50       28 unless(defined $self->data)
400             {
401 0         0 $con->send_response(425 => 'Unable to build data connection');
402 0         0 return;
403             }
404              
405 5         13 eval {
406 5         25 $con->send_response(150 => "Opening ASCII mode data connection for file list");
407 5         14 my @list;
408 5 100       20 if($dir)
409             {
410 4         21 my $h = $self->find($dir);
411 4 100       48 if(ref($h) eq 'HASH')
412             {
413 2         13 $dir = Path::Class::Dir->new_foreign('Unix', $dir);
414 2         381 @list = map { $dir->file($_) } sort keys %$h;
  6         473  
415             }
416             else
417             {
418 2         42 $dir = Path::Class::File->new_foreign('Unix', $dir);
419 2         629 @list = "$dir";
420             }
421             }
422             else
423             {
424 1         8 my $h = $self->find($self->cwd);
425 1 50       19 die 'unable to find cwd' unless defined $h;
426 1         18 @list = sort keys %$h;
427             }
428 5         263 $self->data->push_write(join '', map { $_ . "\015\012" } @list);
  11         560  
429 5         293 $self->data->push_shutdown;
430 5         148 $con->send_response(226 => 'Transfer complete');
431             };
432 5 50       33 if(my $error = $@)
433             {
434 0         0 warn $error;
435 0 0       0 if(eval { $error->can('errno') })
  0         0  
436 0         0 { $con->send_response(550 => $error->errno) }
437             else
438 0         0 { $con->send_response(550 => 'Internal error') }
439             };
440 5         34 $self->clear_data;
441 5         23 $self->done;
442             }
443              
444             1;
445              
446              
447             # TODO: cmd_retr
448             # TODO: cmd_list
449             # TODO: cmd_stor
450             # TODO: cmd_appe
451             # TODO: cmd_stou
452              
453             __END__