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
::Data
::FS
::Template
;
289 use base
'Gruta::Data::Template';
290 use base
'Gruta::Data::FS::BASE';
293 return '/templates/';
304 $self->source($driver);
306 if (not open(F
, $self->_filename())) {
310 $self->set('content', join('', <F
>));
321 $self->source($driver) if $driver;
323 if (not open(F
, '>' . $self->_filename())) {
327 print F
$self->get('content');
334 package Gruta
::Data
::FS
::Comment
;
336 use base
'Gruta::Data::Comment';
337 use base
'Gruta::Data::FS::BASE';
346 return '/comments/' . $_[0]->get('topic_id') . '/'
347 . $_[0]->get('story_id') . '/';
351 grep !/content/, $_[0]->SUPER::fields
();
355 return ($_[0]->SUPER::vfields
(), 'content');
362 my @p = split('/', $self->_filename());
367 my $pending = join('/', @p) . '/.pending/' .
369 $self->get('topic_id'),
370 $self->get('story_id'),
382 $self->source($driver) if $driver;
384 # create the directory tree
385 my @p = split('/', $self->_filename());
392 $self->SUPER::save
($driver);
395 my $filename = $self->_filename();
396 $filename =~ s/\.M$//;
398 open F
, '>' . $filename or
399 croak
"Cannot write " . $filename . ': ' . $!;
401 print F
$self->get('content') || '';
405 open F
, '>' . $self->pending_file();
416 if (!$self->SUPER::load
($driver)) {
420 my $filename = $self->_filename();
421 $filename =~ s/\.M$//;
423 if (open F
, $filename) {
424 $self->set('content', join('', <F
>));
437 my $file = $self->_filename();
441 # delete (possible) pending
442 unlink $self->pending_file();
444 $self->SUPER::delete($driver);
451 $self->set('approved', 1);
454 # delete (possible) pending
455 unlink $self->pending_file();
461 package Gruta
::Source
::FS
;
468 $self->{path
} or croak
"Invalid path";
478 my $o = ${class}->new( id
=> $id );
483 return _one
( @_, 'Gruta::Data::FS::Topic' );
491 my $path = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
();
493 if (opendir D
, $path) {
494 while (my $id = readdir D
) {
495 next unless -d
$path . $id;
496 next if $id =~ /^\./;
508 return _one
( @_, 'Gruta::Data::FS::User' );
516 my $path = $self->{path
} . Gruta
::Data
::FS
::User
::base
();
518 if (opendir D
, $path) {
519 while (my $id = readdir D
) {
520 next if -d
$path . $id;
531 return _one
(@_, 'Gruta::Data::FS::Template');
539 my $path = $self->{path
} . Gruta
::Data
::FS
::Template
::base
();
541 if (opendir D
, $path) {
542 while (my $id = readdir D
) {
543 next if -d
$path . $id;
556 my $topic_id = shift;
557 my $story_id = shift;
560 my $comment = Gruta
::Data
::FS
::Comment
->new(
561 topic_id
=> $topic_id,
562 story_id
=> $story_id,
566 if (not $comment->load($self)) {
574 sub pending_comments
{
579 my $path = $self->{path
} . Gruta
::Data
::FS
::Comment
::base
()
582 if (opendir D
, $path) {
583 while (my $id = readdir D
) {
594 push @ret, [ split(':', $id) ];
611 my $topic_id = $story->get('topic_id');
612 my $story_id = $story->get('id');
614 my $base_path = $self->{path
} . Gruta
::Data
::FS
::Comment
::base
();
616 my $pend_path = $base_path . '/.pending/';
617 my $path = join('/', ($base_path, $topic_id, $story_id)) . '/';
619 if (opendir D
, $path) {
620 while (my $id = readdir D
) {
624 next if $f =~ /\.M$/;
626 my $pf = $pend_path . join(':', ($topic_id, $story_id, $id));
635 # not all wanted and this comment not approved? skip
636 if (!$all && -f
$pf) {
640 push @ret, [ $topic_id, $story_id, $id ];
652 my $topic_id = shift;
657 if ($story = $self->cache_story($topic_id, $id)) {
661 $story = Gruta
::Data
::FS
::Story
->new( topic_id
=> $topic_id, id
=> $id );
663 if (not $story->load( $self )) {
665 $story = Gruta
::Data
::FS
::Story
->new( topic_id
=> $topic_id . '-arch',
668 if (not $story->load( $self )) {
673 # now load the content
674 my $file = $story->_filename();
677 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
681 my $field = shift(@d);
683 if (open F
, $file . $ext) {
684 $story->set($field, join('', <F
>));
689 $self->cache_story($topic_id, $id, $story);
696 my $topic_id = shift;
700 if (!$self->topic($topic_id)) {
704 my $path = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
706 if (opendir D
, $path) {
707 while (my $id = readdir D
) {
708 if ($id =~ s/\.M$// || $id =~ s/\.META$//) {
722 my $topic_id = shift;
724 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
732 if (not open I
, $index) {
735 foreach my $id ($self->stories($topic_id)) {
736 my $story = $self->story($topic_id, $id);
738 push(@i, ($story->get('date') || ('0' x
14)). ':' . $id);
741 open I
, '>' . $index or croak
"Can't create INDEX for $topic_id: $!";
744 foreach my $l (reverse(sort(@i))) {
755 sub _update_top_ten
{
758 my $topic_id = shift;
761 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
766 if (open F
, $index) {
768 while (my $l = <F
>) {
771 my ($h, $t, $i) = split(':', $l);
773 if ($u == 0 && $h < $hits) {
775 push(@l, "$hits:$topic_id:$id");
778 if ($i ne $id or $t ne $topic_id) {
786 if ($u == 0 && scalar(@l) < $self->{hard_top_ten_limit
}) {
788 push(@l, "$hits:$topic_id:$id");
792 if (open F
, '>' . $index) {
799 if (++$n == $self->{hard_top_ten_limit
}) {
812 sub _stories_by_date
{
814 my $topic_id = shift;
819 my $i = $self->_topic_index($topic_id) or return @r;
820 open I
, $i or return @r;
828 my ($date, $id) = split(/:/);
830 # skip future stories
831 next if not $args{future
} and $date gt Gruta
::Data
::today
();
833 # skip if date is above the threshold
834 next if $args{'to'} and $date gt $args{'to'};
836 # exit if date is below the threshold
837 last if $args{'from'} and $date lt $args{'from'};
839 # skip offset stories
840 next if $args{'offset'} and ++$o <= $args{'offset'};
842 push(@r, [ $topic_id, $id, $date ]);
844 # exit if we have all we need
845 last if $args{'num'} and $args{'num'} == scalar(@r);
854 sub stories_by_date
{
862 @topics = $self->topics();
865 @topics = @
{ $topics };
869 $args{offset
} = 0 if $args{offset
} < 0;
871 # only one topic? execute it and return
872 if (scalar(@topics) == 1) {
873 return $self->_stories_by_date($topics[0], %args);
876 # more than one topic; 'num' and 'offset' need to be
877 # calculated from the full set
880 foreach my $topic_id (@topics) {
882 my @r = $self->_stories_by_date($topic_id,
883 %args, num
=> 0, offset
=> 0);
889 @R = sort { $b->[2] cmp $a->[2] } @R;
893 @R = @R[$args{offset
} .. ($args{offset
} + $args{num
} - 1)];
896 @R = @R[$args{offset
} .. (scalar(@R) - 1)];
899 return grep { defined $_ } @R;
904 my $topic_id = shift;
908 my @q = split(/\s+/,$query);
912 foreach my $id ($self->stories($topic_id)) {
914 my $story = $self->story($topic_id, $id);
916 if (!$future and $story->get('date') gt Gruta
::Data
::today
()) {
920 my $content = $story->get('content');
923 # try complete query first
924 if($content =~ /\b$query\b/i) {
930 if(length($q) > 1 and $content =~ /\b$q\b/i) {
936 if ($found == scalar(@q)) {
937 $r{$id} = $story->get('title');
941 return sort { $r{$a} cmp $r{$b} } keys %r;
944 sub stories_by_text
{
954 @topics = $self->topics();
957 @topics = @
{ $topics };
960 foreach my $t (@topics) {
961 foreach my $id ($self->search_stories($t, $query, $future)) {
962 push(@ret, [ $t, $id ]);
969 sub stories_top_ten
{
975 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
977 if (open F
, $index) {
980 while (defined(my $l = <F
>) and $num--) {
982 push(@r, [ split(':', $l) ]);
998 foreach my $topic_id (@topics) {
1000 my $topic = $self->topic($topic_id);
1002 my $files = $topic->_filename();
1003 $files =~ s/\.M$/\/*.T
/;
1005 my @ls = glob($files);
1007 foreach my $f (@ls) {
1013 my ($id) = ($f =~ m{/([^/]+)\.T});
1016 [ $topic_id, $id, [ split(/\s*,\s*/, $tags) ] ]
1026 sub stories_by_tag
{
1032 my @tags = map { lc($_) } split(/\s*,\s*/, $tag);
1037 @topics = $self->topics();
1040 @topics = @
{ $topics };
1045 foreach my $tr ($self->_collect_tags(@topics)) {
1047 my @ts = @
{$tr->[2]};
1049 # skip stories with less tags than the wanted ones
1050 if (scalar(@ts) < scalar(@tags)) {
1057 foreach my $t (@ts) {
1058 if (grep(/^$t$/, @tags)) {
1063 if ($c >= scalar(@tags)) {
1065 my $story = $self->story($tr->[0], $tr->[1]);
1067 # if no future stories are wanted, discard them
1069 if ($story->get('date') gt Gruta
::Data
::today
()) {
1074 $r{$story->get('title')} =
1075 [ $tr->[0], $tr->[1], $story->get('date') ];
1079 return map { $r{$_} } sort keys %r;
1089 foreach my $tr ($self->_collect_tags($self->topics())) {
1091 foreach my $t (@
{$tr->[2]}) {
1096 foreach my $k (keys(%h)) {
1097 push(@ret, [ $k, $h{$k} ]);
1100 return sort { $a->[0] cmp $b->[0] } @ret;
1105 return _one
( @_, 'Gruta::Data::FS::Session' );
1108 sub purge_old_sessions
{
1111 my $path = $self->{path
} . Gruta
::Data
::FS
::Session
::base
();
1113 if (opendir D
, $path) {
1114 while(my $s = readdir D
) {
1136 bless($obj, $class);
1137 $obj->save( $self );
1143 $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic');
1147 $_[0]->_insert($_[1], 'Gruta::Data::FS::User');
1150 sub insert_template
{
1151 $_[0]->_insert($_[1], 'Gruta::Data::FS::Template');
1154 sub insert_comment
{
1156 my $comment = shift;
1158 if (!$comment->setup($self)) {
1162 $self->_insert($comment, 'Gruta::Data::FS::Comment');
1169 if (not $story->get('id')) {
1170 # alloc an id for the story
1174 $id = $story->new_id();
1176 } while $self->story($story->get('topic_id'), $id);
1178 $story->set('id', $id);
1181 $self->_insert($story, 'Gruta::Data::FS::Story');
1185 sub insert_session
{
1186 $_[0]->_insert($_[1], 'Gruta::Data::FS::Session');
1193 my @l = map { $self->{path
} . $_ } (
1194 Gruta
::Data
::FS
::Topic
::base
(),
1195 Gruta
::Data
::FS
::User
::base
(),
1196 Gruta
::Data
::FS
::Session
::base
(),
1197 Gruta
::Data
::FS
::Template
::base
(),
1198 Gruta
::Data
::FS
::Comment
::base
(),
1199 Gruta
::Data
::FS
::Comment
::base
() . '/.pending/'
1202 foreach my $d (@l) {
1204 mkdir $d, 0755 or die "Cannot mkdir $d";
1215 my $s = bless( { @_ }, $class);
1217 $s->{hard_top_ten_limit
} ||= 100;