File Coverage

blib/lib/Net/Async/HTTP/DAV.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             package Net::Async::HTTP::DAV;
2             # ABSTRACT: WebDAV using Net::Async::HTTP
3 1     1   33551 use strict;
  1         3  
  1         30  
4 1     1   5 use warnings;
  1         2  
  1         28  
5              
6 1     1   643 use parent qw(IO::Async::Notifier);
  1         248  
  1         4  
7              
8             our $VERSION = '0.001';
9              
10             =head1 NAME
11              
12             Net::Async::HTTP::DAV - support for WebDAV over L
13              
14             =head1 VERSION
15              
16             Version 0.001
17              
18             =head1 SYNOPSIS
19              
20             use IO::Async::Loop;
21             use Net::Async::HTTP;
22             use Net::Async::HTTP::DAV;
23             use POSIX qw(strftime);
24             my $loop = IO::Async::Loop->new;
25             $loop->add(my $dav = Net::Async::HTTP::DAV->new(
26             host => 'cpan.perlsite.co.uk',
27             ));
28             $dav->propfind(
29             path => '/authors/id/T/TE/TEAM/',
30             on_item => sub {
31             my ($item) = @_;
32             printf "%-32.32s %-64.64s %12d\n", strftime("%Y-%m-%d %H:%M:%S", localtime $item->{modified}), $item->{displayname}, $item->{size};
33             },
34             )->get;
35              
36             =head1 DESCRIPTION
37              
38             Does some very basic WebDAV stuff.
39              
40             See L.
41              
42             Highly experimental, no documentation, see examples/ in source distribution.
43             API is likely to change.
44              
45             =cut
46              
47             use Net::Async::HTTP;
48             use Net::Async::HTTP::DAV::Response;
49              
50             use File::Spec;
51             use Scalar::Util qw(weaken);
52             use Encode qw(encode_utf8);
53              
54             =head1 METHODS
55              
56             =cut
57              
58             =head2 configure
59              
60             Accepts configuration parameters (can also be passed to L).
61              
62             =over 4
63              
64             =item * host - which host we're connecting to
65              
66             =item * path - base path for requests
67              
68             =item * user - optional username
69              
70             =item * pass - optional password, Basic auth
71              
72             =item * http - a pre-existing L instance
73              
74             =back
75              
76             =cut
77              
78             sub configure {
79             my ($self, %args) = @_;
80             foreach (qw(user pass path host http)) {
81             $self->{$_} = delete $args{$_} if exists $args{$_};
82             }
83             return $self;
84             }
85              
86             =head2 http
87              
88             Accessor for the internal L instance.
89              
90             =cut
91              
92             sub http {
93             my $self = shift;
94             if(@_) {
95             shift->{http} = shift;
96             return $self
97             }
98             unless($self->{http}) {
99             my $ua = $self->ua_factory;
100             $self->add_child($ua);
101             Scalar::Util::weaken($self->{http} = $ua);
102             }
103             return $self->{http};
104             }
105              
106             =head2 ua_factory
107              
108             Populates the L instance via factory or default settings.
109              
110             =cut
111              
112             sub ua_factory {
113             my ($self) = @_;
114             $self->{ua_factory}->() if $self->{ua_factory};
115             Net::Async::HTTP->new(
116             decode_content => 0,
117             fail_on_error => 1,
118             max_connections_per_host => 4,
119             stall_timeout => 60,
120             )
121             }
122              
123             =head2 path
124              
125             Base path for requests.
126              
127             =cut
128              
129             sub path { shift->{path} }
130              
131             =head2 propfind
132              
133             Does a propfind request.
134              
135             Parameters are basically 'path' and on_item for a per-item callback.
136              
137             =cut
138              
139             sub propfind {
140             my $self = shift;
141             my %args = @_;
142             # want a trailing /
143             my $uri = $self->uri_from_path(File::Spec->catdir(($self->path // ()), $args{path}) . '/') or die "Invalid URL?";
144             my $body = <<"EOF";
145            
146            
147            
148            
149             EOF
150              
151             my $req = HTTP::Request->new(
152             PROPFIND => $uri->path, [
153             'Host' => $uri->host,
154             'Depth' => 1,
155             'Content-Type' => 'text/xml'
156             ], encode_utf8($body)
157             );
158             $req->protocol('HTTP/1.1');
159             $req->authorization_basic($self->user, $self->pass) if defined($self->user);
160             $self->http->do_request(
161             request => $req,
162             host => $uri->host,
163             port => $uri->scheme || 80,
164             SSL => $uri->scheme eq 'https' ? 1 : 0,
165             on_header => sub {
166             my $response = shift;
167             my $result = Net::Async::HTTP::DAV::Response->new(
168             %args,
169             path => $uri->path
170             );
171             # Seems we'll need to return the response?
172             weaken $response;
173             return sub {
174             $result->parse_chunk($_[0]) if @_;
175             $response
176             };
177             },
178             );
179             }
180              
181             sub getinfo {
182             my $self = shift;
183             my %args = @_;
184             my $uri = $self->uri_from_path($args{path} // $self->{path}) or die "Invalid URL?";
185             my $body = <<"EOF";
186            
187            
188            
189            
190             EOF
191              
192             my $req = HTTP::Request->new(
193             PROPFIND => $uri->path, [
194             'Host' => $uri->host,
195             'Depth' => 0,
196             'Content-Type' => 'text/xml'
197             ], encode_utf8($body)
198             );
199             $req->protocol('HTTP/1.1');
200             $req->authorization_basic($self->user, $self->pass) if $self->user;
201             $self->http->do_request(
202             request => $req,
203             host => $uri->host,
204             port => $uri->scheme || 80,
205             SSL => $uri->scheme eq 'https' ? 1 : 0,
206             on_header => sub {
207             my $response = shift;
208             my $result = Net::Async::HTTP::DAV::Response->new(
209             %args,
210             path => $uri->path,
211             on_item => sub {
212             my $item = shift;
213             $args{on_size}->($item->{size});
214             }
215             );
216             return sub {
217             $result->parse_chunk($_[0]) if @_;
218             };
219             },
220             on_error => sub {
221             my ( $message ) = @_;
222             die "Failed - $message\n";
223             }
224             );
225             return $self;
226             }
227              
228             =head2 head
229              
230             Perform HEAD request on given path.
231              
232             =cut
233              
234             sub head {
235             my $self = shift;
236             my %args = @_;
237             my $uri = $self->uri_from_path($args{path} // $self->{path}) or die "Invalid URL?";
238             my $req = HTTP::Request->new(
239             HEAD => $uri->path, [
240             'Host' => $uri->host,
241             ]
242             );
243             $req->protocol('HTTP/1.1');
244             $req->authorization_basic($self->user, $self->pass) if $self->user;
245             $self->http->do_request(
246             request => $req,
247             host => $uri->host,
248             port => $uri->scheme || 80,
249             SSL => $uri->scheme eq 'https' ? 1 : 0,
250             on_response => sub {
251             my $response = shift;
252             # $args{on_size}->($response->content_length);
253             },
254             on_error => sub {
255             my ( $message ) = @_;
256             die "Failed - $message\n";
257             }
258             );
259             return $self;
260             }
261              
262             =head2 get
263              
264             GET the given resource
265              
266             =cut
267              
268             sub get {
269             my $self = shift;
270             my %args = @_;
271             my $uri = $self->uri_from_path($args{path} // $self->{path}) or die "Invalid URL?";
272             my $req = HTTP::Request->new(
273             GET => $uri->path, [
274             'Host' => $uri->host,
275             ]
276             );
277             $req->protocol('HTTP/1.1');
278             $req->authorization_basic($self->user, $self->pass) if $self->user;
279             $self->http->do_request(
280             request => $req,
281             host => $uri->host,
282             port => $uri->scheme || 80,
283             SSL => $uri->scheme eq 'https' ? 1 : 0,
284             on_header => sub {
285             my $response = shift;
286             return $args{on_header}->($response);
287             },
288             on_error => sub {
289             my ( $message ) = @_;
290             die "Failed - $message\n";
291             }
292             );
293             return $self;
294             }
295              
296             =head2 put
297              
298             Write data directly to the given resource.
299              
300             =cut
301              
302             sub put {
303             my $self = shift;
304             my %args = @_;
305             my $handler = delete $args{response_body};
306             my $uri = $self->uri_from_path($args{path} // $self->{path});
307              
308             my $req = HTTP::Request->new(
309             PUT => $uri->path, [
310             'Host' => $uri->host,
311             'Content-Type' => 'application/octetstream',
312             ], defined $args{content} ? $args{content} : ()
313             );
314             $req->protocol('HTTP/1.1');
315             $req->authorization_basic($self->{user}, $self->{pass});
316             $req->content_length($args{size}) unless defined $args{content};
317              
318             my $fh;
319             $self->http->do_request(
320             request => $req,
321             host => $uri->host,
322             port => $uri->scheme || 80,
323             SSL => $uri->scheme eq 'https' ? 1 : 0,
324             (defined $args{content})
325             ? ()
326             : (request_body => $handler || sub {
327             my ($stream) = @_;
328             warn $stream;
329             return '';
330             my $read = sysread $fh, my $buffer, 32768;
331             warn $! unless defined $read;
332             return $buffer if $read;
333             return;
334             }),
335             on_error => sub {
336             my ( $message ) = @_;
337             die "Failed - $message\n";
338             },
339             on_response => $args{on_response} || sub {
340             my ($response) = @_;
341             my $msg = $response->message;
342             $msg =~ s/\s+/ /ig;
343             $msg =~ s/(?:^\s+)|(?:\s+$)//g; # trim
344             warn $response->code . " - $msg\n";
345             }
346             );
347             }
348              
349             sub host { shift->{host} }
350             sub user { shift->{user} }
351             sub pass { shift->{pass} }
352              
353             sub uri_from_path {
354             my $self = shift;
355             my $path = shift // '/';
356             $path = "/$path" unless substr($path, 0, 1) eq '/';
357             $path =~ s{/+}{/}g;
358             return URI->new('http://' . $self->host . $path) || die "Invalid URL?";
359             }
360              
361             1;
362              
363             __END__