Fixed stupidity in FS's tags().
[gruta.git] / Gruta / Source / FS.pm
blob9f3d6b3fecc5c7ff79b4ff87cc57f5908b35ff6d
1 package Gruta::Source::FS;
3 use strict;
4 use warnings;
6 use Gruta::Data;
8 package Gruta::Data::FS::BASE;
10 use Carp;
12 sub ext { return '.META'; }
14 sub _filename {
15 my $self = shift;
17 $self->_assert();
18 $self->source->_assert();
20 return $self->source->{path} . $self->base() .
21 $self->get('id') . $self->ext();
25 sub load {
26 my $self = shift;
27 my $driver = shift;
29 $self->source( $driver );
31 if (not open F, $self->_filename()) {
32 return undef;
35 while (<F>) {
36 chop;
38 if(/^([^:]*): (.*)$/) {
39 my ($key, $value) = ($1, $2);
41 $key =~ s/-/_/g;
43 if (grep (/^$key$/, $self->fields())) {
44 $self->set($key, $value);
49 close F;
51 return $self;
54 sub save {
55 my $self = shift;
56 my $driver = shift;
58 $self->source( $driver ) if $driver;
60 my $filename = $self->_filename();
62 open F, '>' . $filename or croak "Can't write " . $filename . ': ' . $!;
64 foreach my $k ($self->fields()) {
65 my $f = $k;
67 $f =~ s/_/-/g;
69 print F $f . ': ' . ($self->get($k) || '') . "\n";
72 close F;
74 return $self;
78 sub delete {
79 my $self = shift;
80 my $driver = shift;
82 $self->source( $driver ) if $driver;
84 unlink $self->_filename();
86 return $self;
89 package Gruta::Data::FS::Story;
91 use base 'Gruta::Data::Story';
92 use base 'Gruta::Data::FS::BASE';
94 use Carp;
96 sub base { return Gruta::Data::FS::Topic::base() . $_[0]->get('topic_id') . '/'; }
98 sub fields { grep !/content/, $_[0]->SUPER::fields(); }
99 sub vfields { return ($_[0]->SUPER::vfields(), 'content'); }
101 sub save {
102 my $self = shift;
103 my $driver = shift;
105 $self->SUPER::save( $driver );
107 my $filename = $self->_filename();
108 $filename =~ s/\.META$//;
110 open F, '>' . $filename or croak "Can't write " . $filename . ': ' . $!;
112 print F $self->get('content') || '';
113 close F;
115 # destroy the topic index, to be rewritten
116 # in the future by _topic_index()
117 $filename =~ s!/[^/]+$!/.INDEX!;
118 unlink $filename;
120 return $self;
123 sub touch {
124 my $self = shift;
126 my $hits = $self->get('hits') + 1;
128 $self->set('hits', $hits);
129 $self->save();
131 $self->source->_update_top_ten($hits, $self->get('topic_id'),
132 $self->get('id'));
134 return $self;
137 sub tags {
138 my $self = shift;
139 my @ret = undef;
141 my $filename = $self->_filename();
142 $filename =~ s/\.META$/.TAGS/;
144 if (scalar(@_)) {
145 if (open F, '>' . $filename) {
146 print F join(', ', @_), "\n";
147 close F;
150 else {
151 if (open F, $filename) {
152 my $l = <F>;
153 close F;
155 chomp($l);
156 @ret = split(/,\s+/, $l);
160 return @ret;
163 package Gruta::Data::FS::Topic;
165 use base 'Gruta::Data::Topic';
166 use base 'Gruta::Data::FS::BASE';
168 sub base { return '/topics/'; }
170 sub save {
171 my $self = shift;
172 my $driver = shift;
174 $self->SUPER::save( $driver );
176 my $filename = $self->_filename();
177 $filename =~ s/\.META$//;
179 mkdir $filename;
181 return $self;
184 package Gruta::Data::FS::User;
186 use base 'Gruta::Data::User';
187 use base 'Gruta::Data::FS::BASE';
189 sub ext { return ''; }
190 sub base { return '/users/'; }
192 package Gruta::Data::FS::Session;
194 use base 'Gruta::Data::Session';
195 use base 'Gruta::Data::FS::BASE';
197 sub ext { return ''; }
198 sub base { return '/sids/'; }
200 package Gruta::Source::FS;
202 use Carp;
204 sub _assert {
205 my $self = shift;
207 $self->{path} or croak "Invalid path";
209 return $self;
212 sub _one {
213 my $self = shift;
214 my $id = shift;
215 my $class = shift;
217 my $o = ${class}->new( id => $id );
218 $o->load( $self );
221 sub topic { return _one( @_, 'Gruta::Data::FS::Topic' ); }
223 sub topics {
224 my $self = shift;
226 my @ret = ();
228 my $path = $self->{path} . Gruta::Data::FS::Topic::base();
230 if (opendir D, $path) {
231 while (my $id = readdir D) {
232 next unless -d $path . $id;
233 next if $id =~ /^\./;
235 push @ret, $id;
238 closedir D;
241 return @ret;
244 sub user { return _one( @_, 'Gruta::Data::FS::User' ); }
246 sub users {
247 my $self = shift;
249 my @ret = ();
251 my $path = $self->{path} . Gruta::Data::FS::User::base();
253 if (opendir D, $path) {
254 while (my $id = readdir D) {
255 next if -d $path . $id;
256 push @ret, $id;
259 closedir D;
262 return @ret;
265 sub story {
266 my $self = shift;
267 my $topic_id = shift;
268 my $id = shift;
270 my $story = Gruta::Data::FS::Story->new( topic_id => $topic_id, id => $id );
271 if (not $story->load( $self )) {
273 $story = Gruta::Data::FS::Story->new( topic_id => $topic_id . '-arch',
274 id => $id );
276 if (not $story->load( $self )) {
277 return undef;
281 # now load the content
282 my $file = $story->_filename();
283 $file =~ s/\.META$//;
285 open F, $file or croak "Can't open $file content: $!";
287 $story->set('content', join('', <F>));
288 close F;
290 return $story;
293 sub stories {
294 my $self = shift;
295 my $topic_id = shift;
297 my @ret = ();
299 my $path = $self->{path} . Gruta::Data::FS::Topic::base() . $topic_id;
301 if (opendir D, $path) {
302 while (my $id = readdir D) {
303 if ($id =~ s/\.META$//) {
304 push(@ret, $id);
308 closedir D;
311 return @ret;
315 sub _topic_index {
316 my $self = shift;
317 my $topic_id = shift;
319 my $index = $self->{path} . Gruta::Data::FS::Topic::base() .
320 $topic_id . '/.INDEX';
322 if (not open I, $index) {
324 my @i = ();
325 foreach my $id ($self->stories($topic_id)) {
326 my $story = $self->story($topic_id, $id);
328 push(@i, $story->get('date') . ':' . $id);
331 open I, '>' . $index or croak "Can't create INDEX for $topic_id: $!";
332 flock I, 2;
334 foreach my $l (reverse(sort(@i))) {
335 print I $l, "\n";
339 close I;
341 return $index;
345 sub _update_top_ten {
346 my $self = shift;
347 my $hits = shift;
348 my $topic_id = shift;
349 my $id = shift;
351 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . '/.top_ten';
353 my $u = 0;
354 my @l = ();
356 if (open F, $index) {
357 flock F, 1;
358 while (my $l = <F>) {
359 chomp($l);
361 my ($h, $t, $i) = split(':', $l);
363 if ($u == 0 && $h < $hits) {
364 $u = 1;
365 push(@l, "$hits:$topic_id:$id");
368 if ($t ne $topic_id or $i ne $id) {
369 push(@l, $l);
373 close F;
376 if ($u == 0 && scalar(@l) < 100) {
377 $u = 1;
378 push(@l, "$hits:$topic_id:$id");
381 if ($u) {
382 if (open F, '>' . $index) {
383 flock F, 2;
384 my $n = 0;
386 foreach my $l (@l) {
387 print F $l, "\n";
389 if (++$n == 100) {
390 last;
394 close F;
398 return undef;
402 sub stories_by_date {
403 my $self = shift;
404 my $topic_id = shift;
405 my %args = @_;
407 $args{offset} += 0;
408 $args{offset} = 0 if $args{offset} < 0;
410 open I, $self->_topic_index($topic_id);
411 flock I, 1;
413 my @r = ();
414 my $o = 0;
416 while(<I>) {
417 chomp;
419 my ($date, $id) = (/^(\d*):(.*)$/);
421 # skip future stories
422 next if not $args{future} and
423 $args{today} and
424 $date > $args{today};
426 # skip if date is above the threshold
427 next if $args{'to'} and $date > $args{'to'};
429 # exit if date is below the threshold
430 last if $args{'from'} and $date < $args{'from'};
432 # skip offset stories
433 next if $args{'offset'} and ++$o <= $args{'offset'};
435 push(@r, $id);
437 # exit if we have all we need
438 last if $args{'num'} and $args{'num'} == scalar(@r);
441 close I;
443 return @r;
446 sub search_stories {
447 my $self = shift;
448 my $topic_id = shift;
449 my $query = shift;
451 my @q = split(/\s+/,$query);
453 my @r = ();
455 foreach my $id ($self->stories_by_date( $topic_id )) {
457 my $story = $self->story($topic_id, $id);
458 my $content = $story->get('content');
459 my $found = 0;
461 # try complete query first
462 if($content =~ /\b$query\b/i) {
463 $found = 1;
465 else {
466 # try separate words
467 foreach my $q (@q) {
468 if(length($q) > 1 and $content =~ /\b$q\b/i) {
469 $found = 1;
470 last;
475 push(@r, $id) if $found;
478 return @r;
481 sub stories_top_ten {
482 my $self = shift;
483 my $num = shift;
485 my @r = ();
487 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . '/.top_ten';
489 if (open F, $index) {
490 flock F, 1;
492 while (defined(my $l = <F>) and $num--) {
493 chomp($l);
494 push(@r, [ split(':', $l) ]);
497 close F;
500 return @r;
504 sub stories_by_tag {
505 my $self = shift;
506 my @tags = shift;
508 return ();
512 sub tags {
513 my $self = shift;
515 return ();
519 sub session { return _one( @_, 'Gruta::Data::FS::Session' ); }
521 sub purge_old_sessions {
522 my $self = shift;
524 my $path = $self->{path} . Gruta::Data::FS::Session::base();
526 if (opendir D, $path) {
527 while(my $s = readdir D) {
528 my $f = $path . $s;
530 next if -d $f;
532 if (-M $f > 1) {
533 unlink $f;
537 closedir D;
540 return undef;
544 sub _insert {
545 my $self = shift;
546 my $obj = shift;
547 my $class = shift;
549 bless($obj, $class);
550 $obj->save( $self );
552 return $obj;
555 sub insert_topic { $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic'); }
556 sub insert_user { $_[0]->_insert($_[1], 'Gruta::Data::FS::User'); }
558 sub insert_story {
559 my $self = shift;
560 my $story = shift;
562 if (not $story->get('id')) {
563 # alloc an id for the story
564 my $id = time();
566 while ($self->story($story->get('topic_id'), $id)) {
567 $id++;
570 $story->set('id', $id);
573 $self->_insert($story, 'Gruta::Data::FS::Story');
574 return $story;
577 sub insert_session { $_[0]->_insert($_[1], 'Gruta::Data::FS::Session'); }
580 sub create {
581 my $self = shift;
583 mkdir $self->{path}, 0755;
584 mkdir $self->{path} . Gruta::Data::FS::Topic::base(), 0755;
585 mkdir $self->{path} . Gruta::Data::FS::User::base(), 0755;
586 mkdir $self->{path} . Gruta::Data::FS::Session::base(), 0755;
590 sub new {
591 my $class = shift;
593 my $s = bless( { @_ }, $class);
595 $s->_assert();
597 return $s;