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) {
48 if(/^([^:]*): (.*)$/) {
49 my ($key, $value) = ($1, $2);
53 if (grep (/^$key$/, $self->fields())) {
54 $self->set($key, $value);
68 $self->source( $driver ) if $driver;
70 my $filename = $self->_filename();
72 open F
, '>' . $filename or croak
"Can't write " . $filename . ': ' . $!;
75 foreach my $k ($self->fields()) {
80 print F
$f . ': ' . ($self->get($k) || '') . "\n";
93 $self->source( $driver ) if $driver;
95 unlink $self->_filename();
100 package Gruta
::Data
::FS
::Story
;
102 use base
'Gruta::Data::Story';
103 use base
'Gruta::Data::FS::BASE';
108 return Gruta
::Data
::FS
::Topic
::base
() . $_[0]->get('topic_id') . '/';
112 grep !/(content|topic_id|abstract|body)/, $_[0]->SUPER::fields
();
116 return ($_[0]->SUPER::vfields
(), 'content', 'topic_id', 'abstract', 'body');
122 my $filename = $self->_filename();
124 # destroy the topic index, to be rewritten
125 # in the future by _topic_index()
126 $filename =~ s!/[^/]+$!/.INDEX!;
134 $self->SUPER::save
( $driver );
136 my $filename = $self->_filename();
137 $filename =~ s/\.M$//;
139 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
143 my $field = shift(@d);
145 open F
, '>' . $filename . $ext or
146 croak
"Cannot write " . $filename . $ext . ': ' . $!;
147 print F
$self->get($field) || '';
151 $self->_destroy_index();
159 if (! $self->source->dummy_touch()) {
160 my $hits = $self->get('hits') + 1;
162 $self->set('hits', $hits);
164 # call $self->SUPER::save() instead of $self->save()
165 # to avoid saving content (unnecessary) and deleting
166 # the topic INDEX (even probably dangerous)
167 $self->SUPER::save
();
169 $self->source->_update_top_ten($hits, $self->get('topic_id'),
180 my $filename = $self->_filename();
181 $filename =~ s/\.M$/.T/;
184 if (open F
, '>' . $filename) {
186 print F
join(', ', map { s/^\s+//; s/\s+$//; lc($_) } @_), "\n";
191 if (open F
, $filename) {
197 @ret = split(/\s*,\s*/, $l);
208 my $file = $self->_filename();
210 $self->SUPER::delete($driver);
212 # also delete content and tags
220 $self->_destroy_index();
230 if (!$self->SUPER::load
( $driver )) {
234 my $filename = $self->_filename();
235 $filename =~ s/\.M$//;
237 rename($filename . '.TAGS', $filename . '.T');
243 package Gruta
::Data
::FS
::Topic
;
245 use base
'Gruta::Data::Topic';
246 use base
'Gruta::Data::FS::BASE';
256 $self->SUPER::save
( $driver );
258 my $filename = $self->_filename();
259 $filename =~ s/\.M$//;
266 package Gruta
::Data
::FS
::User
;
268 use base
'Gruta::Data::User';
269 use base
'Gruta::Data::FS::BASE';
279 package Gruta
::Data
::FS
::Session
;
281 use base
'Gruta::Data::Session';
282 use base
'Gruta::Data::FS::BASE';
292 package Gruta
::Data
::FS
::Template
;
294 use base
'Gruta::Data::Template';
295 use base
'Gruta::Data::FS::BASE';
298 return '/templates/';
309 $self->source($driver);
311 if (not open(F
, $self->_filename())) {
315 $self->set('content', join('', <F
>));
326 $self->source($driver) if $driver;
328 if (not open(F
, '>' . $self->_filename())) {
332 print F
$self->get('content');
339 package Gruta
::Data
::FS
::Comment
;
341 use base
'Gruta::Data::Comment';
342 use base
'Gruta::Data::FS::BASE';
351 return '/comments/' . $_[0]->get('topic_id') . '/'
352 . $_[0]->get('story_id') . '/';
356 grep !/content/, $_[0]->SUPER::fields
();
360 return ($_[0]->SUPER::vfields
(), 'content');
367 my @p = split('/', $self->_filename());
372 my $pending = join('/', @p) . '/.pending/' .
374 $self->get('topic_id'),
375 $self->get('story_id'),
387 $self->source($driver) if $driver;
389 # create the directory tree
390 my @p = split('/', $self->_filename());
394 my $d = join('/', @p);
396 mkdir $d or croak
"Error posting comment: $d, $!";
403 mkdir $d or croak
"Error posting comment: $d, $!";
406 $self->SUPER::save
($driver);
409 my $filename = $self->_filename();
410 $filename =~ s/\.M$//;
412 open F
, '>' . $filename or
413 croak
"Cannot write " . $filename . ': ' . $!;
415 print F
$self->get('content') || '';
418 # if not approved, write pending
419 if (!$self->get('approved')) {
420 open F
, '>' . $self->pending_file();
432 if (!$self->SUPER::load
($driver)) {
436 my $filename = $self->_filename();
437 $filename =~ s/\.M$//;
439 if (open F
, $filename) {
440 $self->set('content', join('', <F
>));
453 my $file = $self->_filename();
458 # delete (possible) pending
459 unlink $self->pending_file();
466 $self->set('approved', 1);
469 # delete (possible) pending
470 unlink $self->pending_file();
476 package Gruta
::Source
::FS
;
483 $self->{path
} or croak
"Invalid path";
493 my $o = ${class}->new( id
=> $id );
498 return _one
( @_, 'Gruta::Data::FS::Topic' );
506 my $path = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
();
508 if (opendir D
, $path) {
509 while (my $id = readdir D
) {
510 next unless -d
$path . $id;
511 next if $id =~ /^\./;
523 return _one
( @_, 'Gruta::Data::FS::User' );
531 my $path = $self->{path
} . Gruta
::Data
::FS
::User
::base
();
533 if (opendir D
, $path) {
534 while (my $id = readdir D
) {
535 next if -d
$path . $id;
546 return _one
(@_, 'Gruta::Data::FS::Template');
554 my $path = $self->{path
} . Gruta
::Data
::FS
::Template
::base
();
556 if (opendir D
, $path) {
557 while (my $id = readdir D
) {
558 next if -d
$path . $id;
571 my $topic_id = shift;
572 my $story_id = shift;
575 my $comment = Gruta
::Data
::FS
::Comment
->new(
576 topic_id
=> $topic_id,
577 story_id
=> $story_id,
581 if (not $comment->load($self)) {
589 sub pending_comments
{
594 my $path = $self->{path
} . Gruta
::Data
::FS
::Comment
::base
()
597 if (opendir D
, $path) {
598 while (my $id = readdir D
) {
603 push @ret, [ split(':', $id) ];
609 return sort { $b->[2] cmp $a->[2] } @ret;
621 my $path = $self->{path
} . Gruta
::Data
::FS
::Comment
::base
();
623 if (opendir BASE
, $path) {
624 while (my $topic_id = readdir BASE
) {
625 next if $topic_id =~ /^\./;
627 my $f = $path . $topic_id;
629 if (opendir TOPIC
, $f) {
630 while (my $story_id = readdir TOPIC
) {
631 next if $story_id =~ /^\./;
633 my $sf = $f . '/' . $story_id;
635 if (opendir STORY
, $sf) {
636 while (my $id = readdir STORY
) {
637 if ($id =~ /^(.+)\.M/) {
639 my $c = $self->comment($topic_id,
642 if ($c && $c->get('approved')) {
643 push @ret, [ $topic_id, $story_id, $1 ];
659 @ret = sort { $b->[2] cmp $a->[2] } @ret;
660 @ret = @ret[0 .. ($max - 1)];
662 return grep { defined $_ } @ret;
674 my $expire_days_t = $self->template('cfg_comment_expire_days');
676 if ($expire_days_t) {
677 $expire_days = $expire_days_t->get('content');
680 my $topic_id = $story->get('topic_id');
681 my $story_id = $story->get('id');
683 my $base_path = $self->{path
} . Gruta
::Data
::FS
::Comment
::base
();
685 my $pend_path = $base_path . '/.pending/';
686 my $path = join('/', ($base_path, $topic_id, $story_id)) . '/';
688 if (opendir D
, $path) {
689 while (my $id = readdir D
) {
693 next if $f =~ /\.M$/;
695 my $pf = $pend_path . join(':', ($topic_id, $story_id, $id));
698 if (-f
$pf && -M
$f >= $expire_days) {
705 # not all wanted and this comment not approved? skip
706 if (!$all && -f
$pf) {
710 push @ret, [ $topic_id, $story_id, $id ];
716 return sort { $a->[2] cmp $b->[2] } @ret;
722 my $topic_id = shift;
727 if ($story = $self->cache_story($topic_id, $id)) {
731 $story = Gruta
::Data
::FS
::Story
->new( topic_id
=> $topic_id, id
=> $id );
733 if (not $story->load( $self )) {
735 $story = Gruta
::Data
::FS
::Story
->new( topic_id
=> $topic_id . '-arch',
738 if (not $story->load( $self )) {
743 # now load the content
744 my $file = $story->_filename();
747 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
751 my $field = shift(@d);
753 if (open F
, $file . $ext) {
754 $story->set($field, join('', <F
>));
759 $self->cache_story($topic_id, $id, $story);
766 my $topic_id = shift;
770 if (!$self->topic($topic_id)) {
774 my $path = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
776 if (opendir D
, $path) {
777 while (my $id = readdir D
) {
778 if ($id =~ s/\.M$// || $id =~ s/\.META$//) {
792 my $topic_id = shift;
794 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
802 if (not open I
, $index) {
805 foreach my $id ($self->stories($topic_id)) {
806 my $story = $self->story($topic_id, $id);
808 push(@i, ($story->get('date') || ('0' x
14)). ':' . $id);
811 open I
, '>' . $index or croak
"Can't create INDEX for $topic_id: $!";
814 foreach my $l (reverse(sort(@i))) {
825 sub _update_top_ten
{
828 my $topic_id = shift;
831 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
836 if (open F
, $index) {
838 while (my $l = <F
>) {
841 my ($h, $t, $i) = split(':', $l);
843 if ($u == 0 && $h < $hits) {
845 push(@l, "$hits:$topic_id:$id");
848 if ($i ne $id or $t ne $topic_id) {
856 if ($u == 0 && scalar(@l) < $self->{hard_top_ten_limit
}) {
858 push(@l, "$hits:$topic_id:$id");
862 if (open F
, '>' . $index) {
869 if (++$n == $self->{hard_top_ten_limit
}) {
882 sub _stories_by_date
{
884 my $topic_id = shift;
889 my $i = $self->_topic_index($topic_id) or return @r;
890 open I
, $i or return @r;
898 my ($date, $id) = split(/:/);
900 # skip future stories
901 next if not $args{future
} and $date gt Gruta
::Data
::today
();
903 # skip if date is above the threshold
904 next if $args{'to'} and $date gt $args{'to'};
906 # exit if date is below the threshold
907 last if $args{'from'} and $date lt $args{'from'};
909 # skip offset stories
910 next if $args{'offset'} and ++$o <= $args{'offset'};
912 push(@r, [ $topic_id, $id, $date ]);
914 # exit if we have all we need
915 last if $args{'num'} and $args{'num'} == scalar(@r);
924 sub stories_by_date
{
932 @topics = $self->topics();
935 @topics = @
{ $topics };
938 if (!$args{offset
} || $args{offset
} < 0) {
942 # only one topic? execute it and return
943 if (scalar(@topics) == 1) {
944 return $self->_stories_by_date($topics[0], %args);
947 # more than one topic; 'num' and 'offset' need to be
948 # calculated from the full set
951 foreach my $topic_id (@topics) {
953 my @r = $self->_stories_by_date($topic_id,
954 %args, num
=> 0, offset
=> 0);
960 @R = sort { $b->[2] cmp $a->[2] } @R;
964 @R = @R[$args{offset
} .. ($args{offset
} + $args{num
} - 1)];
967 @R = @R[$args{offset
} .. (scalar(@R) - 1)];
970 return grep { defined $_ } @R;
975 my $topic_id = shift;
979 my @q = split(/\s+/,$query);
983 foreach my $id ($self->stories($topic_id)) {
985 my $story = $self->story($topic_id, $id);
987 if (!$future and $story->get('date') gt Gruta
::Data
::today
()) {
991 my $content = $story->get('content');
994 # try complete query first
995 if($content =~ /\b$query\b/i) {
1000 foreach my $q (@q) {
1001 if(length($q) > 1 and $content =~ /\b$q\b/i) {
1007 if ($found == scalar(@q)) {
1008 $r{$id} = $story->get('title');
1012 return sort { $r{$a} cmp $r{$b} } keys %r;
1015 sub stories_by_text
{
1025 @topics = $self->topics();
1028 @topics = @
{ $topics };
1031 foreach my $t (@topics) {
1032 foreach my $id ($self->search_stories($t, $query, $future)) {
1033 push(@ret, [ $t, $id ]);
1040 sub stories_top_ten
{
1046 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
1048 if (open F
, $index) {
1051 while (defined(my $l = <F
>) and $num--) {
1053 push(@r, [ split(':', $l) ]);
1069 foreach my $topic_id (@topics) {
1071 my $topic = $self->topic($topic_id)
1072 or croak
("Bad topic $topic_id");
1074 my $files = $topic->_filename();
1075 $files =~ s/\.M$/\/*.T
/;
1077 my @ls = glob($files);
1079 foreach my $f (@ls) {
1087 my ($id) = ($f =~ m{/([^/]+)\.T});
1090 [ $topic_id, $id, [ split(/\s*,\s*/, $tags) ] ]
1101 sub stories_by_tag
{
1110 @topics = $self->topics();
1113 @topics = @
{ $topics };
1119 my @tags = map { lc($_) } split(/\s*,\s*/, $tag);
1121 foreach my $tr ($self->_collect_tags(@topics)) {
1123 my @ts = @
{$tr->[2]};
1125 # skip stories with less tags than the wanted ones
1126 if (scalar(@ts) < scalar(@tags)) {
1133 foreach my $t (@ts) {
1134 if (grep(/^$t$/, @tags)) {
1139 if ($c >= scalar(@tags)) {
1141 my $story = $self->story($tr->[0], $tr->[1]);
1143 # if no future stories are wanted, discard them
1145 if ($story->get('date') gt Gruta
::Data
::today
()) {
1150 $r{$story->get('title')} =
1151 [ $tr->[0], $tr->[1], $story->get('date') ];
1156 # return all those stories without tags
1157 foreach my $topic_id (@topics) {
1158 foreach my $story_id ($self->stories($topic_id)) {
1159 my $story = $self->story($topic_id, $story_id);
1161 # if no future stories are wanted, discard them
1163 if ($story->get('date') gt Gruta
::Data
::today
()) {
1168 if (!$story->tags()) {
1169 $r{$story->get('title')} =
1170 [ $topic_id, $story_id, $story->get('date') ];
1176 return map { $r{$_} } sort keys %r;
1186 foreach my $tr ($self->_collect_tags($self->topics())) {
1188 foreach my $t (@
{$tr->[2]}) {
1193 foreach my $k (keys(%h)) {
1194 push(@ret, [ $k, $h{$k} ]);
1197 return sort { $a->[0] cmp $b->[0] } @ret;
1202 return _one
( @_, 'Gruta::Data::FS::Session' );
1205 sub purge_old_sessions
{
1208 my $path = $self->{path
} . Gruta
::Data
::FS
::Session
::base
();
1210 if (opendir D
, $path) {
1211 while(my $s = readdir D
) {
1233 bless($obj, $class);
1234 $obj->save( $self );
1240 $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic');
1244 $_[0]->_insert($_[1], 'Gruta::Data::FS::User');
1247 sub insert_template
{
1248 $_[0]->_insert($_[1], 'Gruta::Data::FS::Template');
1251 sub insert_comment
{
1252 $_[0]->_insert($_[1], 'Gruta::Data::FS::Comment');
1259 if (not $story->get('id')) {
1260 # alloc an id for the story
1264 $id = $story->new_id();
1266 } while $self->story($story->get('topic_id'), $id);
1268 $story->set('id', $id);
1271 $self->_insert($story, 'Gruta::Data::FS::Story');
1275 sub insert_session
{
1276 $_[0]->_insert($_[1], 'Gruta::Data::FS::Session');
1283 my @l = map { $self->{path
} . $_ } (
1284 Gruta
::Data
::FS
::Topic
::base
(),
1285 Gruta
::Data
::FS
::User
::base
(),
1286 Gruta
::Data
::FS
::Session
::base
(),
1287 Gruta
::Data
::FS
::Template
::base
(),
1288 Gruta
::Data
::FS
::Comment
::base
(),
1289 Gruta
::Data
::FS
::Comment
::base
() . '/.pending/'
1292 foreach my $d (@l) {
1294 mkdir $d, 0755 or die "Cannot mkdir $d";
1305 my $s = bless( { @_ }, $class);
1307 $s->{hard_top_ten_limit
} ||= 100;