File Coverage

blib/lib/File/Dropbox.pm
Criterion Covered Total %
statement 73 283 25.8
branch 8 138 5.8
condition 7 74 9.4
subroutine 22 36 61.1
pod 8 8 100.0
total 118 539 21.8


line stmt bran cond sub pod time code
1             package File::Dropbox;
2 12     12   194112 use strict;
  12         18  
  12         275  
3 12     12   39 use warnings;
  12         13  
  12         255  
4 12     12   38 use feature ':5.10';
  12         16  
  12         1117  
5 12     12   42 use base qw{ Tie::Handle Exporter };
  12         14  
  12         5081  
6 12     12   21275 use Symbol;
  12         7978  
  12         762  
7 12     12   6673 use JSON;
  12         125464  
  12         41  
8 12     12   2183 use Errno qw{ ENOENT EISDIR EINVAL EPERM EACCES EAGAIN ECANCELED EFBIG };
  12         2190  
  12         737  
9 12     12   70 use Fcntl qw{ SEEK_CUR SEEK_SET SEEK_END };
  12         12  
  12         439  
10 12     12   4539 use Furl;
  12         204552  
  12         332  
11 12     12   8176 use IO::Socket::SSL;
  12         675552  
  12         83  
12 12     12   7761 use Net::DNS::Lite;
  12         144558  
  12         18570  
13              
14             our $VERSION = 0.7;
15             our @EXPORT_OK = qw{ contents metadata putfile movefile copyfile createfolder deletefile };
16              
17             my $hosts = {
18             content => 'api-content.dropbox.com',
19             api => 'api.dropbox.com',
20             };
21              
22             my $version = 1;
23              
24             my $header1 = join ', ',
25             'OAuth oauth_version="1.0"',
26             'oauth_signature_method="PLAINTEXT"',
27             'oauth_consumer_key="%s"',
28             'oauth_token="%s"',
29             'oauth_signature="%s&%s"';
30              
31             my $header2 = 'Bearer %s';
32              
33             sub new {
34 13     13 1 906 my $self = Symbol::gensym;
35 13         246 tie *$self, __PACKAGE__, my $this = { @_[1 .. @_ - 1] };
36              
37 12         22 *$self = $this;
38              
39 12         20 return $self;
40             } # new
41              
42             sub TIEHANDLE {
43 13   33 13   62 my $self = bless $_[1], ref $_[0] || $_[0];
44              
45 13   100     131 $self->{'chunk'} //= 4 << 20;
46 13   100     73 $self->{'root'} //= 'sandbox';
47              
48             die 'Unexpected root value'
49 13 100       97 unless $self->{'root'} =~ m{^(?:drop|sand)box$};
50              
51             $self->{'furl'} = Furl->new(
52             timeout => 10,
53             inet_aton => \&Net::DNS::Lite::inet_aton,
54             ssl_opts => {
55             SSL_verify_mode => SSL_VERIFY_PEER(),
56             },
57              
58 12   100     46 %{ $self->{'furlopts'} //= {} },
  12         117  
59             );
60              
61 12         582 $self->{'closed'} = 1;
62 12         23 $self->{'length'} = 0;
63 12         21 $self->{'position'} = 0;
64 12         25 $self->{'mode'} = '';
65 12         17 $self->{'buffer'} = '';
66              
67 12         146 return $self;
68             } # TIEHANDLE
69              
70             sub READ {
71 1     1   371 my ($self, undef, $length, $offset) = @_;
72              
73 1         2 undef $!;
74              
75             die 'Read is not supported on this handle'
76 1 50       11 if $self->{'mode'} ne '<';
77              
78 0 0 0     0 substr($_[1] //= '', $offset // 0) = '', return 0
      0        
79             if $self->EOF();
80              
81 0         0 my $furl = $self->{'furl'};
82              
83 0         0 my $url = 'https://';
84 0         0 $url .= join '/', $hosts->{'content'}, $version;
85 0         0 $url .= join '/', '/files', $self->{'root'}, $self->{'path'};
86              
87             my $response = $furl->get($url, [
88             Range => sprintf('bytes=%i-%i', $self->{'position'}, $self->{'position'} + ($length || 1)),
89              
90 0   0     0 @{ &__headers__ },
  0         0  
91             ]);
92              
93 0 0       0 return $self->__error__($response)
94             if $response->code != 206;
95              
96 0         0 my $meta = $response->header('X-Dropbox-Metadata');
97 0         0 my $bytes = $response->header('Content-Length');
98              
99 0 0       0 $self->{'position'} += $bytes > $length? $length : $bytes;
100              
101 0   0     0 substr($_[1] //= '', $offset // 0) = substr $response->content(), 0, $length;
      0        
102              
103 0         0 return $bytes;
104             } # READ
105              
106             sub READLINE {
107 1     1   363 my ($self) = @_;
108 1         1 my $length;
109              
110 1         2 undef $!;
111              
112             die 'Readline is not supported on this handle'
113 1 50       11 if $self->{'mode'} ne '<';
114              
115 0 0       0 if ($self->EOF()) {
116 0 0       0 return if wantarray;
117              
118             # Special case: slurp mode + scalar context + empty file
119             # return '' for first call and undef for subsequent
120             return ''
121 0 0 0     0 unless $self->{'eof'} or defined $/;
122              
123 0         0 $self->{'eof'} = 1;
124 0         0 return undef;
125             }
126              
127             {
128 0         0 $length = length $self->{'buffer'};
  0         0  
129              
130 0 0 0     0 if (not wantarray and $length and defined $/) {
      0        
131 0         0 my $position = index $self->{'buffer'}, $/;
132              
133 0 0       0 if (~$position) {
134 0         0 $self->{'position'} += ($position += length $/);
135 0         0 return substr $self->{'buffer'}, 0, $position, '';
136             }
137             }
138              
139 0         0 local $self->{'position'} = $self->{'position'} + $length;
140              
141 0         0 my $bytes = $self->READ($self->{'buffer'}, $self->{'chunk'}, $length);
142              
143 0 0       0 return if $!;
144 0 0 0     0 redo if not $length or $bytes;
145             }
146              
147 0         0 $length = length $self->{'buffer'};
148              
149 0 0       0 if ($length) {
150             # Multiline
151 0 0 0     0 if (wantarray and defined $/) {
152 0         0 $self->{'position'} += $length;
153              
154 0         0 my ($position, $length) = (0, length $/);
155 0         0 my @lines;
156              
157 0         0 foreach ($self->{'buffer'}) {
158 0         0 while (~(my $offset = index $_, $/, $position)) {
159 0         0 $offset += $length;
160 0         0 push @lines, substr $_, $position, $offset - $position;
161 0         0 $position = $offset;
162             }
163              
164 0 0       0 push @lines, substr $_, $position
165             if $position < length;
166              
167 0         0 $_ = '';
168             }
169              
170 0         0 return @lines;
171             }
172              
173             # Slurp or last chunk
174 0         0 $self->{'position'} += $length;
175 0         0 return substr $self->{'buffer'}, 0, $length, '';
176             }
177              
178 0         0 return undef;
179             } # READLINE
180              
181             sub SEEK {
182 1     1   513 my ($self, $position, $whence) = @_;
183              
184 1         3 undef $!;
185              
186             die 'Seek is not supported on this handle'
187 1 50       10 if $self->{'mode'} ne '<';
188              
189 0         0 $self->{'buffer'} = '';
190              
191 0         0 delete $self->{'eof'};
192              
193 0 0       0 if ($whence == SEEK_SET) {
    0          
    0          
194 0         0 $self->{'position'} = $position
195             }
196              
197             elsif ($whence == SEEK_CUR) {
198 0         0 $self->{'position'} += $position
199             }
200              
201             elsif ($whence == SEEK_END) {
202 0         0 $self->{'position'} = $self->{'length'} + $position
203             }
204              
205             else {
206 0         0 $! = EINVAL;
207 0         0 return 0;
208             }
209              
210             $self->{'position'} = 0
211 0 0       0 if $self->{'position'} < 0;
212              
213 0         0 return 1;
214             } # SEEK
215              
216             sub TELL {
217 1     1   3686 my ($self) = @_;
218              
219             die 'Tell is not supported on this handle'
220 1 50       13 if $self->{'mode'} ne '<';
221              
222 0         0 return $self->{'position'};
223             } # TELL
224              
225             sub WRITE {
226 1     1   473 my ($self, $buffer, $length, $offset) = @_;
227              
228 1         3 undef $!;
229              
230             die 'Write is not supported on this handle'
231 1 50       9 if $self->{'mode'} ne '>';
232              
233             die 'Append-only writes supported'
234 0 0 0     0 if $offset and $offset != $self->{'offset'} + $self->{'length'};
235              
236 0   0     0 $self->{'offset'} //= $offset;
237 0         0 $self->{'buffer'} .= $buffer;
238 0         0 $self->{'length'} += $length;
239              
240             $self->__flush__() or return 0
241 0   0     0 while $self->{'length'} >= $self->{'chunk'};
242              
243 0         0 return 1;
244             } # WRITE
245              
246             sub CLOSE {
247 0     0   0 my ($self) = @_;
248              
249 0         0 undef $!;
250              
251             return 1
252 0 0       0 if $self->{'closed'};
253              
254 0         0 my $mode = $self->{'mode'};
255              
256 0 0       0 if ($mode eq '>') {
257 0 0 0     0 if ($self->{'length'} or not $self->{'upload_id'}) {
258             do {
259 0 0 0     0 @{ $self }{qw{ closed mode }} = (1, '') and return 0
  0         0  
260             unless $self->__flush__();
261 0         0 } while length $self->{'buffer'};
262             }
263             }
264              
265 0         0 $self->{'closed'} = 1;
266 0         0 $self->{'mode'} = '';
267              
268 0 0       0 return $self->__flush__()
269             if $mode eq '>';
270              
271 0         0 return 1;
272             } # CLOSE
273              
274             sub OPEN {
275 0     0   0 my ($self, $mode, $file) = @_;
276              
277 0         0 undef $!;
278              
279 0 0       0 ($mode, $file) = $mode =~ m{^([<>]?)(.*)$}s
280             unless $file;
281              
282 0   0     0 $mode ||= '<';
283              
284 0 0       0 $mode = '<' if $mode eq 'r';
285 0 0 0     0 $mode = '>' if $mode eq 'a' or $mode eq 'w';
286              
287 0 0 0     0 die 'Unsupported mode'
288             unless $mode eq '<' or $mode eq '>';
289              
290             $self->CLOSE()
291 0 0       0 unless $self->{'closed'};
292              
293 0         0 $self->{'length'} = 0;
294 0         0 $self->{'position'} = 0;
295 0         0 $self->{'buffer'} = '';
296              
297 0         0 delete $self->{'offset'};
298 0         0 delete $self->{'revision'};
299 0         0 delete $self->{'upload_id'};
300 0         0 delete $self->{'meta'};
301 0         0 delete $self->{'eof'};
302              
303 0 0       0 $self->{'path'} = $file
304             or die 'Path required';
305              
306 0 0 0     0 return 0
307             if $mode eq '<' and not $self->__meta__();
308              
309 0         0 $self->{'mode'} = $mode;
310 0         0 $self->{'closed'} = 0;
311              
312 0         0 return 1;
313             } # OPEN
314              
315             sub EOF {
316 1     1   354 my ($self) = @_;
317              
318             die 'Eof is not supported on this handle'
319 1 50       11 if $self->{'mode'} ne '<';
320              
321 0         0 return $self->{'position'} >= $self->{'length'};
322             } # EOF
323              
324 1     1   1545 sub BINMODE { 1 }
325              
326             sub __headers__ {
327             return [
328             'Authorization',
329             $_[0]->{'oauth2'}?
330             sprintf $header2, $_[0]->{'access_token'}:
331 0 0   0     sprintf $header1, @{ $_[0] }{qw{ app_key access_token app_secret access_secret }},
  0            
332             ];
333             }
334              
335             sub __flush__ {
336 0     0     my ($self) = @_;
337 0           my $furl = $self->{'furl'};
338 0           my $url;
339              
340 0           $url = 'https://';
341 0           $url .= join '/', $hosts->{'content'}, $version;
342              
343             $url .= join '/', '/commit_chunked_upload', $self->{'root'}, $self->{'path'}
344 0 0         if $self->{'closed'};
345              
346             $url .= '/chunked_upload'
347 0 0         unless $self->{'closed'};
348              
349 0           $url .= '?';
350              
351             $url .= join '=', 'upload_id', $self->{'upload_id'}
352 0 0         if $self->{'upload_id'};
353              
354             $url .= '&'
355 0 0         if $self->{'upload_id'};
356              
357             $url .= join '=', 'offset', $self->{'offset'} || 0
358 0 0 0       unless $self->{'closed'};
359              
360 0           my $response;
361              
362 0 0         unless ($self->{'closed'}) {
363 12     12   85 use bytes;
  12         16  
  12         75  
364              
365 0           my $buffer = substr $self->{'buffer'}, 0, $self->{'chunk'}, '';
366 0           my $length = length $buffer;
367              
368 0           $self->{'length'} -= $length;
369 0           $self->{'offset'} += $length;
370              
371 0           $response = $furl->put($url, &__headers__, $buffer);
372             } else {
373 0           $response = $furl->post($url, &__headers__);
374             }
375              
376 0 0         return $self->__error__($response)
377             if $response->code != 200;
378              
379             $self->{'meta'} = from_json($response->content())
380 0 0         if $self->{'closed'};
381              
382 0 0         unless ($self->{'upload_id'}) {
383 0           $response = from_json($response->content());
384 0           $self->{'upload_id'} = $response->{'upload_id'};
385             }
386              
387 0           return 1;
388             } # __flush__
389              
390             sub __meta__ {
391 0     0     my ($self) = @_;
392 0           my ($url, $meta);
393              
394 0           my $furl = $self->{'furl'};
395              
396 0           $url = 'https://';
397 0           $url .= join '/', $hosts->{'api'}, $version;
398 0           $url .= join '/', '/metadata', $self->{'root'}, $self->{'path'};
399              
400             $url .= '?hash='. delete $self->{'hash'}
401 0 0         if $self->{'hash'};
402              
403 0           my $response = $furl->get($url, &__headers__);
404              
405 0           my $code = $response->code();
406              
407 0 0         if ($code == 200) {
    0          
408 0           $meta = $self->{'meta'} = from_json($response->content());
409              
410             # XXX: Dropbox returns metadata for recently deleted files
411 0 0         if ($meta->{'is_deleted'}) {
412 0           $! = ENOENT;
413 0           return 0;
414             }
415             } elsif ($code != 304) {
416 0           return $self->__error__($response);
417             }
418              
419 0 0         if ($meta->{'is_dir'}) {
420 0           $! = EISDIR;
421 0           return 0;
422             }
423              
424 0           $self->{'revision'} = $meta->{'rev'};
425 0           $self->{'length'} = $meta->{'bytes'};
426              
427 0           return 1;
428             } # __meta__
429              
430             sub __fileops__ {
431 0     0     my ($type, $handle, $source, $target) = @_;
432              
433 0           my $self = *$handle{'HASH'};
434 0           my $furl = $self->{'furl'};
435 0           my ($url, @arguments);
436              
437 0           $url = 'https://';
438 0           $url .= join '/', $hosts->{'api'}, $version;
439 0           $url .= join '/', '/fileops', $type;
440              
441 0 0 0       if ($type eq 'move' or $type eq 'copy') {
442 0           @arguments = (
443             from_path => $source,
444             to_path => $target,
445             );
446             } else {
447 0           @arguments = (
448             path => $source,
449             );
450             }
451              
452 0           push @arguments, root => $self->{'root'};
453              
454 0           my $response = $furl->post($url, $self->__headers__(), \@arguments);
455              
456 0 0         return $self->__error__($response)
457             if $response->code != 200;
458              
459 0           $self->{'meta'} = from_json($response->content());
460              
461 0           return 1;
462             } # __fileops__
463              
464             sub __error__ {
465 0     0     my ($self, $response) = @_;
466 0           my $code = $response->code();
467              
468 0 0 0       if ($code == 400) {
    0 0        
    0          
    0          
    0          
    0          
    0          
469 0           $! = EINVAL;
470             }
471              
472             elsif ($code == 401 or $code == 403) {
473 0           $! = EACCES;
474             }
475              
476             elsif ($code == 404) {
477 0           $! = ENOENT;
478 0           return 0;
479             }
480              
481             elsif ($code == 406) {
482 0           $! = EPERM;
483 0           return 0;
484             }
485              
486             elsif ($code == 500 and $response->content() =~ m{\A(?:Cannot|Failed)}) {
487 0           $! = ECANCELED;
488             }
489              
490             elsif ($code == 503) {
491 0           $self->{'meta'} = { retry => $response->header('Retry-After') };
492              
493 0           $! = EAGAIN;
494             }
495              
496             elsif ($code == 507) {
497 0           $! = EFBIG;
498             }
499              
500             else {
501 0           die join ' ', $code, $response->decoded_content();
502             }
503              
504 0           return 0;
505             } # __error__
506              
507             sub contents ($;$$) {
508 0     0 1   my ($handle, $path, $hash) = @_;
509              
510 0 0         die 'GLOB reference expected'
511             unless ref $handle eq 'GLOB';
512              
513 0 0         *$handle->{'hash'} = $hash
514             if $hash;
515              
516 0 0 0       if (open $handle, '<', $path || '/' or $! != EISDIR) {
      0        
517 0           delete *$handle->{'meta'};
518 0           return;
519             }
520              
521 0           undef $!;
522 0           return @{ *$handle->{'meta'}{'contents'} };
  0            
523             } # contents
524              
525             sub putfile ($$$) {
526 0     0 1   my ($handle, $path, $data) = @_;
527              
528 0 0         die 'GLOB reference expected'
529             unless ref $handle eq 'GLOB';
530              
531 0 0         close $handle or return 0;
532              
533 0           my $self = *$handle{'HASH'};
534 0           my $furl = $self->{'furl'};
535 0           my ($url, $length);
536              
537 0           $url = 'https://';
538 0           $url .= join '/', $hosts->{'content'}, $version;
539 0           $url .= join '/', '/files_put', $self->{'root'}, $path;
540              
541             {
542 12     12   9877 use bytes;
  12         17  
  12         48  
  0            
543 0           $length = length $data;
544             }
545              
546 0           my $response = $furl->put($url, $self->__headers__(), $data);
547              
548 0 0         return $self->__error__($response)
549             if $response->code != 200;
550              
551 0           $self->{'path'} = $path;
552 0           $self->{'meta'} = from_json($response->content());
553              
554 0           return 1;
555             } # putfile
556              
557 0     0 1   sub movefile ($$$) { __fileops__('move', @_) }
558 0     0 1   sub copyfile ($$$) { __fileops__('copy', @_) }
559 0     0 1   sub deletefile ($$) { __fileops__('delete', @_) }
560 0     0 1   sub createfolder ($$) { __fileops__('create_folder', @_) }
561              
562             sub metadata ($) {
563 0     0 1   my ($handle) = @_;
564              
565 0 0         die 'GLOB reference expected'
566             unless ref $handle eq 'GLOB';
567              
568 0           my $self = *$handle{'HASH'};
569              
570             die 'Meta is unavailable for incomplete upload'
571 0 0         if $self->{'mode'} eq '>';
572              
573 0           return $self->{'meta'};
574             } # metadata
575              
576             =head1 NAME
577              
578             File::Dropbox - Convenient and fast Dropbox API abstraction
579              
580             =head1 SYNOPSIS
581              
582             use File::Dropbox;
583             use Fcntl;
584              
585             # Application credentials
586             my %app = (
587             oauth2 => 1,
588             access_token => $access_token,
589             );
590              
591             my $dropbox = File::Dropbox->new(%app);
592              
593             # Open file for writing
594             open $dropbox, '>', 'example' or die $!;
595              
596             while (<>) {
597             # Upload data using 4MB chunks
598             print $dropbox $_;
599             }
600              
601             # Commit upload (optional, close will be called on reopen)
602             close $dropbox or die $!;
603              
604             # Open for reading
605             open $dropbox, '<', 'example' or die $!;
606              
607             # Download and print to STDOUT
608             # Buffered, default buffer size is 4MB
609             print while <$dropbox>;
610              
611             # Reset file position
612             seek $dropbox, 0, Fcntl::SEEK_SET;
613              
614             # Get first character (unbuffered)
615             say getc $dropbox;
616              
617             close $dropbox;
618              
619             =head1 DESCRIPTION
620              
621             C provides high-level Dropbox API abstraction based on L. Code required to get C and
622             C for signed OAuth 1.0 requests or C for OAuth 2.0 requests is not included in this module.
623             To get C and C you need to register your application with Dropbox.
624              
625             At this moment Dropbox API is not fully supported, C covers file read/write and directory listing methods. If you need full
626             API support take look at L. C main purpose is not 100% API coverage,
627             but simple and high-performance file operations.
628              
629             Due to API limitations and design you can not do read and write operations on one file at the same time. Therefore handle can be in read-only
630             or write-only state, depending on last call to L. Supported functions for read-only state are: L,
631             L, L, L, L, L,
632             L, L, L. For write-only state: L, L,
633             L, L, L, L.
634              
635             All API requests are done using L module. For more accurate timeouts L is used, as described in L. Furl settings
636             can be overriden using C.
637              
638             =head1 METHODS
639              
640             =head2 new
641              
642             my $dropbox = File::Dropbox->new(
643             access_secret => $access_secret,
644             access_token => $access_token,
645             app_secret => $app_secret,
646             app_key => $app_key,
647             chunk => 8 * 1024 * 1024,
648             root => 'dropbox',
649             furlopts => {
650             timeout => 20
651             }
652             );
653              
654             my $dropbox = File::Dropbox->new(
655             access_token => $access_token,
656             oauth2 => 1
657             );
658              
659             Constructor, takes key-value pairs list
660              
661             =over
662              
663             =item access_secret
664              
665             OAuth 1.0 access secret
666              
667             =item access_token
668              
669             OAuth 1.0 access token or OAuth 2.0 access token
670              
671             =item app_secret
672              
673             OAuth 1.0 app secret
674              
675             =item app_key
676              
677             OAuth 1.0 app key
678              
679             =item oauth2
680              
681             OAuth 2.0 switch, defaults to false.
682              
683             =item chunk
684              
685             Upload chunk size in bytes. Also buffer size for C. Optional. Defaults to 4MB.
686              
687             =item root
688              
689             Access type, C for app-folder only access and C for full access.
690              
691             =item furlopts
692              
693             Parameter hash, passed to L constructor directly. Default options
694              
695             timeout => 10,
696             inet_aton => \&Net::DNS::Lite::inet_aton,
697             ssl_opts => {
698             SSL_verify_mode => SSL_VERIFY_PEER(),
699             }
700              
701             =back
702              
703             =head1 FUNCTIONS
704              
705             All functions are not exported by default but can be exported on demand.
706              
707             use File::Dropbox qw{ contents metadata putfile };
708              
709             First argument for all functions should be GLOB reference, returned by L.
710              
711             =head2 contents
712              
713             Arguments: $dropbox [, $path]
714              
715             Function returns list of hashrefs representing directory content. Hash fields described in L
716             docs|https://www.dropbox.com/developers/core/docs#metadata>. C<$path> defaults to C. If there is
717             unfinished chunked upload on handle, it will be commited.
718              
719             foreach my $file (contents($dropbox, '/data')) {
720             next if $file->{'is_dir'};
721             say $file->{'path'}, ' - ', $file->{'bytes'};
722             }
723              
724             =head2 metadata
725              
726             Arguments: $dropbox
727              
728             Function returns stored metadata for read-only handle, closed write handle or after
729             call to L or L.
730              
731             open $dropbox, '<', '/data/2013.dat' or die $!;
732              
733             my $meta = metadata($dropbox);
734              
735             if ($meta->{'bytes'} > 1024) {
736             # Do something
737             }
738              
739             =head2 putfile
740              
741             Arguments: $dropbox, $path, $data
742              
743             Function is useful for uploading small files (up to 150MB possible) in one request (at least
744             two API requests required for chunked upload, used in open-write-close sequence). If there is
745             unfinished chunked upload on handle, it will be commited.
746              
747             local $/;
748             open my $data, '<', '2012.dat' or die $!;
749              
750             putfile($dropbox, '/data/2012.dat', <$data>) or die $!;
751              
752             say 'Uploaded ', metadata($dropbox)->{'bytes'}, ' bytes';
753              
754             close $data;
755              
756             =head2 copyfile
757              
758             Arguments: $dropbox, $source, $target
759              
760             Function copies file or directory from one location to another. Metadata for copy
761             can be accessed using L function.
762              
763             copyfile($dropbox, '/data/2012.dat', '/data/2012.dat.bak') or die $!;
764              
765             say 'Created backup with revision ', metadata($dropbox)->{'revision'};
766              
767             =head2 movefile
768              
769             Arguments: $dropbox, $source, $target
770              
771             Function moves file or directory from one location to another. Metadata for moved file
772             can be accessed using L function.
773              
774             movefile($dropbox, '/data/2012.dat', '/data/2012.dat.bak') or die $!;
775              
776             say 'Created backup with size ', metadata($dropbox)->{'size'};
777              
778             =head2 deletefile
779              
780             Arguments: $dropbox, $path
781              
782             Function deletes file or folder at specified path. Metadata for deleted item
783             is accessible via L function.
784              
785             deletefile($dropbox, '/data/2012.dat.bak') or die $!;
786              
787             say 'Deleted backup with last modification ', metadata($dropbox)->{'modification'};
788              
789             =head2 createfolder
790              
791             Arguments: $dropbox, $path
792              
793             Function creates folder at specified path. Metadata for created folder
794             is accessible via L function.
795              
796             createfolder($dropbox, '/data/backups') or die $!;
797              
798             say 'Created folder at path ', metadata($dropbox)->{'path'};
799              
800             =head1 SEE ALSO
801              
802             L, L, L, L
803              
804             =head1 AUTHOR
805              
806             Alexander Nazarov
807              
808             =head1 COPYRIGHT AND LICENSE
809              
810             Copyright 2013-2016 Alexander Nazarov
811              
812             This program is free software; you can redistribute it and/or modify it
813             under the same terms as Perl itself.
814              
815             =cut
816              
817             1;