1 package Gruta
::Source
::FS
;
8 package Gruta
::Data
::FS
::BASE
;
12 sub ext
{ return '.META'; }
18 $self->source->_assert();
20 return $self->source->{path
} . $self->base() .
21 $self->get('id') . $self->ext();
29 $self->source( $driver );
31 if (not open F
, $self->_filename()) {
38 if(/^([^:]*): (.*)$/) {
39 my ($key, $value) = ($1, $2);
43 if (grep (/^$key$/, $self->fields())) {
44 $self->set($key, $value);
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()) {
69 print F
$f . ': ' . ($self->get($k) || '') . "\n";
82 $self->source( $driver ) if $driver;
84 unlink $self->_filename();
89 package Gruta
::Data
::FS
::Story
;
91 use base
'Gruta::Data::Story';
92 use base
'Gruta::Data::FS::BASE';
96 sub base
{ return Gruta
::Data
::FS
::Topic
::base
() . $_[0]->get('topic_id') . '/'; }
98 sub fields
{ grep !/(content|topic_id)/, $_[0]->SUPER::fields
(); }
99 sub vfields
{ return ($_[0]->SUPER::vfields
(), 'content', 'topic_id'); }
104 my $filename = $self->_filename();
106 # destroy the topic index, to be rewritten
107 # in the future by _topic_index()
108 $filename =~ s!/[^/]+$!/.INDEX!;
116 $self->SUPER::save
( $driver );
118 my $filename = $self->_filename();
119 $filename =~ s/\.META$//;
121 open F
, '>' . $filename or croak
"Can't write " . $filename . ': ' . $!;
123 print F
$self->get('content') || '';
126 $self->_destroy_index();
134 my $hits = $self->get('hits') + 1;
136 $self->set('hits', $hits);
138 # call $self->SUPER::save() instead of $self->save()
139 # to avoid saving content (unnecessary) and deleting
140 # the topic INDEX (even probably dangerous)
141 $self->SUPER::save
();
143 $self->source->_update_top_ten($hits, $self->get('topic_id'),
153 my $filename = $self->_filename();
154 $filename =~ s/\.META$/.TAGS/;
157 if (open F
, '>' . $filename) {
158 print F
join(', ', map { s/^\s+//; s/\s+$//; lc($_) } @_), "\n";
163 if (open F
, $filename) {
168 @ret = split(/\s*,\s*/, $l);
179 my $file = $self->_filename();
181 $self->SUPER::delete($driver);
183 # also delete content and TAGS
184 $file =~ s/\.META$//;
187 unlink $file . '.TAGS';
189 $self->_destroy_index();
195 package Gruta
::Data
::FS
::Topic
;
197 use base
'Gruta::Data::Topic';
198 use base
'Gruta::Data::FS::BASE';
200 sub base
{ return '/topics/'; }
206 $self->SUPER::save
( $driver );
208 my $filename = $self->_filename();
209 $filename =~ s/\.META$//;
216 package Gruta
::Data
::FS
::User
;
218 use base
'Gruta::Data::User';
219 use base
'Gruta::Data::FS::BASE';
221 sub ext
{ return ''; }
222 sub base
{ return '/users/'; }
224 package Gruta
::Data
::FS
::Session
;
226 use base
'Gruta::Data::Session';
227 use base
'Gruta::Data::FS::BASE';
229 sub ext
{ return ''; }
230 sub base
{ return '/sids/'; }
232 package Gruta
::Source
::FS
;
239 $self->{path
} or croak
"Invalid path";
249 my $o = ${class}->new( id
=> $id );
253 sub topic
{ return _one
( @_, 'Gruta::Data::FS::Topic' ); }
260 my $path = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
();
262 if (opendir D
, $path) {
263 while (my $id = readdir D
) {
264 next unless -d
$path . $id;
265 next if $id =~ /^\./;
276 sub user
{ return _one
( @_, 'Gruta::Data::FS::User' ); }
283 my $path = $self->{path
} . Gruta
::Data
::FS
::User
::base
();
285 if (opendir D
, $path) {
286 while (my $id = readdir D
) {
287 next if -d
$path . $id;
299 my $topic_id = shift;
302 my $story = Gruta
::Data
::FS
::Story
->new( topic_id
=> $topic_id, id
=> $id );
303 if (not $story->load( $self )) {
305 $story = Gruta
::Data
::FS
::Story
->new( topic_id
=> $topic_id . '-arch',
308 if (not $story->load( $self )) {
313 # now load the content
314 my $file = $story->_filename();
315 $file =~ s/\.META$//;
317 open F
, $file or croak
"Can't open $file content: $!";
319 $story->set('content', join('', <F
>));
327 my $topic_id = shift;
331 my $path = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
333 if (opendir D
, $path) {
334 while (my $id = readdir D
) {
335 if ($id =~ s/\.META$//) {
349 my $topic_id = shift;
351 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . $topic_id;
359 if (not open I
, $index) {
362 foreach my $id ($self->stories($topic_id)) {
363 my $story = $self->story($topic_id, $id);
365 push(@i, $story->get('date') . ':' . $id);
368 open I
, '>' . $index or croak
"Can't create INDEX for $topic_id: $!";
371 foreach my $l (reverse(sort(@i))) {
382 sub _update_top_ten
{
385 my $topic_id = shift;
388 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
393 if (open F
, $index) {
395 while (my $l = <F
>) {
398 my ($h, $t, $i) = split(':', $l);
400 if ($u == 0 && $h < $hits) {
402 push(@l, "$hits:$topic_id:$id");
405 if ($t ne $topic_id or $i ne $id) {
413 if ($u == 0 && scalar(@l) < 100) {
415 push(@l, "$hits:$topic_id:$id");
419 if (open F
, '>' . $index) {
439 sub stories_by_date
{
447 @topics = $self->topics();
450 @topics = @
{ $topics };
454 $args{offset
} = 0 if $args{offset
} < 0;
458 foreach my $topic_id (@topics) {
459 my $i = $self->_topic_index($topic_id) or next;
469 my ($date, $id) = (/^(\d*):(.*)$/);
471 # skip future stories
472 next if not $args{future
} and $date gt Gruta
::Data
::today
();
474 # skip if date is above the threshold
475 next if $args{'to'} and $date gt $args{'to'};
477 # exit if date is below the threshold
478 last if $args{'from'} and $date lt $args{'from'};
480 # skip offset stories
481 next if $args{'offset'} and ++$o <= $args{'offset'};
483 push(@r, [ $id, $topic_id, $date ]);
485 # exit if we have all we need
486 last if $args{'num'} and $args{'num'} == scalar(@r);
499 my $topic_id = shift;
503 my @q = split(/\s+/,$query);
507 foreach my $id ($self->stories_by_date( [ $topic_id ], future
=> $future )) {
509 my $story = $self->story($topic_id, $id->[0]);
510 my $content = $story->get('content');
513 # try complete query first
514 if($content =~ /\b$query\b/i) {
520 if(length($q) > 1 and $content =~ /\b$q\b/i) {
526 push(@r, $id->[0]) if $found == scalar(@q);
532 sub stories_top_ten
{
538 my $index = $self->{path
} . Gruta
::Data
::FS
::Topic
::base
() . '/.top_ten';
540 if (open F
, $index) {
543 while (defined(my $l = <F
>) and $num--) {
545 push(@r, [ split(':', $l) ]);
560 foreach my $topic_id ($self->topics()) {
562 my $topic = $self->topic($topic_id);
564 my $files = $topic->_filename();
565 $files =~ s/\.META$/\/*.TAGS
/;
567 my @ls = glob($files);
569 foreach my $f (@ls) {
575 my ($id) = ($f =~ m{/([^/]+)\.TAGS});
578 [ $topic_id, $id, [ split(/\s*,\s*/, $tags) ] ]
588 sub search_stories_by_tag
{
593 my @tags = map { lc($_) } split(/\s*,\s*/, $tag);
597 foreach my $tr ($self->_collect_tags()) {
599 foreach my $t (@
{$tr->[2]}) {
600 if (grep(/^$t$/, @tags)) {
602 # if no future stories are to be shown,
605 my $story = $self->story(
609 if ($story->get('date') >
610 Gruta
::Data
::today
()) {
615 push(@ret, [ $tr->[0], $tr->[1] ]);
631 foreach my $tr ($self->_collect_tags()) {
633 foreach my $t (@
{$tr->[2]}) {
638 foreach my $k (keys(%h)) {
639 push(@ret, [ $k, $h{$k} ]);
646 sub session
{ return _one
( @_, 'Gruta::Data::FS::Session' ); }
648 sub purge_old_sessions
{
651 my $path = $self->{path
} . Gruta
::Data
::FS
::Session
::base
();
653 if (opendir D
, $path) {
654 while(my $s = readdir D
) {
682 sub insert_topic
{ $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic'); }
683 sub insert_user
{ $_[0]->_insert($_[1], 'Gruta::Data::FS::User'); }
689 if (not $story->get('id')) {
690 # alloc an id for the story
694 $id = $story->new_id();
696 } while $self->story($story->get('topic_id'), $id);
698 $story->set('id', $id);
701 $self->_insert($story, 'Gruta::Data::FS::Story');
705 sub insert_session
{ $_[0]->_insert($_[1], 'Gruta::Data::FS::Session'); }
711 my @l = map { $self->{path
} . $_ } (
712 Gruta
::Data
::FS
::Topic
::base
(),
713 Gruta
::Data
::FS
::User
::base
(),
714 Gruta
::Data
::FS
::Session
::base
()
719 mkdir $d, 0755 or die "Cannot mkdir $d";
730 my $s = bless( { @_ }, $class);