Add __END_CONFIG__ token to remaining gavinc plugins.
[blosxom-plugins.git] / gavinc / entries_timestamp
blobb814c9f0e028410e3bbf6a056c1bf71e4c56a248
1 # Blosxom Plugin: entries_timestamp
2 # Author(s): Gavin Carr <gavin@openfusion.com.au>
3 # Version: 0.002000
4 # Documentation: See the bottom of this file or type: perldoc entries_timestamp
6 package entries_timestamp;
8 use strict;
9 use File::stat;
10 use File::Find;
11 use Data::Dumper;
12 use Time::Local;
13 use CGI ();
15 #use Blosxom::Debug debug_level => 2;
17 # --- Configurable variables -----
19 # Where should I store the entries_timestamp index file?
20 # IMO timestamps are metadata rather than state, but you may well not care.
21 my $meta_dir = "$blosxom::datadir/../meta";
22 #my $meta_dir = $blosxom::plugin_state_dir;
24 # What name should my entries_timestamp index file be called?
25 # If you want to migrate from entries_index, you can just use the original
26 # entries_index .entries_index.index file, or or just copy/rename it.
27 my $entries_index = 'entries_timestamp.index';
28 #my $entries_index = '.entries_index.index';
30 # Reindexing password. If entries_timestamp finds a '?reindex=$reindex_password'
31 # parameter it will check and resync machine timestamps to the human versions
32 my $reindex_password = 'abracad';    # CHANGEME!
34 # --------------------------------
35 # __END_CONFIG__
37 my $q = CGI->new;
39 use vars qw($TS_MACHINE $TS_HUMAN $SYMLINKS $VAR1 $VAR2 $VAR3);
41 sub start { 1 }
43 sub entries {
44   return sub {
45     my(%indexes, %files_ts, %files_ts_str, %files_symlinks);
47     # Read $entries_index
48     if ( open ENTRIES, "$meta_dir/$entries_index" ) {
49       my $index = join '', <ENTRIES>;
50       close ENTRIES;
51       if ( $index =~ m/\$(TS_\w+|VAR1) = \{/ ) {
52         eval $index;
53         if ( $@ ) {
54           warn "(entries_timestamp) eval of $entries_index failed: $@";
55           return;
56         }
57         else {
58           if ($TS_MACHINE && keys %$TS_MACHINE) {
59             %files_ts = %$TS_MACHINE;
60           } elsif ($VAR1 && keys %$VAR1) {
61             %files_ts = %$VAR1;
62           }
63           if ($TS_HUMAN && keys %$TS_HUMAN) {
64             %files_ts_str = %$TS_HUMAN;
65           } elsif ($VAR2 && keys %$VAR2) {
66             %files_ts_str = %$VAR2;
67           }
68           if ($SYMLINKS && keys %$SYMLINKS) {
69             %files_symlinks = %$SYMLINKS;
70           } elsif ($VAR3 && keys %$VAR3) {
71             %files_symlinks = %$VAR3;
72           }
73         }
74       } 
75     }
76     %files_ts_str = () unless defined %files_ts_str;
77     %files_symlinks = () unless defined %files_symlinks;
79     my $index_mods = 0;
81     # Check for deleted files
82     for my $file (keys %files_ts) { 
83       if ( ! -f $file || ( -l $file && ! -f readlink($file)) ) {
84         $index_mods++; 
85         delete $files_ts{$file};
86         delete $files_ts_str{$file};
87         delete $files_symlinks{$file};
88         # debug(2, "deleting removed file '$file' from indexes");
89       } 
90     }
92     # Check for new files
93     find(
94       sub {
95         my $d; 
96         my $curr_depth = $File::Find::dir =~ tr[/][]; 
97         if ( $blosxom::depth and $curr_depth > $blosxom::depth ) {
98           delete $files_ts{$File::Find::name};
99           delete $files_ts_str{$File::Find::name};
100           delete $files_symlinks{$File::Find::name};
101           return;
102         }
103      
104         # Return unless a match
105         return unless $File::Find::name =~ 
106           m! ^$blosxom::datadir/(?:(.*)/)?(.+)\.$blosxom::file_extension$ !x;
107         my $path = $1;
108         my $filename = $2;
109         # Return if an index, a dotfile, or unreadable
110         if ( $filename eq 'index' or $filename =~ /^\./ or ! -r $File::Find::name ) {
111           # debug(1, "(entries_timetamp) '$path/$filename.$blosxom::file_extension' is an index, a dotfile, or is unreadable - skipping\n");
112           return;
113         }
115         # Get modification time
116         my $mtime = stat($File::Find::name)->mtime or return;
118         # Ignore if future unless $show_future_entries is set
119         return unless $blosxom::show_future_entries or $mtime <= time;
121         my @nice_date = blosxom::nice_date( $mtime );
123         # If a new symlink, add to %files_symlinks
124         if ( -l $File::Find::name ) {
125           if ( ! exists $files_symlinks{ $File::Find::name } ) {
126             $files_symlinks{$File::Find::name} = 1;
127             $index_mods++;
128             # Backwards compatibility deletes
129             delete $files_ts{$File::Find::name};
130             delete $files_ts_str{$File::Find::name};
131             # debug(2, "new file_symlinks entry $File::Find::name, index_mods now $index_mods");
132           }
133         }
135         # If a new file, add to %files_ts and %files_ts_str
136         else {
137           if ( ! exists $files_ts{$File::Find::name} ) {
138             $files_ts{$File::Find::name} = $mtime;
139             $index_mods++;
140             # debug(2, "new file entry $File::Find::name, index_mods now $index_mods");
141           }
142           if ( ! exists $files_ts_str{$File::Find::name} ) {
143             my $date = join('-', @nice_date[5,2,3]);
144             my $time = sprintf '%s:%02d', $nice_date[4], (localtime($mtime))[0];
145             $files_ts_str{$File::Find::name} = join(' ', $date, $time, $nice_date[6]);
146             $index_mods++;
147             # debug(2, "new file_ts_str entry $File::Find::name, index_mods now $index_mods");
148           }
149          
150           # If asked to reindex, check and sync machine timestamps to the human ones
151           if ( my $reindex = $q->param('reindex') ) {
152             if ( $reindex eq $reindex_password ) {
153               if ( my $reindex_ts = parse_ts( $files_ts_str{$File::Find::name} )) {
154                 if ($reindex_ts != $files_ts{$File::Find::name}) {
155                   # debug(1, "reindex: updating timestamp on '$File::Find::name'\n");
156                   # debug(2, "reindex_ts $reindex_ts, files_ts $files_ts{$File::Find::name}");
157                   $files_ts{$File::Find::name} = $reindex_ts;
158                   $index_mods++;
159                 }
160               }
161               else {
162                 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";
163               }
164             }
165             else {
166               warn "(entries_timestamp) Warning: reindex requested with incorrect password\n";
167             }
168           }
169         }
171         # Static rendering
172         if ($blosxom::static_entries) {
173           my $static_file = "$blosxom::static_dir/$path/index.$blosxom::static_flavours[0]";
174           if ( $q->param('-all') 
175                or ! -f $static_file
176                or stat($static_file)->mtime < $mtime ) {
177             # debug(3, "static_file: $static_file");
178             $indexes{$path} = 1;
179             $d = join('/', @nice_date[5,2,3]);
180             $indexes{$d} = $d;
181             $path = $path ? "$path/" : '';
182             $indexes{ "$path$filename.$blosxom::file_extension" } = 1;
183           }
184         }
185       }, $blosxom::datadir
186     );
188     # If updates, save back to index
189     if ( $index_mods ) {
190       # debug(1, "index_mods $index_mods, saving \%files to $meta_dir/$entries_index");
191       if ( open ENTRIES, "> $meta_dir/$entries_index" ) {
192         print ENTRIES Data::Dumper->Dump([ \%files_ts_str, \%files_ts, \%files_symlinks ],
193           [ qw(TS_HUMAN TS_MACHINE SYMLINKS) ] );
194         close ENTRIES;
195       } 
196       else {
197         warn "(entries_timestamp) couldn't open $meta_dir/$entries_index for writing: $!\n";
198       }
199     }
201     # Generate blosxom %files from %files_ts and %files_symlinks
202     my %files = %files_ts;
203     for (keys %files_symlinks) {
204       # Add to %files with mtime of referenced file
205       my $target = readlink $_;
206       # Note that we only support symlinks pointing to other posts
207       $files{ $_ } = $files{ $target } if exists $files{ $target };
208     }
210     return (\%files, \%indexes);
211   };
214 # Helper function to parse human-friendly %Y-%m-%d %T %z timestamps
215 sub parse_ts {
216   my ($ts_str) = @_;
218   if ($ts_str =~ m/^(\d{4})-(\d{2})-(\d{2})           # %Y-%m-%d
219                     \s+ (\d{2}):(\d{2})(?::(\d{2}))?  # %H-%M-%S
220                     (?:\s+ [+-]?(\d{2})(\d{2})?)?     # %z
221                   /x) {
222     my ($yy, $mm, $dd, $hh, $mi, $ss, $zh, $zm) = ($1, $2, $3, $4, $5, $6, $7, $8);
223     $mm--;
224     # FIXME: just use localtime for now
225     if (my $mtime = timelocal($ss, $mi, $hh, $dd, $mm, $yy)) {
226       return $mtime;
227     }
228   }
230   return 0;
235 __END__
237 =head1 NAME
239 entries_timestamp: blosxom plugin to capture and preserve the original
240 creation timestamp on blosxom posts
242 =head1 SYNOPSIS
244 entries_timestamp is a blosxom plugin for capturing and preserving the
245 original creation timestamp on blosxom posts. It is based on Rael
246 Dornfest's original L<entries_index> plugin, and works in the same way:
247 it maintains an index file (configurable, but 'entries_timestamp.index',
248 by default) maintaining creation timestamps for all blosxom posts, and
249 replaces the default $blosxom::entries subrouting with one returning a 
250 file hash using that index.
252 It differs from Rael's L<entries_index> as follows:
254 =over 4
256 =item User-friendly timestamps
258 The index file contains two timestamps for every file - the 
259 machine-friendly system L<time/2> version, for use by blosxom, and a
260 human-friendly stringified timestamp, to allow timestamps to be reviewed
261 and or modified easily.
263 entries_timestamp ordinarily just assumes those timestamps are in sync,
264 and ignores the string version. If you update the string version and want
265 that to override the system time, you should pass a 
266 ?reindex=<reindex_password> argument to blosxom to force the system
267 timestamps to be checked and updated.
269 =item Separate symlink handling
271 entries_timestamp uses separate indexes for posts that are files and
272 posts that are symlinks, and doesn't bother to cache timestamps for
273 the latter at all, deriving them instead from the post they point to.
274 (Note that this means entries_timestamp currently doesn't support 
275 symlinks to non-post files at all - they're just ignored).
277 =item Configurable index file name and location
279 I consider post timestamps to be metadata rather than state, so I 
280 tend to use a separate C<meta> directory alongside by posts for this,
281 rather than the traditional $plugin_state_dir. You may note care. ;-)
283 =item A complete rewrite
285 Completely rewritten code, since the original used evil evil and-chains 
286 and was pretty difficult to understand (imho).
288 =back
290 =head1 SEE ALSO
292 L<entries_index>, L<entries_cache>, L<entries_cache_meta>
294 Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
296 =head1 ACKNOWLEDGEMENTS
298 This plugin is largely based on Rael Dornfest's original 
299 L<entries_index> plugin.
301 =head1 BUGS AND LIMITATIONS
303 entries_timestamp currently only supports symlinks to local post files,
304 not symlinks to arbitrary files outside your $datadir.
306 entries_timestamp doesn't currently do any kind caching, so it's not
307 directly equivalent to L<entries_cache> or L<entries_cache_meta>.
309 Please report bugs either directly to the author or to the blosxom 
310 development mailing list: <blosxom-devel@lists.sourceforge.net>.
312 =head1 AUTHOR
314 Gavin Carr <gavin@openfusion.com.au>, http://www.openfusion.net/
316 =head1 LICENSE
318 Copyright 2007, Gavin Carr.
320 This plugin is licensed under the same terms as blosxom itself i.e.
322 Permission is hereby granted, free of charge, to any person obtaining a
323 copy of this software and associated documentation files (the "Software"),
324 to deal in the Software without restriction, including without limitation
325 the rights to use, copy, modify, merge, publish, distribute, sublicense,
326 and/or sell copies of the Software, and to permit persons to whom the
327 Software is furnished to do so, subject to the following conditions:
329 The above copyright notice and this permission notice shall be included
330 in all copies or substantial portions of the Software.
332 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
333 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
334 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
335 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
336 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
337 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
338 OTHER DEALINGS IN THE SOFTWARE.
340 =cut
342 # vim:ft=perl