# Blosxom Plugin: entries_timestamp
# Author(s): Gavin Carr <gavin@openfusion.com.au>
# Version: 0.002000
# 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 ();

# Uncomment next line to enable debug output (don't uncomment debug() lines)
#use Blosxom::Debug debug_level => 2;

use Blosxom::Include qw(entries_timestamp);

# --- Configurable variables -----

my %config = ();

# Where should I store the entries_timestamp index file?
# IMO timestamps are metadata rather than state, but you may well not care.
$config{meta_dir} = "$blosxom::datadir/../meta";
#$config{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.
$config{entries_index} = 'entries_timestamp.index';
#$config{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
$config{reindex_password} = 'abracad';    # CHANGEME!

# --------------------------------
# __END_CONFIG__

my $q = CGI->new;

use vars qw($TS_MACHINE $TS_HUMAN $SYMLINKS $VAR1 $VAR2 $VAR3);

sub start { 1 }

sub entries {
  return sub {
    my(%indexes, %files_ts, %files_ts_str, %files_symlinks);

    # Read $config{entries_index}
    if ( open ENTRIES, "$config{meta_dir}/$config{entries_index}" ) {
      my $index = join '', <ENTRIES>;
      close ENTRIES;
      if ( $index =~ m/\$(TS_\w+|VAR1) = \{/ ) {
        eval $index;
        if ( $@ ) {
          warn "(entries_timestamp) eval of $config{entries_index} failed: $@";
          return;
        }
        else {
          if ($TS_MACHINE && keys %$TS_MACHINE) {
            %files_ts = %$TS_MACHINE;
          } elsif ($VAR1 && keys %$VAR1) {
            %files_ts = %$VAR1;
          }
          if ($TS_HUMAN && keys %$TS_HUMAN) {
            %files_ts_str = %$TS_HUMAN;
          } elsif ($VAR2 && keys %$VAR2) {
            %files_ts_str = %$VAR2;
          }
          if ($SYMLINKS && keys %$SYMLINKS) {
            %files_symlinks = %$SYMLINKS;
          } elsif ($VAR3 && keys %$VAR3) {
            %files_symlinks = %$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 $config{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 $config{meta_dir}/$config{entries_index}");
      if ( open ENTRIES, "> $config{meta_dir}/$config{entries_index}" ) {
        print ENTRIES Data::Dumper->Dump([ \%files_ts_str, \%files_ts, \%files_symlinks ],
          [ qw(TS_HUMAN TS_MACHINE SYMLINKS) ] );
        close ENTRIES;
      } 
      else {
        warn "(entries_timestamp) couldn't open $config{meta_dir}/$config{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
