File Coverage

blib/lib/STF/Dispatcher/PSGI.pm
Criterion Covered Total %
statement 151 166 90.9
branch 62 84 73.8
condition 9 16 56.2
subroutine 20 20 100.0
pod 6 10 60.0
total 248 296 83.7


line stmt bran cond sub pod time code
1             package STF::Dispatcher::PSGI;
2 2     2   3146 use strict;
  2         6  
  2         121  
3             our $VERSION = '1.12';
4 2     2   13 use Carp ();
  2         5  
  2         34  
5 2     2   1047 use HTTP::Date ();
  2         5069  
  2         46  
6 2     2   324385 use Plack::Request;
  2         600187  
  2         78  
7 2     2   5770 use Plack::Middleware::HTTPExceptions;
  2         24100  
  2         76  
8 2     2   17 use Scalar::Util ();
  2         6  
  2         34  
9 2     2   1285 use STF::Dispatcher::PSGI::HTTPException;
  2         7  
  2         66  
10             use Class::Accessor::Lite
11 2         21 rw => [ qw(impl nosniff_header) ]
12 2     2   895 ;
  2         1289  
13             use constant +{
14 2         8612 STF_REPLICATION_HEADER => 'X-STF-Replication-Count',
15             STF_REPLICATION_HEADER_DEPRECATED => 'X-Replication-Count',
16             STF_RECURSIVE_DELETE_HEADER => 'X-STF-Recursive-Delete',
17             STF_CONSISTENCY_HEADER => "X-STF-Consistency",
18             # XXX This below is should be deprecated. don't use
19             STF_FORCE_MASTER_HEADER => "X-STF-Force-MasterDB",
20             STF_DELETED_OBJECTS_HEADER => "X-STF-Deleted-Objects",
21             STF_MOVE_OBJECT_HEADER => "X-STF-Move-Destination",
22             STF_DEFAULT_REPLICATION_COUNT => 2,
23 2     2   221 };
  2         4  
24              
25             sub new {
26 2     2 0 9 my ($class, %args) = @_;
27              
28 2 50       11 my $impl = $args{impl} or
29             Carp::croak( "Required parameter 'impl' not specified" );
30 2         9 foreach my $method (qw(create_bucket get_bucket delete_bucket create_object get_object delete_object delete_bucket modify_object rename_object is_valid_object)) {
31 20 50       111 $impl->can( $method ) or
32             Carp::croak("$impl does not implement $method");
33             }
34              
35 2         20 bless { nosniff_header => 1, %args }, $class;
36             }
37              
38             sub to_app {
39 2     2 1 5 my $self = shift;
40 2     60   12 my $app = sub { $self->handle_psgi(@_) };
  60         108427  
41 2         30 $app = Plack::Middleware::HTTPExceptions->wrap( $app );
42 2 50       125 if ($self->nosniff_header) {
43 2         1309 require Plack::Middleware::Header;
44 2         717 $app = Plack::Middleware::Header->wrap( $app,
45             set => [ 'X-Content-Type-Options' => 'nosniff' ]
46             );
47             }
48 2         89 $app;
49             }
50              
51             sub handle_psgi {
52 60     60 0 147 my ($self, $env) = @_;
53              
54 60         254 my $guard = $self->impl->start_request( $env );
55              
56 60         11052 my $req = Plack::Request->new($env);
57 60         1023 my $res;
58 60         133 my $method = $env->{REQUEST_METHOD};
59 60 100       411 if ($method =~ /^(?:GET|HEAD)$/) {
    100          
    100          
    100          
    50          
60 20         87 $res = $self->get_object( $req );
61             } elsif ($method eq 'PUT') {
62 18   100     101 my $cl = $env->{CONTENT_LENGTH} || 0;
63 18 100       48 if ( $cl == 0 ) {
64 8         33 $res = $self->create_bucket( $req );
65             } else {
66 10         53 $res = $self->create_object( $req );
67             }
68             } elsif ($method eq 'DELETE') {
69 10         46 $res = $self->delete_object( $req );
70             } elsif ($method eq 'POST') {
71 2         9 $res = $self->modify_object( $req );
72             } elsif ($method eq 'MOVE') {
73 10         37 $res = $self->rename_object( $req );
74             } else {
75 0         0 $res = $req->new_response(400, [ "Content-Type" => "text/plain" ], [ "Bad Request" ]);
76             }
77              
78 58         7528 return $res->finalize();
79             }
80              
81             sub parse_names {
82 70     70 0 1327 my ($self, $path) = @_;
83 70 50       415 if ( $path !~ m{^/([^/]+)(?:/(.+)$)?} ) {
84 0         0 return ();
85             }
86 70         313 return ($1, $2);
87             }
88              
89             sub create_bucket {
90 8     8 1 12 my ($self, $req) = @_;
91              
92 8         38 my ($bucket_name, $object_name) = $self->parse_names( $req->path );
93 8 100       81 if ( $object_name ) {
94 2         16 return $req->new_response( 400, [], [ "Bad bucket name $bucket_name/$object_name" ] );
95             }
96              
97 6         23 my $bucket = $self->impl->get_bucket( {
98             bucket_name => $bucket_name,
99             request => $req,
100             } );
101 6 100       46 if ($bucket) {
102 2         12 return $req->new_response( 204, [], [] );
103             }
104              
105 4         12 $bucket = $self->impl->create_bucket( {
106             bucket_name => $bucket_name,
107             request => $req,
108             } );
109 4 50       30 if (! $bucket) {
110 0         0 return $req->new_response( 500, [], [ "Failed to create bucket" ] );
111             }
112              
113 4         36 return $req->new_response( 201, [], [ "Created bucket" ] );
114             }
115              
116             sub create_object {
117 10     10 1 23 my ($self, $req) = @_;
118              
119             # find the appropriate bucket
120 10         45 my ($bucket_name, $object_name) = $self->parse_names( $req->path );
121 10         43 my $bucket = $self->impl->get_bucket( {
122             bucket_name => $bucket_name,
123             request => $req
124             } );
125              
126 10 100       128 if (! $bucket) {
127             # XXX Should be 403?
128 2         14 return $req->new_response(500, ["Content-Type" => "text/plain"], [ "Failed to find bucket" ] );
129             }
130 8 100       31 if (! $object_name) {
131 2         13 return $req->new_response(400, [], ["Could not extract object name"]);
132             }
133              
134             # try to find a suffix
135 6         25 my ($suffix) = ( $req->path =~ /\.([a-zA-Z0-9]+)$/ );
136 6   50     971 $suffix ||= 'dat';
137              
138 6         73 my $input = $req->input;
139 6         49 my $code;
140 6 50       98 if ( $code = $input->can('rewind') ) {
    50          
141 0         0 $code->( $input );
142             } elsif ( $code = $input->can('seek') ) {
143 6         41 $code->( $input, 0, 0 );
144             }
145              
146 6         58 my %ext_args;
147 6 50       31 if ($req->content_type) {
148 6         67 $ext_args{content_type} = $req->content_type;
149             }
150              
151 6   50     69 my $object = $self->impl->create_object( {
      50        
      50        
152             bucket => $bucket,
153             consistency => $req->header( STF_CONSISTENCY_HEADER ) || 0,
154             object_name => $object_name,
155             size => $req->content_length || 0,
156             suffix => $suffix,
157             input => $input,
158             replicas => $req->header( STF_REPLICATION_HEADER ) ||
159             $req->header( STF_REPLICATION_HEADER_DEPRECATED ) ||
160             STF_DEFAULT_REPLICATION_COUNT || 0,
161             request => $req,
162             %ext_args
163             } );
164 6 50       93 if (! $object) {
165 0         0 return $req->new_response( 500, [], [ "Failed to create object" ] );
166             }
167 6         54 return $req->new_response( 201, [], [ "Created " . $req->path ] );
168             }
169              
170             sub delete_object {
171 10     10 1 17 my ($self, $req) = @_;
172              
173             # find the appropriate bucket
174 10         36 my ($bucket_name, $object_name) = $self->parse_names( $req->path );
175 10         40 my $bucket = $self->impl->get_bucket( {
176             bucket_name => $bucket_name,
177             request => $req
178             } );
179 10 100       80 if (! $bucket) {
180 4 100       11 if ( ! $object_name ) {
181 2         14 return $req->new_response(404, ["Content-Type" => "text/plain"], [ "No such bucket" ] );
182             } else {
183 2         13 return $req->new_response(500, ["Content-Type" => "text/plain"], [ "Failed to find bucket" ] );
184             }
185             }
186              
187             # if there's no object_name, then this is a request to delete
188             # the bucket, not an object
189 6 100       20 if ( ! $object_name ) {
190 2   50     11 my $ret = $self->impl->delete_bucket( {
191             bucket => $bucket,
192             recursive => $req->header( STF_RECURSIVE_DELETE_HEADER ) || 0,
193             request => $req,
194             } );
195 2 50       21 if (! $ret) {
196 0         0 return $req->new_response(500, ["Content-Type" => "text/plain"], ["Failed to delete bucket" ]);
197             }
198              
199 2         15 return $req->new_response( 204, [], [] );
200             }
201              
202 4         15 my $is_valid = $self->impl->is_valid_object( {
203             bucket => $bucket,
204             object_name => $object_name,
205             request => $req,
206             });
207 4 100       18 if (! $is_valid) {
208 2         11 return $req->new_response(404, [], [ "No such object" ]);
209             }
210              
211 2 50       9 if ($self->impl->delete_object( {
212             bucket => $bucket,
213             object_name => $object_name,
214             request => $req,
215             } )) {
216 2         11 return $req->new_response( 204, [], [] );
217             } else {
218 0         0 return $req->new_response( 500, [ "Content-Type" => "text/plain" ], [ "Failed to delete object" ] );
219             }
220             }
221              
222             sub get_object {
223 20     20 1 40 my ($self, $req) = @_;
224             # find the appropriate bucket
225 20         77 my ($bucket_name, $object_name) = $self->parse_names( $req->path );
226 20         72 my $bucket = $self->impl->get_bucket( {
227             bucket_name => $bucket_name,
228             request => $req
229             } );
230 20 100       145 if (! $bucket) {
231 2         18 return $req->new_response(404, ["Content-Type" => "text/plain"], [ "Failed to find bucket" ] );
232             }
233              
234 18   50     72 my $object = $self->impl->get_object( {
235             bucket => $bucket,
236             object_name => $object_name,
237             request => $req,
238             force_master => $req->header( STF_FORCE_MASTER_HEADER ) || 0,
239             } );
240 16 100       295 if (! $object) {
241 6         33 return $req->new_response( 404, [], [ "Failed to get object" ] );
242             }
243              
244 10         18 my @headers;
245 10 50       75 if ( my $ct = $object->can('content_type') ) {
246 10         37 push @headers, "Content-Type", $object->content_type;
247             }
248 10 50       118 if ( my $lm = $object->can('modified_on') ) {
249 10         36 push @headers, "Last-Modified", HTTP::Date::time2str($object->modified_on);
250             }
251              
252 10 100       290 return $req->new_response( 200,
253             \@headers,
254             [ $req->method eq 'HEAD' ? '' : $object->content ]
255             );
256             }
257              
258             sub modify_object {
259 2     2 1 5 my ($self, $req) = @_;
260              
261 2         9 my ($bucket_name, $object_name) = $self->parse_names( $req->path );
262 2         9 my $bucket = $self->impl->get_bucket( {
263             bucket_name => $bucket_name,
264             request => $req
265             } );
266 2 50       16 if (! $bucket) {
267 0         0 return $req->new_response(500, ["Content-Type" => "text/plain"], [ "Failed to find bucket" ] );
268             }
269              
270 2         8 my $is_valid = $self->impl->is_valid_object( {
271             bucket => $bucket,
272             object_name => $object_name,
273             request => $req,
274             });
275 2 50       12 if (! $is_valid) {
276 0         0 return $req->new_response(404, [], [ "No such object" ]);
277             }
278              
279 2   50     9 my $ret = $self->impl->modify_object( {
280             bucket => $bucket,
281             object_name => $object_name,
282             replicas => $req->header( STF_REPLICATION_HEADER ) ||
283             $req->header( STF_REPLICATION_HEADER_DEPRECATED ) ||
284             STF_DEFAULT_REPLICATION_COUNT || 0,
285             request => $req,
286             } );
287              
288 2 50       11 if ($ret) {
289 2         9 return $req->new_response(204, [], []);
290             } else {
291 0         0 return $req->new_response(500, ["Content-Type" => "text/plain"], [ "Failed to modify object" ]);
292             }
293             }
294              
295             sub rename_object {
296 10     10 0 16 my ($self, $req) = @_;
297              
298             # source
299 10         38 my ($bucket_name, $object_name) = $self->parse_names( $req->path );
300 10         36 my $bucket = $self->impl->get_bucket( {
301             bucket_name => $bucket_name,
302             request => $req
303             } );
304 10 50       75 if (! $bucket) {
305             # XXX should be 403?
306 0         0 return $req->new_response(500, ["Content-Type" => "text/plain"], [ "Failed to find source bucket" ] );
307             }
308              
309 10 100       32 if (! $object_name) {
310 4         17 my ($dest_bucket, $dest_object) = $self->parse_names( $req->header( STF_MOVE_OBJECT_HEADER ) );
311 4 50       14 if (! $dest_bucket) {
312 0         0 return $req->new_response(500, ["Content-Type" => "text/plain"], ["Destination bucket name was not specified"]);
313             }
314 4 50       12 if ($dest_object) {
315 0         0 return $req->new_response(500, ["Content-Type" => "text/plain"], ["Destination contains object name"]);
316             }
317              
318 4         17 my $ok = $self->impl->rename_bucket({
319             bucket => $bucket,
320             name => $dest_bucket
321             });
322 4 100       187 if ($ok) {
323 2         16 return $req->new_response(201, [], ["Moved"]);
324             } else {
325 2         11 return $req->new_response(500, [], ["Failed to rename bucket"]);
326             }
327             }
328              
329 6         23 my $is_valid = $self->impl->is_valid_object( {
330             bucket => $bucket,
331             object_name => $object_name,
332             request => $req,
333             });
334 6 50       28 if (! $is_valid) {
335 0         0 return $req->new_response(404, [], [ "No such source object" ]);
336             }
337              
338            
339 6         26 my $destination = $req->header( STF_MOVE_OBJECT_HEADER );
340 6         1202 my ($dest_bucket_name, $dest_object_name) = $self->parse_names( $destination );
341 6         9 my $dest_bucket;
342 6 100       20 if ( $dest_bucket_name eq $bucket_name ) {
343 2         4 $dest_bucket = $bucket
344             } else {
345 4         14 $dest_bucket = $self->impl->get_bucket( {
346             bucket_name => $dest_bucket_name,
347             request => $req
348             } );
349 4 100       28 if (! $dest_bucket) {
350             # XXX should be 403?
351 2         13 return $req->new_response(500, ["Content-Type" => "text/plain"], [ "Failed to find source bucket" ] );
352             }
353             }
354              
355 4         17 my $rv = $self->impl->rename_object( {
356             source_bucket => $bucket,
357             source_object_name => $object_name,
358             destination_bucket => $dest_bucket,
359             destination_object_name => $dest_object_name,
360             } );
361              
362 4 50       27 if (! $rv) {
363 0         0 return $req->new_response( 500, [], [ "Failed to create object" ] );
364             }
365 4         20 return $req->new_response( 201, [], [ "Created " . $req->path ] );
366             }
367              
368             1;
369              
370             __END__