/[Frey]/branches/zimbardo/lib/Frey/CouchAPI.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /branches/zimbardo/lib/Frey/CouchAPI.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1191 - (show annotations)
Mon Sep 28 20:25:07 2009 UTC (14 years, 7 months ago) by dpavlin
File size: 8505 byte(s)
separate glob for database name and files so we can select subset of files from directory
1 package Frey::CouchAPI;
2
3 =head1 DESCRIPTION
4
5 This is REST wrapper using following L<Mojo> implement Apache's CouchDB API
6
7 You can access it using normal C</_utils/> URI, just like on real CouchDB and
8 it will bring up partially functional Futon interface against this module.
9
10 L<Mojo::URL>
11
12 L<Mojo::Transaction>
13
14
15 =head1 Supported HTTP API
16
17 =cut
18
19 use warnings;
20 use strict;
21
22 use JSON;
23 use Data::Dump qw/dump/;
24 use URI::Escape;
25 use File::Path;
26 use Storable;
27
28 our $VERSION = '0.3';
29 $VERSION .= " (Frey $Frey::VERSION)" if $Frey::VERSION;
30
31 our $debug = $Frey::debug || 0;
32
33 sub rewrite_urls {
34 my ( $self, $tx ) = @_;
35 if ( $tx->req->url->path =~ m{/_utils/} ) {
36 my $path = $tx->req->url->path;
37 $path =~ s{(/_utils)/?$}{$1/index.html}; # poor man's DirectoryIndex
38 $path =~ s{/_utils}{/static/futon};
39 $tx->req->url->path( $path );
40 my $url = $tx->req->url->to_string;
41 my $old = $url;
42 $url = $tx->req->url->to_string;
43 warn "# rewrite $old -> $url\n";
44 }
45 }
46
47 our $config = {
48 database => {
49 path => '/data/webpac2/var/row',
50 database_glob => '*/*',
51 data_glob => '*',
52 }
53 };
54
55 $config = {
56 data => {
57 base_path => '/home/dpavlin/x/Frey/var/svn/home/dpavlin/private/svn',
58 database => '*',
59 files => '*.storable',
60 },
61 };
62
63 sub _glob_databases {
64 my $path = $config->{data}->{base_path};
65 map {
66 my $p = $_;
67 $p =~ s{^$path/+}{};
68 $p;
69 } glob "$path/$config->{data}->{database}"
70 }
71
72 sub _glob_files {
73 my $path = $config->{data}->{base_path} . '/' . shift;
74 map {
75 my $p = $_;
76 $p =~ s{^$path/+}{};
77 $p;
78 } glob "$path/$config->{data}->{files}"
79 };
80
81 my $regex_dbs = '[a-zA-Z][a-zA-Z0-9_\$\(\)\+\-/]+';
82
83 our $json = {};
84 our $status;
85
86 sub ok {
87 $json = { ok => JSON::true };
88 $status = 200;
89 warn "ok from ",join(' ',caller),$/;
90 }
91
92 sub file_rev { (stat($_[0]))[9] } # mtime
93
94 sub data_from_tx {
95 my $tx = shift;
96 my $data = $tx->req->content->file->slurp;
97 $data = JSON->new->allow_nonref->decode( $data );
98 warn "## data ",dump( $data );
99 return $data;
100 }
101
102 sub dispatch {
103 my ($self,$tx) = @_;
104
105 $status = 500; # Internal Error
106
107 my $url = $tx->req->url->to_string;
108 $url = uri_unescape( $url );
109 my $method = $tx->req->method;
110 my $path = $config->{data}->{base_path};
111
112 die "base_path $path doesn't exist" unless -e $path;
113
114 if ( $url eq '/' ) {
115 $json = {
116 couchdb => "Welcome",
117 version => "CouchAPI $VERSION",
118 };
119 $status = 200;
120 } elsif ( $url eq '/_all_dbs' ) {
121 $json = [
122 map {
123 my $db = $_;
124 $db =~ s{^\Q$path\E/*}{};
125 $db;
126 } _glob_databases
127 ];
128 $status = 200;
129 } elsif ( $url =~ m{^/_config/?(.+)} ) {
130
131 $json = $config;
132
133 if ( $method eq 'PUT' ) {
134
135 my $part = $1;
136 my $data = data_from_tx( $tx );
137 warn "## part $part = $data\n";
138
139 $part =~ s!/!'}->{'!;
140
141 # poor man's transaction :-)
142 my $code = "\$config->{'$part'} = \$data;";
143 eval $code; ## no critic
144 if ( $@ ) {
145 warn "ERROR: $code -> $@";
146 $status = 500;
147 } else {
148 $status = 200;
149 }
150
151 warn "# config after $code is ",dump( $config ),$/;
152
153 } elsif ( $method eq 'GET' ) {
154 $status = 200;
155 } else {
156 $status = 501;
157 }
158
159 =head2 Database
160
161 L<http://wiki.apache.org/couchdb/HTTP_database_API> except compaction
162
163 =cut
164
165 } elsif (
166 $url =~ m{($regex_dbs)/$}
167 # DELETE doesn't have trailing slash
168 || $method eq 'DELETE' && $url =~ m{($regex_dbs)$}
169 ) {
170
171 my $database = $1;
172
173 my $dir = "$path/$database";
174
175 if ( $method eq 'GET' ) {
176 $json = database_get( $database );
177 } elsif ( $method eq 'DELETE' ) {
178 if ( ! -e $dir ) {
179 $status = 404;
180 } else {
181 rmtree($dir);
182 if ( ! -d $dir ) {
183 ok;
184 } else {
185 $status = 500;
186 }
187 }
188 } elsif ( $method eq 'PUT' ) {
189 if ( -e $dir ) {
190 $status = 412;
191 } else {
192 mkpath($dir);
193 if ( -e $path ) {
194 ok;
195 $status = 201;
196 } else {
197 $status = 500;
198 }
199 }
200 }
201
202 warn "## database $database $status ",dump( $json );
203
204 } elsif ( $url =~ m{($regex_dbs)/([^?]+)\??(.+)?$} ) {
205 my ($database,$id,$args) = ($1,$2,$3);
206
207 =head2 Document
208
209 L<http://wiki.apache.org/couchdb/HTTP_Document_API>
210
211 =cut
212
213 my $arg;
214 if ( $args ) {
215 foreach my $a ( split(/[&;]/,$args) ) {
216 my ($n,$v) = split(/=/,$a);
217 $v =~ s{(["'])(.+)\1}{$2};
218 $arg->{$n} = $v;
219 }
220 }
221
222 warn "ERROR: path $path doesn't exist\n" unless -e $path;
223
224 my $p = "$path/$database/$id";
225 warn "## database: $database id: $id -> $p ", dump( $arg ),"\n";
226
227
228 if ( $id =~ m{_all_docs(\w*)$} ) {
229
230 my $by = $1;
231 my $offset = 0;
232 my $startkey = delete $arg->{startkey};
233 $startkey ||= delete $arg->{startkey_docid}; # XXX key == id
234 my $endkey = delete $arg->{endkey};
235 my $limit = delete $arg->{limit};
236 my $skip = delete $arg->{skip};
237 my $total_rows = 0;
238 my $collected_rows = 0;
239
240 my @docs = grep { length($_) > 0 } map { ## no critic
241
242 my $id = $_;
243 $total_rows++;
244
245 if ( $limit > 0 && $collected_rows == $limit ) {
246 '';
247 } else {
248
249 if ( defined $endkey && $id gt $endkey ) {
250 '';
251 } elsif ( $startkey ) {
252 if ( $id ge $startkey ) {
253 $collected_rows++;
254 $id;
255 } else {
256 $offset++;
257 '';
258 }
259 } else {
260 $collected_rows++;
261 $id;
262 }
263 }
264
265 } _glob_files( $database );
266
267 $offset += $skip if $skip;
268
269 warn "## docs $startkey -> $endkey limit $limit ", dump( @docs ); # if $debug;
270
271 $json = {
272 total_rows => $total_rows,
273 offset => $offset,
274 rows => [],
275 };
276
277 my $rows;
278 my @ids;
279
280 foreach my $id ( @docs ) {
281 warn "++ $id\n" if $debug;
282 my $p = "$path/$database/$id";
283 my $data = eval { Storable::retrieve( $p ) };
284 if ( $@ ) {
285 warn "ERROR: $p | $@\n";
286 next;
287 }
288 push @ids, $id;
289 $rows->{$id} = {
290 id => $id,
291 key => $id,
292 value => {
293 rev => file_rev $p,
294 }
295 };
296 }
297
298 my $descending = delete $arg->{descending};
299 my @sorted = sort @ids;
300
301 warn "creating rows in ", $descending ? "descending" : "", " order\n";
302
303 foreach my $id ( $descending ? reverse @sorted : @sorted ) {
304 warn ">> $id ", $descending ? 'desc' : 'asc', "\n" if $debug;
305 push @{ $json->{rows} }, $rows->{$id};
306 }
307
308 $status = 200;
309
310 } elsif ( $method eq 'PUT' ) {
311
312 warn "## ",dump( $tx->req ) if $debug;
313
314 my $data = $tx->req->content->file->slurp;
315
316 my $db_path = "$path/$database";
317 make_path $db_path unless -e $db_path;
318
319 Storable::store( from_json($data), $p );
320 my $rev = file_rev $p;
321 warn "store $p $rev size ", -s $p, " bytes | $data\n";
322
323 $status = 201; # Created
324 $json = {
325 id => $id,
326 ok => JSON::true,
327 rev => $rev,
328 };
329
330 } elsif ( $method eq 'GET' ) {
331 if ( ! -e $p ) {
332 $status = 404;
333 } else {
334 warn "retrive $p ", -s $p, " bytes\n";
335 $json = Storable::retrieve( $p );
336 if ( delete $arg->{revs_info} ) {
337 my $rev = file_rev $p;
338 $json->{_rev} = $rev;
339 $json->{_revs_info} = [
340 { rev => $rev, status => 'available' }
341 ];
342 }
343 $status = 200;
344
345 }
346 } elsif ( $method eq 'DELETE' ) {
347 if ( -e $p ) {
348 unlink $p && ok || { $status = 500 };
349 } else {
350 $status = 404;
351 }
352 } elsif ( $method eq 'POST' ) {
353 my $data = data_from_tx( $tx );
354
355 # FIXME implement real view server and return 200
356 $json = { total_rows => 0, offset => 0 };
357 $status = 202;
358
359 } else {
360 $status = 501;
361 }
362
363 if ( keys %$arg ) {
364 warn "WARNING: arg left from $url = ",dump( $arg ),$/;
365 $status = 501;
366 }
367
368 }
369
370 $json = { error => 'not_found', reason => 'Missing' } if $status == 404;
371
372 if ( $method =~ m{(DELETE|PUT)} ) {
373 # $tx->res->headers->add_line( 'Location' => $tx->req->url->to_abs );
374 }
375
376 $tx->res->code( $status );
377 $tx->res->headers->content_type( 'text/plain;charset=utf-8' );
378 my $body = to_json $json;
379 $tx->res->body( $body );
380 # $tx->res->headers->add_line( 'Cache-Control' => 'must-revalidate' );
381 # $tx->res->headers->add_line( 'Server' => "Frey::CouchAPI/$VERSION" );
382
383 print "$method $url $status\n$body\n";
384
385 warn "## headers ", $tx->res->headers->to_string;
386
387 return $tx;
388
389 }
390
391 sub database_get {
392 my ($db_name) = @_;
393 warn "# collecting docs for $db_name\n";
394 my @docs = _glob_files( $db_name );
395 warn dump @docs;
396 my $json = {
397 db_name => $db_name,
398 doc_count => $#docs + 1,
399 doc_del_count => 0,
400 update_seq => 0,
401 purge_seq => 0,
402 capacity_running => JSON::false,
403 disk_size => 0,
404 instance_start_time => time(),
405 };
406
407 warn "## calculating disk_size\n";
408 $json->{disk_size} += -s $_ foreach @docs;
409 $status = 200;
410 return $json;
411 }
412
413 1;
414 __END__
415
416 =head1 SEE ALSO
417
418 L<http://wiki.apache.org/couchdb/Reference>
419

  ViewVC Help
Powered by ViewVC 1.1.26