Code reformatting.
[gruta.git] / Gruta / Source / FS.pm
bloba35287b69d3b3a4e9019a2cbb6a60ceb5df4e523
1 package Gruta::Source::FS;
3 use base 'Gruta::Source::BASE';
5 use strict;
6 use warnings;
8 use Gruta::Data;
10 package Gruta::Data::FS::BASE;
12 use Carp;
14 sub ext {
15 return '.M';
18 sub _filename {
19 my $self = shift;
21 $self->_assert();
22 $self->source->_assert();
24 return $self->source->{path} . $self->base() .
25 $self->get('id') . $self->ext();
29 sub load {
30 my $self = shift;
31 my $driver = shift;
33 $self->source( $driver );
35 # rename old .META files into .M
36 my $filename = $self->_filename();
37 rename($filename . 'ETA', $filename);
39 if (not open F, $filename) {
40 return undef;
43 while (<F>) {
44 chop;
46 if(/^([^:]*): (.*)$/) {
47 my ($key, $value) = ($1, $2);
49 $key =~ s/-/_/g;
51 if (grep (/^$key$/, $self->fields())) {
52 $self->set($key, $value);
57 close F;
59 return $self;
62 sub save {
63 my $self = shift;
64 my $driver = shift;
66 $self->source( $driver ) if $driver;
68 my $filename = $self->_filename();
70 open F, '>' . $filename or croak "Can't write " . $filename . ': ' . $!;
72 foreach my $k ($self->fields()) {
73 my $f = $k;
75 $f =~ s/_/-/g;
77 print F $f . ': ' . ($self->get($k) || '') . "\n";
80 close F;
82 return $self;
86 sub delete {
87 my $self = shift;
88 my $driver = shift;
90 $self->source( $driver ) if $driver;
92 unlink $self->_filename();
94 return $self;
97 package Gruta::Data::FS::Story;
99 use base 'Gruta::Data::Story';
100 use base 'Gruta::Data::FS::BASE';
102 use Carp;
104 sub base {
105 return Gruta::Data::FS::Topic::base() . $_[0]->get('topic_id') . '/';
108 sub fields {
109 grep !/(content|topic_id|abstract|body)/, $_[0]->SUPER::fields();
112 sub vfields {
113 return ($_[0]->SUPER::vfields(), 'content', 'topic_id', 'abstract', 'body');
116 sub _destroy_index {
117 my $self = shift;
119 my $filename = $self->_filename();
121 # destroy the topic index, to be rewritten
122 # in the future by _topic_index()
123 $filename =~ s!/[^/]+$!/.INDEX!;
124 unlink $filename;
127 sub save {
128 my $self = shift;
129 my $driver = shift;
131 $self->SUPER::save( $driver );
133 my $filename = $self->_filename();
134 $filename =~ s/\.M$//;
136 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
138 while (@d) {
139 my $ext = shift(@d);
140 my $field = shift(@d);
142 open F, '>' . $filename . $ext or
143 croak "Cannot write " . $filename . $ext . ': ' . $!;
144 print F $self->get($field) || '';
145 close F;
148 $self->_destroy_index();
150 return $self;
153 sub touch {
154 my $self = shift;
156 if (! $self->source->dummy_touch()) {
157 my $hits = $self->get('hits') + 1;
159 $self->set('hits', $hits);
161 # call $self->SUPER::save() instead of $self->save()
162 # to avoid saving content (unnecessary) and deleting
163 # the topic INDEX (even probably dangerous)
164 $self->SUPER::save();
166 $self->source->_update_top_ten($hits, $self->get('topic_id'),
167 $self->get('id'));
170 return $self;
173 sub tags {
174 my $self = shift;
175 my @ret = ();
177 my $filename = $self->_filename();
178 $filename =~ s/\.M$/.T/;
180 if (scalar(@_)) {
181 if (open F, '>' . $filename) {
182 print F join(', ', map { s/^\s+//; s/\s+$//; lc($_) } @_), "\n";
183 close F;
186 else {
187 if (open F, $filename) {
188 my $l = <F>;
189 close F;
191 chomp($l);
192 @ret = split(/\s*,\s*/, $l);
196 return @ret;
199 sub delete {
200 my $self = shift;
201 my $driver = shift;
203 my $file = $self->_filename();
205 $self->SUPER::delete($driver);
207 # also delete content and tags
208 $file =~ s/\.M$//;
210 unlink $file;
211 unlink $file . '.A';
212 unlink $file . '.B';
213 unlink $file . '.T';
215 $self->_destroy_index();
217 return $self;
221 sub load {
222 my $self = shift;
223 my $driver = shift;
225 if (!$self->SUPER::load( $driver )) {
226 return undef;
229 my $filename = $self->_filename();
230 $filename =~ s/\.M$//;
232 rename($filename . '.TAGS', $filename . '.T');
234 return $self;
238 package Gruta::Data::FS::Topic;
240 use base 'Gruta::Data::Topic';
241 use base 'Gruta::Data::FS::BASE';
243 sub base {
244 return '/topics/';
247 sub save {
248 my $self = shift;
249 my $driver = shift;
251 $self->SUPER::save( $driver );
253 my $filename = $self->_filename();
254 $filename =~ s/\.M$//;
256 mkdir $filename;
258 return $self;
261 package Gruta::Data::FS::User;
263 use base 'Gruta::Data::User';
264 use base 'Gruta::Data::FS::BASE';
266 sub ext {
267 return '';
270 sub base {
271 return '/users/';
274 package Gruta::Data::FS::Session;
276 use base 'Gruta::Data::Session';
277 use base 'Gruta::Data::FS::BASE';
279 sub ext {
280 return '';
283 sub base {
284 return '/sids/';
287 package Gruta::Source::FS;
289 use Carp;
291 sub _assert {
292 my $self = shift;
294 $self->{path} or croak "Invalid path";
296 return $self;
299 sub _one {
300 my $self = shift;
301 my $id = shift;
302 my $class = shift;
304 my $o = ${class}->new( id => $id );
305 $o->load( $self );
308 sub topic {
309 return _one( @_, 'Gruta::Data::FS::Topic' );
312 sub topics {
313 my $self = shift;
315 my @ret = ();
317 my $path = $self->{path} . Gruta::Data::FS::Topic::base();
319 if (opendir D, $path) {
320 while (my $id = readdir D) {
321 next unless -d $path . $id;
322 next if $id =~ /^\./;
324 push @ret, $id;
327 closedir D;
330 return @ret;
333 sub user {
334 return _one( @_, 'Gruta::Data::FS::User' );
337 sub users {
338 my $self = shift;
340 my @ret = ();
342 my $path = $self->{path} . Gruta::Data::FS::User::base();
344 if (opendir D, $path) {
345 while (my $id = readdir D) {
346 next if -d $path . $id;
347 push @ret, $id;
350 closedir D;
353 return @ret;
356 sub story {
357 my $self = shift;
358 my $topic_id = shift;
359 my $id = shift;
361 my $story;
363 if ($story = $self->cache_story($topic_id, $id)) {
364 return $story;
367 $story = Gruta::Data::FS::Story->new( topic_id => $topic_id, id => $id );
369 if (not $story->load( $self )) {
371 $story = Gruta::Data::FS::Story->new( topic_id => $topic_id . '-arch',
372 id => $id );
374 if (not $story->load( $self )) {
375 return undef;
379 # now load the content
380 my $file = $story->_filename();
381 $file =~ s/\.M$//;
383 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
385 while (@d) {
386 my $ext = shift(@d);
387 my $field = shift(@d);
389 if (open F, $file . $ext) {
390 $story->set($field, join('', <F>));
391 close F;
395 $self->cache_story($topic_id, $id, $story);
397 return $story;
400 sub stories {
401 my $self = shift;
402 my $topic_id = shift;
404 my @ret = ();
406 if (!$self->topic($topic_id)) {
407 return @ret;
410 my $path = $self->{path} . Gruta::Data::FS::Topic::base() . $topic_id;
412 if (opendir D, $path) {
413 while (my $id = readdir D) {
414 if ($id =~ s/\.M$// || $id =~ s/\.META$//) {
415 push(@ret, $id);
419 closedir D;
422 return @ret;
426 sub _topic_index {
427 my $self = shift;
428 my $topic_id = shift;
430 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . $topic_id;
432 if (! -d $index) {
433 return undef;
436 $index .= '/.INDEX';
438 if (not open I, $index) {
440 my @i = ();
441 foreach my $id ($self->stories($topic_id)) {
442 my $story = $self->story($topic_id, $id);
444 push(@i, ($story->get('date') || ('0' x 14)). ':' . $id);
447 open I, '>' . $index or croak "Can't create INDEX for $topic_id: $!";
448 flock I, 2;
450 foreach my $l (reverse(sort(@i))) {
451 print I $l, "\n";
455 close I;
457 return $index;
461 sub _update_top_ten {
462 my $self = shift;
463 my $hits = shift;
464 my $topic_id = shift;
465 my $id = shift;
467 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . '/.top_ten';
469 my $u = 0;
470 my @l = ();
472 if (open F, $index) {
473 flock F, 1;
474 while (my $l = <F>) {
475 chomp($l);
477 my ($h, $t, $i) = split(':', $l);
479 if ($u == 0 && $h < $hits) {
480 $u = 1;
481 push(@l, "$hits:$topic_id:$id");
484 if ($i ne $id or $t ne $topic_id) {
485 push(@l, $l);
489 close F;
492 if ($u == 0 && scalar(@l) < 100) {
493 $u = 1;
494 push(@l, "$hits:$topic_id:$id");
497 if ($u) {
498 if (open F, '>' . $index) {
499 flock F, 2;
500 my $n = 0;
502 foreach my $l (@l) {
503 print F $l, "\n";
505 if (++$n == 100) {
506 last;
510 close F;
514 return undef;
518 sub _stories_by_date {
519 my $self = shift;
520 my $topic_id = shift;
521 my %args = @_;
523 my @r = ();
525 my $i = $self->_topic_index($topic_id) or return @r;
526 open I, $i or return @r;
527 flock I, 1;
529 my $o = 0;
531 while (<I>) {
532 chomp;
534 my ($date, $id) = (/^(\d*):(.*)$/);
536 # skip future stories
537 next if not $args{future} and $date gt Gruta::Data::today();
539 # skip if date is above the threshold
540 next if $args{'to'} and $date gt $args{'to'};
542 # exit if date is below the threshold
543 last if $args{'from'} and $date lt $args{'from'};
545 # skip offset stories
546 next if $args{'offset'} and ++$o <= $args{'offset'};
548 push(@r, [ $topic_id, $id, $date ]);
550 # exit if we have all we need
551 last if $args{'num'} and $args{'num'} == scalar(@r);
554 close I;
556 return @r;
560 sub stories_by_date {
561 my $self = shift;
562 my $topics = shift;
563 my %args = @_;
565 my @topics;
567 if (!$topics) {
568 @topics = $self->topics();
570 else {
571 @topics = @{ $topics };
574 $args{offset} += 0;
575 $args{offset} = 0 if $args{offset} < 0;
577 # only one topic? execute it and return
578 if (scalar(@topics) == 1) {
579 return $self->_stories_by_date($topics[0], %args);
582 # more than one topic; 'num' and 'offset' need to be
583 # calculated from the full set
584 my @R = ();
586 foreach my $topic_id (@topics) {
588 my @r = $self->_stories_by_date($topic_id,
589 %args, num => 0, offset => 0);
591 push(@R, @r);
594 # sort by date
595 @R = sort { $b->[2] cmp $a->[2] } @R;
597 # split now
598 if ($args{num}) {
599 @R = @R[$args{offset} .. ($args{offset} + $args{num} - 1)];
601 else {
602 @R = @R[$args{offset} .. (scalar(@R) - 1)];
605 return grep { defined $_ } @R;
608 sub search_stories {
609 my $self = shift;
610 my $topic_id = shift;
611 my $query = shift;
612 my $future = shift;
614 my @q = split(/\s+/,$query);
616 my %r = ();
618 foreach my $id ($self->stories($topic_id)) {
620 my $story = $self->story($topic_id, $id);
622 if (!$future and $story->get('date') gt Gruta::Data::today()) {
623 next;
626 my $content = $story->get('content');
627 my $found = 0;
629 # try complete query first
630 if($content =~ /\b$query\b/i) {
631 $found = scalar(@q);
633 else {
634 # try separate words
635 foreach my $q (@q) {
636 if(length($q) > 1 and $content =~ /\b$q\b/i) {
637 $found++;
642 if ($found == scalar(@q)) {
643 $r{$id} = $story->get('title');
647 return sort { $r{$a} cmp $r{$b} } keys %r;
650 sub stories_top_ten {
651 my $self = shift;
652 my $num = shift;
654 my @r = ();
656 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . '/.top_ten';
658 if (open F, $index) {
659 flock F, 1;
661 while (defined(my $l = <F>) and $num--) {
662 chomp($l);
663 push(@r, [ split(':', $l) ]);
666 close F;
669 return @r;
673 sub _collect_tags {
674 my $self = shift;
675 my @topics = @_;
677 my @ret = ();
679 foreach my $topic_id (@topics) {
681 my $topic = $self->topic($topic_id);
683 my $files = $topic->_filename();
684 $files =~ s/\.M$/\/*.T/;
686 my @ls = glob($files);
688 foreach my $f (@ls) {
689 if (open F, $f) {
690 my $tags = <F>;
691 chomp $tags;
692 close F;
694 my ($id) = ($f =~ m{/([^/]+)\.T});
696 push(@ret,
697 [ $topic_id, $id, [ split(/\s*,\s*/, $tags) ] ]
703 return @ret;
707 sub stories_by_tag {
708 my $self = shift;
709 my $topics = shift;
710 my $tag = shift;
711 my $future = shift;
713 my @tags = map { lc($_) } split(/\s*,\s*/, $tag);
715 my @topics;
717 if (!$topics) {
718 @topics = $self->topics();
720 else {
721 @topics = @{ $topics };
724 my %r = ();
726 foreach my $tr ($self->_collect_tags(@topics)) {
728 my @ts = @{$tr->[2]};
730 # skip stories with less tags than the wanted ones
731 if (scalar(@ts) < scalar(@tags)) {
732 next;
735 # count matches
736 my $c = 0;
738 foreach my $t (@ts) {
739 if (grep(/^$t$/, @tags)) {
740 $c++;
744 if ($c >= scalar(@tags)) {
746 my $story = $self->story($tr->[0], $tr->[1]);
748 # if no future stories are wanted, discard them
749 if (!$future) {
750 if ($story->get('date') gt Gruta::Data::today()) {
751 next;
755 $r{$story->get('title')} =
756 [ $tr->[0], $tr->[1], $story->get('date') ];
760 return map { $r{$_} } sort keys %r;
764 sub tags {
765 my $self = shift;
767 my @ret = ();
768 my %h = ();
770 foreach my $tr ($self->_collect_tags($self->topics())) {
772 foreach my $t (@{$tr->[2]}) {
773 $h{$t}++;
777 foreach my $k (keys(%h)) {
778 push(@ret, [ $k, $h{$k} ]);
781 return sort { $a->[0] cmp $b->[0] } @ret;
785 sub session {
786 return _one( @_, 'Gruta::Data::FS::Session' );
789 sub purge_old_sessions {
790 my $self = shift;
792 my $path = $self->{path} . Gruta::Data::FS::Session::base();
794 if (opendir D, $path) {
795 while(my $s = readdir D) {
796 my $f = $path . $s;
798 next if -d $f;
800 if (-M $f > 1) {
801 unlink $f;
805 closedir D;
808 return undef;
812 sub _insert {
813 my $self = shift;
814 my $obj = shift;
815 my $class = shift;
817 bless($obj, $class);
818 $obj->save( $self );
820 return $obj;
823 sub insert_topic {
824 $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic');
827 sub insert_user {
828 $_[0]->_insert($_[1], 'Gruta::Data::FS::User');
831 sub insert_story {
832 my $self = shift;
833 my $story = shift;
835 if (not $story->get('id')) {
836 # alloc an id for the story
837 my $id = undef;
839 do {
840 $id = $story->new_id();
842 } while $self->story($story->get('topic_id'), $id);
844 $story->set('id', $id);
847 $self->_insert($story, 'Gruta::Data::FS::Story');
848 return $story;
851 sub insert_session {
852 $_[0]->_insert($_[1], 'Gruta::Data::FS::Session');
856 sub create {
857 my $self = shift;
859 my @l = map { $self->{path} . $_ } (
860 Gruta::Data::FS::Topic::base(),
861 Gruta::Data::FS::User::base(),
862 Gruta::Data::FS::Session::base()
865 foreach my $d (@l) {
866 if (! -d $d) {
867 mkdir $d, 0755 or die "Cannot mkdir $d";
871 return $self;
875 sub new {
876 my $class = shift;
878 my $s = bless( { @_ }, $class);
880 $s->_assert();
882 $s->create();
884 return $s;