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