3 ################################################################################
5 # File: shared/Arc/Archive.pm #
7 # Authors: André Malo <nd@o3media.de> #
9 # Description: Severance of Threads and archiving #
11 ################################################################################
23 create_forum_xml_string
32 use Time
::German
'localtime';
36 ################################################################################
44 sub VERSION
{(q
$Revision$ =~ /([\d.]+)\s*$/)[0] or '0.0'}
46 ################################################################################
50 use base
qw(Exporter);
51 @EXPORT = qw(cut_tail);
53 ### delete_no_archived () ######################################################
55 # remove no archived branches vom thread
57 # Params: $xml - XML::DOM::Document node
58 # $msg - arrayref - messages
59 # $percent - voting limit (percent)
63 sub delete_no_archived
($) {
66 my ($xml, $sum, $tid, $msg, $percent) = map {$par->{$_}}
67 qw( xml sum tid msg percent);
69 # $oldlevel: contains the level of last checked msg
70 # @path : contains the current branch
71 # %archive : contains the mids, that will be archived
72 # %hidden : contains the invisible mids
74 my ($oldlevel, @path, %archive, %hidden) = (0, 0);
76 # check all messages of thread
78 for my $z (0..$#{$msg}) {
80 if ($msg -> [$z] -> {level
} > $oldlevel) {
81 # this msg is a child of the last one
84 $oldlevel = $msg -> [$z] -> {level
};
87 elsif ($msg -> [$z] -> {level
} < $oldlevel) {
88 # this starts a new subbranch (-1+ level(s))
91 # remove last msg (incl. kids), that is on same level
93 splice @path, $msg -> [$z] -> {level
};
95 $oldlevel = $msg -> [$z] -> {level
};
99 # the msg is a sister of the last one
104 # 'archive' is an admin flag
105 # if set, the message (incl. branch) MUST be archived
107 if (defined $msg->[$z]->{archive
} and $msg->[$z]->{archive
}) {
108 $archive{$msg->[$_]->{mid
}} = 1 for (@path);
111 # notice invisble messages
112 # while they are in @path and archive flag is not set,
113 # they and their kids WON'T be archived
115 $hidden{$z} = 1 if ($msg->[$z]->{deleted
});
117 # if 'archive' is NOT set and message not deleted,
119 unless ($msg->[$z]->{archive
} or $msg->[$z]->{deleted
}) {
120 my $key = $sum->{$tid}->{$msg->[$z]->{mid
}};
122 # ...and they've voted enough, it will be archived
124 if ($percent == 0 or ($key->{views
} and ($key->{votings
} * 100 / $key->{views
}) >= $percent)) {
127 # check on hidden messages in @path
136 # set archive-flag for messages in @path,
137 # unless a parent message is hidden
139 unless ($hidden_in_path) {
140 $archive{$msg->[$_]->{mid
}} = 1 for (@path);
146 # now remove messages without 'archive'-flag
149 for (reverse grep {!$archive{$_->{mid
}}} @
$msg) {
150 my $h = get_message_node
($xml, "t$tid", 'm'.$_->{mid
});
152 # remove message entry
154 $h -> getParentNode
-> removeChild
($h);
156 # remove message text
158 $h = get_body_node
($xml, 'm'.$_->{mid
});
159 $h -> getParentNode
-> removeChild
($h);
167 ### create_arcdir () ###########################################################
169 # check, if specific directories for year and month exist, create
172 # Params: $path - archive root
173 # $time - Thread time (GMT)
175 # Return: List: $path - /path/to/ to archived thread file
176 # $error - error or undef
178 sub create_arcdir
($$) {
179 my ($path, $time) = @_;
181 my ($month, $year) = (localtime ($time))[4,5];
183 # use the 'real' values for directory names
185 $month++; $year+=1900;
187 my $yeardir = $path . $year;
188 my $monthdir = $yeardir . '/' . $month;
189 my $monthpath = $monthdir . '/';
191 mkdir $yeardir, 0777 unless (-d
$yeardir);
192 return ('', "could not create directory '$yeardir'") unless (-d
$yeardir);
194 mkdir $monthdir, 0777 unless (-d
$monthdir);
195 return ('', "could not create directory '$monthdir'") unless (-d
$monthdir);
197 # return path, successfully created
202 ### process_threads () #########################################################
204 # process obsolete threads
205 # (transmit views/votings from cache, do archive, if necessary)
207 # Params: $par - hash reference
208 # (opt, cache, failed, obsolete, messagePath,
209 # archivePath, adminDefault)
211 # Return: hashref (tid => $msg)
213 sub process_threads
($) {
216 my ($opt, $failed, $obsolete, $cache) = map {$par->{$_}} qw
217 ( opt failed obsolete cache
);
221 if ($opt->{exArchiving
}) {
225 my $sum = $cache -> summary
;
228 # iterate over all obsolete threads, that are not failed yet
230 for my $tid (grep {not exists ($failed->{$_})} @
$obsolete) {
231 my $xml = parse_xml_file
($par->{messagePath
}."t$tid.xml");
236 $failed->{$tid} = 'could not parse thread file.';
241 my $tnode = $xml -> getElementsByTagName
('Thread') -> item
(0);
242 my $msg = parse_single_thread
($tnode, KEEP_DELETED
);
244 if ($opt->{archiving
} eq 'UserVotings') {
246 # filter out the bad stuff
248 delete_no_archived
({
253 percent
=> $par->{adminDefault
}->{Voting
}->{Limit
}
257 # save back xml file (into archive)
259 if ($tnode -> hasChildNodes
) {
261 # insert views and votings counter
263 for ($tnode -> getElementsByTagName
('Message')) {
264 my ($id) = $_ -> getAttribute
('id') =~ /(\d+)/;
265 $_ -> setAttribute
('views' => $sum->{$tid}->{$id}->{views
});
266 $_ -> setAttribute
('votings' => $sum->{$tid}->{$id}->{votings
});
269 # create archive dir, unless exists
271 my ($path, $error) = create_arcdir
($par -> {archivePath
}, $msg->[0]->{time});
274 $failed->{$tid} = $error;
279 my $file = "${path}t$tid.xml";
280 unless (save_file
($file => \
($xml -> toString
))) {
281 $failed->{$tid} = "could not save '$file'";
284 $archived{$tid} = $msg
292 @
$failed{@
$obsolete} = 'error: could not load summary';
299 ### append_threads () ##########################################################
301 # open specified index file, append threads, save it back
303 # Params: $file - /path/to/indexfile
304 # $threads - hashref (threads)
306 # Return: success code (boolean)
308 sub append_threads
($$) {
309 my ($file, $threads) = @_;
312 my $index = new Lock
($file);
314 return unless ($index -> lock (LH_EXCL
));
317 $thash = get_all_threads
($file => KEEP_DELETED
);
318 $thash->{$_} = $threads->{$_} for (keys %$threads);
326 my $saved = save_file
(
327 $file => create_forum_xml_string
(
339 return unless $saved;
344 ### indexpath () ###############################################################
346 # compose relative path of archive index file
348 # Params: $param - hash reference
351 # Return: $string (relative path)
356 my ($month, $year) = (localtime ($root->{time}))[4,5];
358 # use the 'real' values for directory names
360 $month++; $year+=1900;
365 ### index_threads () ###########################################################
367 # add threads to their specific archive index file
369 # Params: $param - hash reference
370 # (threads, archivePath, archiveIndex, failed)
374 sub index_threads
($) {
377 my ($threads, $failed) = map {$par->{$_}} qw
380 # indexfile => hashref of threads
381 # for more efficiency (open each index file *once*)
385 # iterate over all archived threads,
386 # prepare indexing and assign threads to indexfiles
388 for my $thread (keys %$threads) {
390 # index only, if the root is visible
392 unless ($threads->{$thread}->[0]->{deleted
}) {
393 my $file = $par->{archivePath
} . indexpath
($threads->{$thread}->[0]) . $par->{archiveIndex
};
394 $index{$file} = {} unless exists($index{$file});
396 $index{$file} -> {$thread} = [$threads->{$thread}->[0]];
400 # now append threads to index files
402 for my $file (keys %index) {
403 unless (append_threads
($file => $index{$file})) {
404 $failed->{$_} = "error: could not list in '$file'" for (keys %{$index{$file}});
409 ### cut_tail () ################################################################
411 # shorten the main file and archive, if necessary
413 # Params: $param - hash reference
414 # (forumFile, messagePath, archivePath, lockFile, adminDefault,
415 # cachePath, archiveIndex)
417 # Return: hash reference - empty if all right done
424 $param->{adminDefault
}->{Severance
}->{severance
} ne 'instant' or
425 $param->{adminDefault
}->{Instant
}->{execute
}
427 # run only one instance at the same time
428 # (exlusive lock on sev_lock)
430 my $sev = new Lock
($param->{lockFile
});
431 if ($sev -> lock(LH_EXCL
)) {
433 # lock and parse forum main file
435 my $forum = new Lock
($param->{forumFile
});
436 if ($forum -> lock (LH_EXCL
)) {
442 ) = get_all_threads
($forum->filename, KEEP_DELETED
);
444 # get obsolete threads...
446 my $obsolete = get_obsolete_threads
({
447 parsedThreads
=> $threads,
448 adminDefault
=> $param->{adminDefault
}
451 unless (@
$obsolete) {
452 # nothing to cut - we're ready
457 # ...and delete them from main
461 $obsolete{$_} = $threads->{$_};
462 delete $threads->{$_};
467 my $saved = save_file
(
468 $param -> {forumFile
},
469 create_forum_xml_string
(
473 lastMessage
=> $last_message,
474 lastThread
=> $last_thread
479 # ...and masterlock the obsolete thread files
483 new Lock
($param->{messagePath
}."t$_.xml")->lock(LH_MASTER
) or $failed{$_} = 'error: could not set master lock';
487 # release forum main file...
492 # ...and now process thread files
494 my $sev_opt = ($param -> {adminDefault
} -> {Severance
} -> {severance
} eq 'instant')
495 ?
$param -> {adminDefault
} -> {Instant
} -> {Severance
}
496 : ($param -> {adminDefault
} -> {Severance
});
498 my $cache = new Posting
::Cache
($param->{cachePath
});
504 obsolete
=> $obsolete,
505 messagePath
=> $param->{messagePath
},
506 archivePath
=> $param->{archivePath
},
507 adminDefault
=> $param->{adminDefault
}
510 # delete processed files, that are not failed
514 for (grep {not exists($failed{$_})} @
$obsolete) {
515 if (exists($failed{$_})) {
516 delete $obsolete{$_};
519 unless (unlink ($param->{messagePath
}."t$_.xml")) {
520 $failed{$_} = 'warning: could not delete thread file';
524 #file_removed ($param->{messagePath}."t$_.xml");
530 $cache -> delete_threads
(@removed);
531 $cache -> garbage_collection
;
534 # add archived threads to archive index
537 threads
=> \
%obsolete,
538 archivePath
=> $param->{archivePath
},
539 archiveIndex
=> $param->{archiveIndex
},
545 # we're ready, tell this other (waiting?) instances
556 # keep 'require' happy
561 ### end of Arc::Archive ########################################################