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