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