Add entries_timestamp.
authorGavin Carr <gonzai@users.sourceforge.net>
Mon, 29 Oct 2007 09:37:22 +0000 (09:37 +0000)
committerGavin Carr <gonzai@users.sourceforge.net>
Mon, 29 Oct 2007 09:37:22 +0000 (09:37 +0000)
gavinc/entries_timestamp [new file with mode: 0644]

diff --git a/gavinc/entries_timestamp b/gavinc/entries_timestamp
new file mode 100644 (file)
index 0000000..e32f0c5
--- /dev/null
@@ -0,0 +1,328 @@
+# 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