--- /dev/null
+# Blosxom Plugin: entries_timestamp
+# Author(s): Gavin Carr <gavin@openfusion.com.au>
+# Version: 0.001000
+# Documentation: See the bottom of this file or type: perldoc entries_timestamp
+
+package entries_timestamp;
+
+use strict;
+use File::stat;
+use File::Find;
+use Data::Dumper;
+use Time::Local;
+use CGI ();
+
+#use Blosxom::Debug debug_level => 1;
+
+# --- Configurable variables -----
+
+# Where should I store the entries_timestamp index file?
+# IMO timestamps are metadata rather than state, but you may well not care.
+my $meta_dir = "$blosxom::datadir/../meta";
+#my $meta_dir = $blosxom::plugin_state_dir;
+
+# What name should my entries_timestamp index file be called?
+# If you want to migrate from entries_index, you can just use the original
+# entries_index .entries_index.index file, or or just copy/rename it.
+my $entries_index = 'entries_timestamp.index';
+#my $entries_index = '.entries_index.index';
+
+# Reindexing password. If entries_timestamp finds a '?reindex=$reindex_password'
+# parameter it will check and resync machine timestamps to the human versions
+my $reindex_password = 'abracad'; # CHANGEME!
+
+# --------------------------------
+
+my $q = CGI->new;
+
+use vars qw($VAR1 $VAR2 $VAR3);
+
+sub start { 1 }
+
+sub entries {
+ return sub {
+ my(%indexes, %files_ts, %files_ts_str, %files_symlinks);
+
+ # Read $entries_index
+ if ( open ENTRIES, "$meta_dir/$entries_index" ) {
+ my $index = join '', <ENTRIES>;
+ close ENTRIES;
+ if ( $index =~ m/\$VAR1 = \{/ ) {
+ eval $index;
+ if ( $@ ) {
+ warn "(entries_timestamp) eval of $entries_index failed: $@";
+ return;
+ }
+ else {
+ %files_ts = %$VAR1;
+ %files_ts_str = %$VAR2 if keys %$VAR2;
+ %files_symlinks = %$VAR3 if keys %$VAR3;
+ }
+ }
+ }
+ %files_ts_str = () unless defined %files_ts_str;
+ %files_symlinks = () unless defined %files_symlinks;
+
+ my $index_mods = 0;
+
+ # Check for deleted files
+ for my $file (keys %files_ts) {
+ if ( ! -f $file || ( -l $file && ! -f readlink($file)) ) {
+ $index_mods++;
+ delete $files_ts{$file};
+ delete $files_ts_str{$file};
+ delete $files_symlinks{$file};
+ # debug(2, "deleting removed file '$file' from indexes");
+ }
+ }
+
+ # Check for new files
+ find(
+ sub {
+ my $d;
+ my $curr_depth = $File::Find::dir =~ tr[/][];
+ if ( $blosxom::depth and $curr_depth > $blosxom::depth ) {
+ delete $files_ts{$File::Find::name};
+ delete $files_ts_str{$File::Find::name};
+ delete $files_symlinks{$File::Find::name};
+ return;
+ }
+
+ # Return unless a match
+ return unless $File::Find::name =~
+ m! ^$blosxom::datadir/(?:(.*)/)?(.+)\.$blosxom::file_extension$ !x;
+ my $path = $1;
+ my $filename = $2;
+ # Return if an index, a dotfile, or unreadable
+ if ( $filename eq 'index' or $filename =~ /^\./ or ! -r $File::Find::name ) {
+ # debug(1, "(entries_timetamp) '$path/$filename.$blosxom::file_extension' is an index, a dotfile, or is unreadable - skipping\n");
+ return;
+ }
+
+ # Get modification time
+ my $mtime = stat($File::Find::name)->mtime or return;
+
+ # Ignore if future unless $show_future_entries is set
+ return unless $blosxom::show_future_entries or $mtime <= time;
+
+ my @nice_date = blosxom::nice_date( $mtime );
+
+ # If a new symlink, add to %files_symlinks
+ if ( -l $File::Find::name ) {
+ if ( ! exists $files_symlinks{ $File::Find::name } ) {
+ $files_symlinks{$File::Find::name} = 1;
+ $index_mods++;
+ # Backwards compatibility deletes
+ delete $files_ts{$File::Find::name};
+ delete $files_ts_str{$File::Find::name};
+ # debug(2, "new file_symlinks entry $File::Find::name, index_mods now $index_mods");
+ }
+ }
+
+ # If a new file, add to %files_ts and %files_ts_str
+ else {
+ if ( ! exists $files_ts{$File::Find::name} ) {
+ $files_ts{$File::Find::name} = $mtime;
+ $index_mods++;
+ # debug(2, "new file entry $File::Find::name, index_mods now $index_mods");
+ }
+ if ( ! exists $files_ts_str{$File::Find::name} ) {
+ my $date = join('-', @nice_date[5,2,3]);
+ my $time = sprintf '%s:%02d', $nice_date[4], (localtime($mtime))[0];
+ $files_ts_str{$File::Find::name} = join(' ', $date, $time, $nice_date[6]);
+ $index_mods++;
+ # debug(2, "new file_ts_str entry $File::Find::name, index_mods now $index_mods");
+ }
+
+ # If asked to reindex, check and sync machine timestamps to the human ones
+ if ( my $reindex = $q->param('reindex') ) {
+ if ( $reindex eq $reindex_password ) {
+ if ( my $reindex_ts = parse_ts( $files_ts_str{$File::Find::name} )) {
+ if ($reindex_ts != $files_ts{$File::Find::name}) {
+ # debug(1, "reindex: updating timestamp on '$File::Find::name'\n");
+ # debug(2, "reindex_ts $reindex_ts, files_ts $files_ts{$File::Find::name}");
+ $files_ts{$File::Find::name} = $reindex_ts;
+ $index_mods++;
+ }
+ }
+ else {
+ warn "(entries_timestamp) Warning: bad timestamp '$files_ts_str{$File::Find::name}' on file '$File::Find::name' - failed to parse (not %Y-%m-%d %T %z?)\n";
+ }
+ }
+ else {
+ warn "(entries_timestamp) Warning: reindex requested with incorrect password\n";
+ }
+ }
+ }
+
+ # Static rendering
+ if ($blosxom::static_entries) {
+ my $static_file = "$blosxom::static_dir/$path/index.$blosxom::static_flavours[0]";
+ if ( $q->param('-all')
+ or ! -f $static_file
+ or stat($static_file)->mtime < $mtime ) {
+ # debug(3, "static_file: $static_file");
+ $indexes{$path} = 1;
+ $d = join('/', @nice_date[5,2,3]);
+ $indexes{$d} = $d;
+ $path = $path ? "$path/" : '';
+ $indexes{ "$path$filename.$blosxom::file_extension" } = 1;
+ }
+ }
+ }, $blosxom::datadir
+ );
+
+ # If updates, save back to index
+ if ( $index_mods ) {
+ # debug(1, "index_mods $index_mods, saving \%files to $meta_dir/$entries_index");
+ if ( open ENTRIES, "> $meta_dir/$entries_index" ) {
+ print ENTRIES Dumper \%files_ts, \%files_ts_str, \%files_symlinks;
+ close ENTRIES;
+ }
+ else {
+ warn "(entries_timestamp) couldn't open $meta_dir/$entries_index for writing: $!\n";
+ }
+ }
+
+ # Generate blosxom %files from %files_ts and %files_symlinks
+ my %files = %files_ts;
+ for (keys %files_symlinks) {
+ # Add to %files with mtime of referenced file
+ my $target = readlink $_;
+ # Note that we only support symlinks pointing to other posts
+ $files{ $_ } = $files{ $target } if exists $files{ $target };
+ }
+
+ return (\%files, \%indexes);
+ };
+}
+
+# Helper function to parse human-friendly %Y-%m-%d %T %z timestamps
+sub parse_ts {
+ my ($ts_str) = @_;
+
+ if ($ts_str =~ m/^(\d{4})-(\d{2})-(\d{2}) # %Y-%m-%d
+ \s+ (\d{2}):(\d{2})(?::(\d{2}))? # %H-%M-%S
+ (?:\s+ [+-]?(\d{2})(\d{2})?)? # %z
+ /x) {
+ my ($yy, $mm, $dd, $hh, $mi, $ss, $zh, $zm) = ($1, $2, $3, $4, $5, $6, $7, $8);
+ $mm--;
+ # FIXME: just use localtime for now
+ if (my $mtime = timelocal($ss, $mi, $hh, $dd, $mm, $yy)) {
+ return $mtime;
+ }
+ }
+
+ return 0;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+entries_timestamp: blosxom plugin to capture and preserve the original
+creation timestamp on blosxom posts
+
+=head1 SYNOPSIS
+
+entries_timestamp is a blosxom plugin for capturing and preserving the
+original creation timestamp on blosxom posts. It is based on Rael
+Dornfest's original L<entries_index> plugin, and works in the same way:
+it maintains an index file (configurable, but 'entries_timestamp.index',
+by default) maintaining creation timestamps for all blosxom posts, and
+replaces the default $blosxom::entries subrouting with one returning a
+file hash using that index.
+
+It differs from Rael's L<entries_index> as follows:
+
+=over 4
+
+=item User-friendly timestamps
+
+The index file contains two timestamps for every file - the
+machine-friendly system L<time/2> version, for use by blosxom, and a
+human-friendly stringified timestamp, to allow timestamps to be reviewed
+and or modified easily.
+
+entries_timestamp ordinarily just assumes those timestamps are in sync,
+and ignores the string version. If you update the string version and want
+that to override the system time, you should pass a
+?reindex=<reindex_password> argument to blosxom to force the system
+timestamps to be checked and updated.
+
+=item Separate symlink handling
+
+entries_timestamp uses separate indexes for posts that are files and
+posts that are symlinks, and doesn't bother to cache timestamps for
+the latter at all, deriving them instead from the post they point to.
+(Note that this means entries_timestamp currently doesn't support
+symlinks to non-post files at all - they're just ignored).
+
+=item Configurable index file name and location
+
+I consider post timestamps to be metadata rather than state, so I
+tend to use a separate C<meta> directory alongside by posts for this,
+rather than the traditional $plugin_state_dir. You may note care. ;-)
+
+=item A complete rewrite
+
+Completely rewritten code, since the original used evil evil and-chains
+and was pretty difficult to understand (imho).
+
+=back
+
+=head1 SEE ALSO
+
+L<entries_index>, L<entries_cache>, L<entries_cache_meta>
+
+Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
+
+=head1 ACKNOWLEDGEMENTS
+
+This plugin is largely based on Rael Dornfest's original
+L<entries_index> plugin.
+
+=head1 BUGS AND LIMITATIONS
+
+entries_timestamp currently only supports symlinks to local post files,
+not symlinks to arbitrary files outside your $datadir.
+
+entries_timestamp doesn't currently do any kind caching, so it's not
+directly equivalent to L<entries_cache> or L<entries_cache_meta>.
+
+Please report bugs either directly to the author or to the blosxom
+development mailing list: <blosxom-devel@lists.sourceforge.net>.
+
+=head1 AUTHOR
+
+Gavin Carr <gavin@openfusion.com.au>, http://www.openfusion.net/
+
+=head1 LICENSE
+
+Copyright 2007, Gavin Carr.
+
+This plugin is licensed under the same terms as blosxom itself i.e.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+=cut
+
+# vim:ft=perl