1 package Gruta
::Source
::FS
;
3 use base
'Gruta::Source::BASE';
10 package Gruta
::Data
::FS
::BASE
;
22 $self->source->_assert();
24 return $self->source->{path
} . $self->base() .
25 $self->get('id') . $self->ext();
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) {
46 if(/^([^:]*): (.*)$/) {
47 my ($key, $value) = ($1, $2);
51 if (grep (/^$key$/, $self->fields())) {
52 $self->set($key, $value);
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()) {
77 print F
$f . ': ' . ($self->get($k) || '') . "\n";
90 $self->source( $driver ) if $driver;
92 unlink $self->_filename();
97 package Gruta
::Data
::FS
::Story
;
99 use base
'Gruta::Data::Story';
100 use base
'Gruta::Data::FS::BASE';
105 return Gruta
::Data
::FS
::Topic
::base
() . $_[0]->get('topic_id') . '/';
109 grep !/(content|topic_id|abstract|body)/, $_[0]->SUPER::fields
();
113 return ($_[0]->SUPER::vfields
(), 'content', 'topic_id', 'abstract', 'body');
119 my $filename = $self->_filename();
121 # destroy the topic index, to be rewritten
122 # in the future by _topic_index()
123 $filename =~ s!/[^/]+$!/.INDEX!;
131 $self->SUPER::save
( $driver );
133 my $filename = $self->_filename();
134 $filename =~ s/\.M$//;
136 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
140 my $field = shift(@d);
142 open F
, '>' . $filename . $ext or
143 croak
"Cannot write " . $filename . $ext . ': ' . $!;
144 print F
$self->get($field) || '';
148 $self->_destroy_index();
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'),
177 my $filename = $self->_filename();
178 $filename =~ s/\.M$/.T/;
181 if (open F
, '>' . $filename) {
182 print F
join(', ', map { s/^\s+//; s/\s+$//; lc($_) } @_), "\n";
187 if (open F
, $filename) {
192 @ret = split(/\s*,\s*/, $l);
203 my $file = $self->_filename();
205 $self->SUPER::delete($driver);
207 # also delete content and tags
215 $self->_destroy_index();
225 if (!$self->SUPER::load
( $driver )) {
229 my $filename = $self->_filename();
230 $filename =~ s/\.M$//;
232 rename($filename . '.TAGS', $filename . '.T');
238 package Gruta
::Data
::FS
::Topic
;
240 use base
'Gruta::Data::Topic';
241 use base
'Gruta::Data::FS::BASE';
251 $self->SUPER::save
( $driver );
253 my $filename = $self->_filename();
254 $filename =~ s/\.M$//;
261 package Gruta
::Data
::FS
::User
;
263 use base
'Gruta::Data::User';
264 use base
'Gruta::Data::FS::BASE';
274 package Gruta
::Data
::FS
::Session
;
276 use base
'Gruta::Data::Session';
277 use base
'Gruta::Data::FS::BASE';
287 package Gruta
::Source
::FS
;
294 $self->{path
} or croak
"Invalid path";
304 my $o = ${class}->new( id
=> $id );
309 return _one
( @_, 'Gruta::Data::FS::Topic' );
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 =~ /^\./;
334 return _one
( @_, 'Gruta::Data::FS::User' );
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;
358 my $topic_id = shift;
363 if ($story = $self->cache_story($topic_id, $id)) {
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',
374 if (not $story->load( $self )) {
379 # now load the content
380 my $file = $story->_filename();
383 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
387 my $field = shift(@d);
389 if (open F
, $file . $ext) {
390 $story->set($field, join('', <F
>));
395 $self->cache_story($topic_id, $id, $story);
402 my $topic_id = shift;
406 if (!$self->topic($topic_id)) {
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$//) {
428 my $topic_id = shift;
430 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
438 if (not open I
, $index) {
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: $!";
450 foreach my $l (reverse(sort(@i))) {
461 sub _update_top_ten
{
464 my $topic_id = shift;
467 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
472 if (open F
, $index) {
474 while (my $l = <F
>) {
477 my ($h, $t, $i) = split(':', $l);
479 if ($u == 0 && $h < $hits) {
481 push(@l, "$hits:$topic_id:$id");
484 if ($i ne $id or $t ne $topic_id) {
492 if ($u == 0 && scalar(@l) < 100) {
494 push(@l, "$hits:$topic_id:$id");
498 if (open F
, '>' . $index) {
518 sub _stories_by_date
{
520 my $topic_id = shift;
525 my $i = $self->_topic_index($topic_id) or return @r;
526 open I
, $i or return @r;
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);
560 sub stories_by_date
{
568 @topics = $self->topics();
571 @topics = @
{ $topics };
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
586 foreach my $topic_id (@topics) {
588 my @r = $self->_stories_by_date($topic_id,
589 %args, num
=> 0, offset
=> 0);
595 @R = sort { $b->[2] cmp $a->[2] } @R;
599 @R = @R[$args{offset
} .. ($args{offset
} + $args{num
} - 1)];
602 @R = @R[$args{offset
} .. (scalar(@R) - 1)];
605 return grep { defined $_ } @R;
610 my $topic_id = shift;
614 my @q = split(/\s+/,$query);
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
()) {
626 my $content = $story->get('content');
629 # try complete query first
630 if($content =~ /\b$query\b/i) {
636 if(length($q) > 1 and $content =~ /\b$q\b/i) {
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
{
656 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
658 if (open F
, $index) {
661 while (defined(my $l = <F
>) and $num--) {
663 push(@r, [ split(':', $l) ]);
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) {
694 my ($id) = ($f =~ m{/([^/]+)\.T});
697 [ $topic_id, $id, [ split(/\s*,\s*/, $tags) ] ]
713 my @tags = map { lc($_) } split(/\s*,\s*/, $tag);
718 @topics = $self->topics();
721 @topics = @
{ $topics };
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)) {
738 foreach my $t (@ts) {
739 if (grep(/^$t$/, @tags)) {
744 if ($c >= scalar(@tags)) {
746 my $story = $self->story($tr->[0], $tr->[1]);
748 # if no future stories are wanted, discard them
750 if ($story->get('date') gt Gruta
::Data
::today
()) {
755 $r{$story->get('title')} =
756 [ $tr->[0], $tr->[1], $story->get('date') ];
760 return map { $r{$_} } sort keys %r;
770 foreach my $tr ($self->_collect_tags($self->topics())) {
772 foreach my $t (@
{$tr->[2]}) {
777 foreach my $k (keys(%h)) {
778 push(@ret, [ $k, $h{$k} ]);
781 return sort { $a->[0] cmp $b->[0] } @ret;
786 return _one
( @_, 'Gruta::Data::FS::Session' );
789 sub purge_old_sessions
{
792 my $path = $self->{path
} . Gruta
::Data
::FS
::Session
::base
();
794 if (opendir D
, $path) {
795 while(my $s = readdir D
) {
824 $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic');
828 $_[0]->_insert($_[1], 'Gruta::Data::FS::User');
835 if (not $story->get('id')) {
836 # alloc an id for the story
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');
852 $_[0]->_insert($_[1], 'Gruta::Data::FS::Session');
859 my @l = map { $self->{path
} . $_ } (
860 Gruta
::Data
::FS
::Topic
::base
(),
861 Gruta
::Data
::FS
::User
::base
(),
862 Gruta
::Data
::FS
::Session
::base
()
867 mkdir $d, 0755 or die "Cannot mkdir $d";
878 my $s = bless( { @_ }, $class);