Supressed a new warning.
[gruta.git] / Gruta / Source / FS.pm
blobb614489977dbb1f916184826970dbe014281c2e1
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 flock F, 1;
45 while (<F>) {
46 chop;
48 if(/^([^:]*): (.*)$/) {
49 my ($key, $value) = ($1, $2);
51 $key =~ s/-/_/g;
53 if (grep (/^$key$/, $self->fields())) {
54 $self->set($key, $value);
59 close F;
61 return $self;
64 sub save {
65 my $self = shift;
66 my $driver = shift;
68 $self->source( $driver ) if $driver;
70 my $filename = $self->_filename();
72 open F, '>' . $filename or croak "Can't write " . $filename . ': ' . $!;
73 flock F, 2;
75 foreach my $k ($self->fields()) {
76 my $f = $k;
78 $f =~ s/_/-/g;
80 print F $f . ': ' . ($self->get($k) || '') . "\n";
83 close F;
85 return $self;
89 sub delete {
90 my $self = shift;
91 my $driver = shift;
93 $self->source( $driver ) if $driver;
95 unlink $self->_filename();
97 return $self;
100 package Gruta::Data::FS::Story;
102 use base 'Gruta::Data::Story';
103 use base 'Gruta::Data::FS::BASE';
105 use Carp;
107 sub base {
108 return Gruta::Data::FS::Topic::base() . $_[0]->get('topic_id') . '/';
111 sub fields {
112 grep !/(content|topic_id|abstract|body)/, $_[0]->SUPER::fields();
115 sub vfields {
116 return ($_[0]->SUPER::vfields(), 'content', 'topic_id', 'abstract', 'body');
119 sub _destroy_index {
120 my $self = shift;
122 my $filename = $self->_filename();
124 # destroy the topic index, to be rewritten
125 # in the future by _topic_index()
126 $filename =~ s!/[^/]+$!/.INDEX!;
127 unlink $filename;
130 sub save {
131 my $self = shift;
132 my $driver = shift;
134 $self->SUPER::save( $driver );
136 my $filename = $self->_filename();
137 $filename =~ s/\.M$//;
139 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
141 while (@d) {
142 my $ext = shift(@d);
143 my $field = shift(@d);
145 open F, '>' . $filename . $ext or
146 croak "Cannot write " . $filename . $ext . ': ' . $!;
147 print F $self->get($field) || '';
148 close F;
151 $self->_destroy_index();
153 return $self;
156 sub touch {
157 my $self = shift;
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'),
170 $self->get('id'));
173 return $self;
176 sub tags {
177 my $self = shift;
178 my @ret = ();
180 my $filename = $self->_filename();
181 $filename =~ s/\.M$/.T/;
183 if (scalar(@_)) {
184 if (open F, '>' . $filename) {
185 flock F, 2;
186 print F join(', ', map { s/^\s+//; s/\s+$//; lc($_) } @_), "\n";
187 close F;
190 else {
191 if (open F, $filename) {
192 flock F, 1;
193 my $l = <F>;
194 close F;
196 chomp($l);
197 @ret = split(/\s*,\s*/, $l);
201 return @ret;
204 sub delete {
205 my $self = shift;
206 my $driver = shift;
208 my $file = $self->_filename();
210 $self->SUPER::delete($driver);
212 # also delete content and tags
213 $file =~ s/\.M$//;
215 unlink $file;
216 unlink $file . '.A';
217 unlink $file . '.B';
218 unlink $file . '.T';
220 $self->_destroy_index();
222 return $self;
226 sub load {
227 my $self = shift;
228 my $driver = shift;
230 if (!$self->SUPER::load( $driver )) {
231 return undef;
234 my $filename = $self->_filename();
235 $filename =~ s/\.M$//;
237 rename($filename . '.TAGS', $filename . '.T');
239 return $self;
243 package Gruta::Data::FS::Topic;
245 use base 'Gruta::Data::Topic';
246 use base 'Gruta::Data::FS::BASE';
248 sub base {
249 return '/topics/';
252 sub save {
253 my $self = shift;
254 my $driver = shift;
256 $self->SUPER::save( $driver );
258 my $filename = $self->_filename();
259 $filename =~ s/\.M$//;
261 mkdir $filename;
263 return $self;
266 package Gruta::Data::FS::User;
268 use base 'Gruta::Data::User';
269 use base 'Gruta::Data::FS::BASE';
271 sub ext {
272 return '';
275 sub base {
276 return '/users/';
279 package Gruta::Data::FS::Session;
281 use base 'Gruta::Data::Session';
282 use base 'Gruta::Data::FS::BASE';
284 sub ext {
285 return '';
288 sub base {
289 return '/sids/';
292 package Gruta::Data::FS::Template;
294 use base 'Gruta::Data::Template';
295 use base 'Gruta::Data::FS::BASE';
297 sub base {
298 return '/templates/';
301 sub ext {
302 return '';
305 sub load {
306 my $self = shift;
307 my $driver = shift;
309 $self->source($driver);
311 if (not open(F, $self->_filename())) {
312 return undef;
315 $self->set('content', join('', <F>));
316 close F;
318 return $self;
322 sub save {
323 my $self = shift;
324 my $driver = shift;
326 $self->source($driver) if $driver;
328 if (not open(F, '>' . $self->_filename())) {
329 return undef;
332 print F $self->get('content');
333 close F;
335 return $self;
339 package Gruta::Data::FS::Comment;
341 use base 'Gruta::Data::Comment';
342 use base 'Gruta::Data::FS::BASE';
344 use Carp;
346 sub base {
347 if (!ref($_[0])) {
348 return '/comments/';
351 return '/comments/' . $_[0]->get('topic_id') . '/'
352 . $_[0]->get('story_id') . '/';
355 sub fields {
356 grep !/content/, $_[0]->SUPER::fields();
359 sub vfields {
360 return ($_[0]->SUPER::vfields(), 'content');
364 sub pending_file {
365 my $self = shift;
367 my @p = split('/', $self->_filename());
368 pop(@p);
369 pop(@p);
370 pop(@p);
372 my $pending = join('/', @p) . '/.pending/' .
373 join(':',
374 $self->get('topic_id'),
375 $self->get('story_id'),
376 $self->get('id')
379 return $pending;
383 sub save {
384 my $self = shift;
385 my $driver = shift;
387 $self->source($driver) if $driver;
389 # create the directory tree
390 my @p = split('/', $self->_filename());
391 pop(@p);
392 my $s = pop(@p);
394 my $d = join('/', @p);
395 if (! -d $d) {
396 mkdir $d or croak "Error posting comment: $d, $!";
399 push(@p, $s);
401 $d = join('/', @p);
402 if (! -d $d) {
403 mkdir $d or croak "Error posting comment: $d, $!";
406 $self->SUPER::save($driver);
408 # write content
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') || '';
416 close F;
418 # if not approved, write pending
419 if (!$self->get('approved')) {
420 open F, '>' . $self->pending_file();
421 close F;
424 return $self;
428 sub load {
429 my $self = shift;
430 my $driver = shift;
432 if (!$self->SUPER::load($driver)) {
433 return undef;
436 my $filename = $self->_filename();
437 $filename =~ s/\.M$//;
439 if (open F, $filename) {
440 $self->set('content', join('', <F>));
441 close F;
444 return $self;
448 sub delete {
449 my $self = shift;
450 my $driver = shift;
452 # delete content
453 my $file = $self->_filename();
454 unlink $file;
455 $file =~ s/\.M$//;
456 unlink $file;
458 # delete (possible) pending
459 unlink $self->pending_file();
463 sub approve {
464 my $self = shift;
466 $self->set('approved', 1);
467 $self->save();
469 # delete (possible) pending
470 unlink $self->pending_file();
472 return $self;
476 package Gruta::Source::FS;
478 use Carp;
480 sub _assert {
481 my $self = shift;
483 $self->{path} or croak "Invalid path";
485 return $self;
488 sub _one {
489 my $self = shift;
490 my $id = shift;
491 my $class = shift;
493 my $o = ${class}->new( id => $id );
494 $o->load( $self );
497 sub topic {
498 return _one( @_, 'Gruta::Data::FS::Topic' );
501 sub topics {
502 my $self = shift;
504 my @ret = ();
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 =~ /^\./;
513 push @ret, $id;
516 closedir D;
519 return @ret;
522 sub user {
523 return _one( @_, 'Gruta::Data::FS::User' );
526 sub users {
527 my $self = shift;
529 my @ret = ();
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;
536 push @ret, $id;
539 closedir D;
542 return @ret;
545 sub template {
546 return _one(@_, 'Gruta::Data::FS::Template');
549 sub templates {
550 my $self = shift;
552 my @ret = ();
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;
559 push @ret, $id;
562 closedir D;
565 return @ret;
569 sub comment {
570 my $self = shift;
571 my $topic_id = shift;
572 my $story_id = shift;
573 my $id = shift;
575 my $comment = Gruta::Data::FS::Comment->new(
576 topic_id => $topic_id,
577 story_id => $story_id,
578 id => $id
581 if (not $comment->load($self)) {
582 return undef;
585 return $comment;
589 sub pending_comments {
590 my $self = shift;
592 my @ret = ();
594 my $path = $self->{path} . Gruta::Data::FS::Comment::base()
595 . '/.pending/';
597 if (opendir D, $path) {
598 while (my $id = readdir D) {
599 my $f = $path . $id;
601 next if -d $f;
603 push @ret, [ split(':', $id) ];
606 closedir D;
609 return sort { $b->[2] cmp $a->[2] } @ret;
613 sub comments {
614 my $self = shift;
615 my $max = shift;
617 $max ||= 20;
619 my @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/) {
638 $id = $1;
639 my $c = $self->comment($topic_id,
640 $story_id, $id);
642 if ($c && $c->get('approved')) {
643 push @ret, [ $topic_id, $story_id, $1 ];
648 closedir STORY;
652 closedir TOPIC;
656 closedir BASE;
659 @ret = sort { $b->[2] cmp $a->[2] } @ret;
660 @ret = @ret[0 .. ($max - 1)];
662 return grep { defined $_ } @ret;
666 sub story_comments {
667 my $self = shift;
668 my $story = shift;
669 my $all = shift;
671 my @ret = ();
673 my $expire_days = 7;
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) {
690 my $f = $path . $id;
692 next if -d $f;
693 next if $f =~ /\.M$/;
695 my $pf = $pend_path . join(':', ($topic_id, $story_id, $id));
697 # too old? delete
698 if (-f $pf && -M $f >= $expire_days) {
699 unlink $f;
700 unlink $f . '.M';
701 unlink $pf;
702 next;
705 # not all wanted and this comment not approved? skip
706 if (!$all && -f $pf) {
707 next;
710 push @ret, [ $topic_id, $story_id, $id ];
713 closedir D;
716 return sort { $a->[2] cmp $b->[2] } @ret;
720 sub story {
721 my $self = shift;
722 my $topic_id = shift;
723 my $id = shift;
725 my $story;
727 if ($story = $self->cache_story($topic_id, $id)) {
728 return $story;
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',
736 id => $id );
738 if (not $story->load( $self )) {
739 return undef;
743 # now load the content
744 my $file = $story->_filename();
745 $file =~ s/\.M$//;
747 my @d = ('', 'content', '.A', 'abstract', '.B', 'body');
749 while (@d) {
750 my $ext = shift(@d);
751 my $field = shift(@d);
753 if (open F, $file . $ext) {
754 $story->set($field, join('', <F>));
755 close F;
759 $self->cache_story($topic_id, $id, $story);
761 return $story;
764 sub stories {
765 my $self = shift;
766 my $topic_id = shift;
768 my @ret = ();
770 if (!$self->topic($topic_id)) {
771 return @ret;
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$//) {
779 push(@ret, $id);
783 closedir D;
786 return @ret;
790 sub _topic_index {
791 my $self = shift;
792 my $topic_id = shift;
794 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . $topic_id;
796 if (! -d $index) {
797 return undef;
800 $index .= '/.INDEX';
802 if (not open I, $index) {
804 my @i = ();
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: $!";
812 flock I, 2;
814 foreach my $l (reverse(sort(@i))) {
815 print I $l, "\n";
819 close I;
821 return $index;
825 sub _update_top_ten {
826 my $self = shift;
827 my $hits = shift;
828 my $topic_id = shift;
829 my $id = shift;
831 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . '/.top_ten';
833 my $u = 0;
834 my @l = ();
836 if (open F, $index) {
837 flock F, 1;
838 while (my $l = <F>) {
839 chomp($l);
841 my ($h, $t, $i) = split(':', $l);
843 if ($u == 0 && $h < $hits) {
844 $u = 1;
845 push(@l, "$hits:$topic_id:$id");
848 if ($i ne $id or $t ne $topic_id) {
849 push(@l, $l);
853 close F;
856 if ($u == 0 && scalar(@l) < $self->{hard_top_ten_limit}) {
857 $u = 1;
858 push(@l, "$hits:$topic_id:$id");
861 if ($u) {
862 if (open F, '>' . $index) {
863 flock F, 2;
864 my $n = 0;
866 foreach my $l (@l) {
867 print F $l, "\n";
869 if (++$n == $self->{hard_top_ten_limit}) {
870 last;
874 close F;
878 return undef;
882 sub _stories_by_date {
883 my $self = shift;
884 my $topic_id = shift;
885 my %args = @_;
887 my @r = ();
889 my $i = $self->_topic_index($topic_id) or return @r;
890 open I, $i or return @r;
891 flock I, 1;
893 my $o = 0;
895 while (<I>) {
896 chomp;
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);
918 close I;
920 return @r;
924 sub stories_by_date {
925 my $self = shift;
926 my $topics = shift;
927 my %args = @_;
929 my @topics;
931 if (!$topics) {
932 @topics = $self->topics();
934 else {
935 @topics = @{ $topics };
938 if (!$args{offset} || $args{offset} < 0) {
939 $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
949 my @R = ();
951 foreach my $topic_id (@topics) {
953 my @r = $self->_stories_by_date($topic_id,
954 %args, num => 0, offset => 0);
956 push(@R, @r);
959 # sort by date
960 @R = sort { $b->[2] cmp $a->[2] } @R;
962 # split now
963 if ($args{num}) {
964 @R = @R[$args{offset} .. ($args{offset} + $args{num} - 1)];
966 else {
967 @R = @R[$args{offset} .. (scalar(@R) - 1)];
970 return grep { defined $_ } @R;
973 sub search_stories {
974 my $self = shift;
975 my $topic_id = shift;
976 my $query = shift;
977 my $future = shift;
979 my @q = split(/\s+/,$query);
981 my %r = ();
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()) {
988 next;
991 my $content = $story->get('content');
992 my $found = 0;
994 # try complete query first
995 if($content =~ /\b$query\b/i) {
996 $found = scalar(@q);
998 else {
999 # try separate words
1000 foreach my $q (@q) {
1001 if(length($q) > 1 and $content =~ /\b$q\b/i) {
1002 $found++;
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 {
1016 my $self = shift;
1017 my $topics = shift;
1018 my $query = shift;
1019 my $future = shift;
1021 my @ret;
1022 my @topics;
1024 if (!$topics) {
1025 @topics = $self->topics();
1027 else {
1028 @topics = @{ $topics };
1031 foreach my $t (@topics) {
1032 foreach my $id ($self->search_stories($t, $query, $future)) {
1033 push(@ret, [ $t, $id ]);
1037 return @ret;
1040 sub stories_top_ten {
1041 my $self = shift;
1042 my $num = shift;
1044 my @r = ();
1046 my $index = $self->{path} . Gruta::Data::FS::Topic::base() . '/.top_ten';
1048 if (open F, $index) {
1049 flock F, 1;
1051 while (defined(my $l = <F>) and $num--) {
1052 chomp($l);
1053 push(@r, [ split(':', $l) ]);
1056 close F;
1059 return @r;
1063 sub _collect_tags {
1064 my $self = shift;
1065 my @topics = @_;
1067 my @ret = ();
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) {
1080 if (open F, $f) {
1081 my $tags = <F>;
1083 if ($tags) {
1084 chomp $tags;
1085 close F;
1087 my ($id) = ($f =~ m{/([^/]+)\.T});
1089 push(@ret,
1090 [ $topic_id, $id, [ split(/\s*,\s*/, $tags) ] ]
1097 return @ret;
1101 sub stories_by_tag {
1102 my $self = shift;
1103 my $topics = shift;
1104 my $tag = shift;
1105 my $future = shift;
1107 my @topics;
1109 if (!$topics) {
1110 @topics = $self->topics();
1112 else {
1113 @topics = @{ $topics };
1116 my %r = ();
1118 if ($tag) {
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)) {
1127 next;
1130 # count matches
1131 my $c = 0;
1133 foreach my $t (@ts) {
1134 if (grep(/^$t$/, @tags)) {
1135 $c++;
1139 if ($c >= scalar(@tags)) {
1141 my $story = $self->story($tr->[0], $tr->[1]);
1143 # if no future stories are wanted, discard them
1144 if (!$future) {
1145 if ($story->get('date') gt Gruta::Data::today()) {
1146 next;
1150 $r{$story->get('title')} =
1151 [ $tr->[0], $tr->[1], $story->get('date') ];
1155 else {
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
1162 if (!$future) {
1163 if ($story->get('date') gt Gruta::Data::today()) {
1164 next;
1168 if (!$story->tags()) {
1169 $r{$story->get('title')} =
1170 [ $topic_id, $story_id, $story->get('date') ];
1176 return map { $r{$_} } sort keys %r;
1180 sub tags {
1181 my $self = shift;
1183 my @ret = ();
1184 my %h = ();
1186 foreach my $tr ($self->_collect_tags($self->topics())) {
1188 foreach my $t (@{$tr->[2]}) {
1189 $h{$t}++;
1193 foreach my $k (keys(%h)) {
1194 push(@ret, [ $k, $h{$k} ]);
1197 return sort { $a->[0] cmp $b->[0] } @ret;
1201 sub session {
1202 return _one( @_, 'Gruta::Data::FS::Session' );
1205 sub purge_old_sessions {
1206 my $self = shift;
1208 my $path = $self->{path} . Gruta::Data::FS::Session::base();
1210 if (opendir D, $path) {
1211 while(my $s = readdir D) {
1212 my $f = $path . $s;
1214 next if -d $f;
1216 if (-M $f > 1) {
1217 unlink $f;
1221 closedir D;
1224 return undef;
1228 sub _insert {
1229 my $self = shift;
1230 my $obj = shift;
1231 my $class = shift;
1233 bless($obj, $class);
1234 $obj->save( $self );
1236 return $obj;
1239 sub insert_topic {
1240 $_[0]->_insert($_[1], 'Gruta::Data::FS::Topic');
1243 sub insert_user {
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');
1255 sub insert_story {
1256 my $self = shift;
1257 my $story = shift;
1259 if (not $story->get('id')) {
1260 # alloc an id for the story
1261 my $id = undef;
1263 do {
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');
1272 return $story;
1275 sub insert_session {
1276 $_[0]->_insert($_[1], 'Gruta::Data::FS::Session');
1280 sub create {
1281 my $self = shift;
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) {
1293 if (! -d $d) {
1294 mkdir $d, 0755 or die "Cannot mkdir $d";
1298 return $self;
1302 sub new {
1303 my $class = shift;
1305 my $s = bless( { @_ }, $class);
1307 $s->{hard_top_ten_limit} ||= 100;
1309 $s->_assert();
1311 $s->create();
1313 return $s;