File Coverage

blib/lib/WE_Frontend/Publish/FTP_MD5Sync.pm
Criterion Covered Total %
statement 25 172 14.5
branch 1 106 0.9
condition 0 33 0.0
subroutine 9 11 81.8
pod n/a
total 35 322 10.8


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # $Id: FTP_MD5Sync.pm,v 1.5 2004/06/10 13:18:02 eserte Exp $
5             # Author: Slaven Rezic
6             #
7             # Copyright (C) 2002 Online Office Berlin. All rights reserved.
8             # Copyright (C) 2002 Slaven Rezic.
9             # This is free software; you can redistribute it and/or modify it under the
10             # terms of the GNU General Public License, see the file COPYING.
11              
12             #
13             # Mail: slaven@rezic.de
14             # WWW: http://we-framework.sourceforge.net
15             #
16              
17             #XXX better mapping mechanism! rdist? an existing perl module?
18              
19             =head1 NAME
20              
21             WE_Frontend::Publish::FTP_MD5Sync - publish with FTP using MD5 fingerprints
22              
23             =head1 SYNOPSIS
24              
25             use WE_Frontend::Main2;
26             use WEsiteinfo qw($c);
27             $c->staging->transport("ftp-md5sync");
28             $main->publish;
29              
30             or
31              
32             use WE_Frontend::Main;
33             use WEsiteinfo;
34             $WEsiteinfo::livetransport = "ftp-md5sync";
35             $main->publish;
36              
37             =head1 DESCRIPTION
38              
39             =cut
40              
41             package WE_Frontend::Publish::FTP_MD5Sync;
42              
43 1     1   929 use vars qw($VERSION);
  1         2  
  1         70  
44             $VERSION = sprintf("%d.%02d", q$Revision: 1.5 $ =~ /(\d+)\.(\d+)/);
45              
46             package WE_Frontend::Main;
47              
48 1     1   6 use strict;
  1         2  
  1         28  
49              
50 1     1   972 use Net::FTP;
  1         41948  
  1         61  
51 1     1   12 use LWP::UserAgent;
  1         3  
  1         21  
52 1     1   5 use Digest::MD5;
  1         3  
  1         33  
53              
54 1     1   5 use WE_Frontend::Publish;
  1         2  
  1         50  
55              
56             BEGIN {
57 1 50   1   21 if ($] < 5.006) {
58 0         0 $INC{"warnings.pm"}++;
59 0         0 eval q{
60             package warnings;
61             sub unimport { }
62 0 0       0 }; die $@ if $@;
63             }
64             }
65              
66             {
67 1     1   4 no warnings 'redefine';
  1         3  
  1         41  
68 1     1   5 use WE::Util::Functions qw(_save_pwd);
  1         1  
  1         1967  
69             }
70              
71             sub publish_ftp_md5sync {
72 0     0     my($self, %args) = @_;
73              
74 0           my $v = delete $args{-verbose};
75 0           my $dryrun = delete $args{-n};
76              
77 0           my $liveuser = $self->Config->staging->user;
78 0           my $livepassword = $self->Config->staging->password;
79 0           my $livedirectory = $self->Config->staging->directory;
80 0           my $livecgidirectory = $self->Config->staging->cgidirectory;
81 0           my $livehost = $self->Config->staging->host;
82 0           my $pubhtmldir = $self->Config->paths->pubhtmldir;
83 0           my @extracgi = (ref $self->Config->project->stagingextracgi eq 'ARRAY'
84 0 0         ? @{ $self->Config->project->stagingextracgi }
85             : ()
86             );
87 0           my $md5listcgi = $self->Config->staging->stagingext->{'md5listcgi'};
88 0           my $topdirectory = $self->Config->staging->stagingext->{'topdirectory'};
89 0           my $deleteold = $self->Config->staging->stagingext->{'deleteold'};
90 0           my $movetotrash = $self->Config->staging->stagingext->{'movetotrash'};
91 0           my $trashdirectory = $self->Config->staging->stagingext->{'trashdirectory'};
92 0 0         if ($self->Config->staging->stagingext->{'dryrun'}) {
93 0           $dryrun++;
94             }
95              
96 0 0 0       die "Can't use deleteold and movetotrash"
97             if $deleteold && $movetotrash;
98 0 0 0       die "movetotrash defined but there is no trashdirectory"
99             if $movetotrash && !defined $trashdirectory;
100              
101             =head2 WESITEINFO CONFIGURATION
102              
103             This refers to the old format (first name) or the new format (second
104             name).
105              
106             =over 4
107              
108             =item $livetransport or $c->staging->transport
109              
110             The transport protocol should be set to "ftp-md5sync".
111              
112             =item $liveuser or $c->staging->user
113              
114             The remote FTP user.
115              
116             =cut
117              
118 0 0 0       if (!defined $liveuser || $liveuser eq '') {
119 0           die "The FTP user is missing (config member WEsiteinfo->staging->user)";
120             }
121              
122             =item $livepassword or $c->staging->password
123              
124             The remote FTP password.
125              
126             =cut
127              
128 0 0 0       if (!defined $livepassword || $livepassword eq '') {
129 0           die "The FTP password is missing (config member WEsiteinfo->staging->password)";
130             }
131              
132             =item $livedirectory or $c->staging->directory
133              
134             The remote FTP directory. This is not the real filesystem path on the
135             remote host, but the virtual FTP path. For example: the real
136             filesystem path may be somthing like C, but if
137             you login to the server as C, you will see C as the FTP
138             root path.
139              
140             If the FTP root is C, the value of C<$livedirectory> should be an
141             empty string.
142              
143             =item $livehost or $c->staging->host
144              
145             The remove host.
146              
147             =cut
148              
149 0 0 0       if (!defined $livehost || $livehost eq '') {
150 0           die "The target FTP host is missing (config member WEsiteinfo->staging->host)";
151             }
152              
153             =item $pubhtmldir or $c->paths->pubhtmldir
154              
155             The local htdocs directory.
156              
157             =cut
158              
159 0 0 0       if (!defined $pubhtmldir || $pubhtmldir eq '') {
160 0           die "The publish html directory is missing (config member WEsiteinfo->paths->pubhtmldir)";
161             }
162              
163             =item $livecgidirectory or $c->staging->cgidirectory
164              
165             If there are CGI programs to be published, the remote cgi directory
166             have to be specified. The same rules as in C<$livedirectory> apply.
167              
168             =item @stagingextracgi or $c->project->stagingextracgi
169              
170             An array reference with additional cgi scripts to be published.
171              
172             =cut
173              
174 0 0 0       if (@extracgi && (!defined $livecgidirectory || $livecgidirectory eq '')) {
      0        
175 0           die "Extra CGI scripts are defined (@extracgi),
176             but the WEsiteinfo->staging->cgidirectory config is missing";
177             }
178              
179             =item $livestagingext or $c->staging->stagingext
180              
181             A hash reference with additional attributes:
182              
183             =over 4
184              
185             =item dryrun
186              
187             If set to a true value, then do not execute the FTP commands, just
188             show them.
189              
190             =item md5listcgi
191              
192             The remote CGI script to create the MD5 list. The script is included
193             in the C as C.
194              
195             =item topdirectory
196              
197             The top directory of the remote server. Here the real filesystem path
198             should be used. In the example above, this would be
199             C.
200              
201             =item deleteold
202              
203             If true, then outdated remote files (not existing on the local side)
204             are deleted.
205              
206             =item movetotrash
207              
208             If true, then outdated remote files will be moved to the
209             C. Cannot be used together with C.
210              
211             =item trashdirectory
212              
213             The FTP directory name of a trash directory. Have to be defined if
214             C is set.
215              
216             =back
217              
218             =back
219              
220             =head2 GETMD5LIST.CGI CONFIGURATION
221              
222             The CGI script C is configured by creating a perl
223             file called C which should reside in the same
224             directory as the CGI script. The following perl variables may be set
225             as configuration variables:
226              
227             =over 4
228              
229             =item @directories
230              
231             A list of directories for which the MD5 fingerprints should be
232             collected. Normally these are C and C
233             from the C configuration.
234              
235             =item @digest_method
236              
237             Specify a list with the preferred methods to get the MD5 digest. This does not need to be set; C is smart enough to get a supported method automatically. Permitted values are:
238              
239             =over
240              
241             =item 'perl:Digest::MD5'
242              
243             Use the perl module L.
244              
245             =item 'perl:MD5'
246              
247             Use the (old) perl module L.
248              
249             =item 'cmd:md5'
250              
251             Use the OS command C (BSD systems).
252              
253             =item 'cmd:md5sum'
254              
255             Use the OS command C (Linux and Solaris systems).
256              
257             =item 'perl:Digest::Perl::MD5'
258              
259             Use the pure perl module L.
260              
261             =item 'cmd:cksum'
262              
263             Use the obsolete C command.
264              
265             =item 'stat:modtime'
266              
267             Just stat the file and use the modification time of the file.
268              
269             =back
270              
271             =item @exclude
272              
273             A list of files to be excluded. The check will be done against the
274             partial filename, beginning at the paths as in C<@directories>.
275              
276             =item %exclude
277              
278             Per-directory (as in C<@directories>) exclude list. For example, if
279              
280             @directories = ("/home/htdocs", "/home/htdocs/cgi-bin");
281              
282             is specified, then C<%exclude> may be
283              
284             %exclude = ("/home/htdocs" => ['.htaccess', 'cgi-bin/.*'],
285             "/home/htdocs/cgi-bin" => ['mails.*']);
286              
287             Note that it is generally problematic to have subdirs specified in
288             C<@directories> --- in such a case the C<%exclude> variable should be
289             set cleverly.
290              
291             =item $verbose
292              
293             Be verbose if set to a true value. The messages are printed to STDERR.
294             Note that some servers do not like output to STDERR --- it will get
295             mixed up with STDOUT output.
296              
297             =back
298              
299             =cut
300              
301 0 0 0       if (!defined $md5listcgi || $md5listcgi eq '') {
302 0           die "The CGI path to the md5list script is not defined";
303             }
304 0 0         if (!defined $topdirectory) {
305 0           die "The topdirectory is missing (config member WEsiteinfo->staging->stagingext->{topdirectory})";
306             }
307              
308 0 0         if ($v) {
309 0           print <
310             Using FTP Protocol.
311 0 0         FTP remote host: $livehost
312             FTP remote user: $liveuser
313             FTP remote directory: $livedirectory
314 0 0         @{[ @extracgi ? "FTP remote CGI directory: $livecgidirectory" : "" ]}
315             md5list CGI: $md5listcgi
316             topdirectory: $topdirectory
317             @{[ $dryrun ? "Do not execute any create/update/delete actions, just show them" : "" ]}
318             EOF
319 0 0         if ($deleteold) {
    0          
320 0           print "delete old files\n";
321             } elsif ($movetotrash) {
322 0           print "move old files to trash directory: $trashdirectory\n";
323             } else {
324 0           print "keep old files\n";
325             }
326             }
327              
328 0           my $ua = LWP::UserAgent->new;
329 0           my $request = HTTP::Request->new('GET', $md5listcgi);
330 0           my $res = $ua->request($request);
331             #my $res = $ua->get($md5listcgi);
332 0 0         if (!$res->is_success) {
333 0           print $res->error_as_HTML;
334 0           die "Can't get MD5 list from $md5listcgi";
335             }
336              
337 0           my %md5list;
338             my $curr_dir;
339 0           foreach my $line (split /\n/, $res->content) {
340 0 0         if ($line =~ /^\#\s*([^:]+):\s*(.*)/) {
341 0           my($key,$val) = ($1,$2);
342 0 0         if ($key =~ /^digest$/i) {
    0          
343 0 0         if ($val !~ /md5/i) {
344 0           die "Sorry, only MD5 digest are supported now";
345             }
346             } elsif ($key =~ /^directory$/i) {
347 0           $curr_dir = $val;
348             } else {
349             # ignore
350             }
351             } else {
352 0 0         if (!defined $curr_dir) {
353 0           die "Current directory is not defined ( $line )";
354             }
355 0           my($file, $md5) = split /\t/, $line;
356 0           $md5list{$curr_dir}->{$file} = $md5;
357             }
358             }
359              
360 0 0         if ($v) {
361 0           print "Got MD5 list from $md5listcgi:\n";
362 0           require Data::Dumper;
363 0           print Data::Dumper->Dumpxs([\%md5list],['md5list']), "\n";
364             }
365              
366 0 0         my $ftp = Net::FTP->new($livehost, Debug => 0) or die $@;
367 0 0         $ftp->login($liveuser, $livepassword) or die "Can't login with $liveuser";
368 0           $ftp->binary();
369 0 0 0       if (defined $livedirectory && $livedirectory ne '') {
370 0 0         $ftp->cwd($livedirectory) or die "Can't remote chdir to $livedirectory";
371 0 0         if ($dryrun) {
372 0           print "Execute chdir $livedirectory, now in directory: " . $ftp->pwd . "\n";
373             }
374             }
375              
376 0           my $pub_files = WE_Frontend::Publish::get_files_to_publish($self, %args);
377 0           my @directories = @{ $pub_files->{Directories} };
  0            
378 0           my @files = @{ $pub_files->{Files} };
  0            
379 0           my @published_files;
380              
381 0 0         my $remotedir = ($topdirectory ne "" ? "$topdirectory/" : "") . $livedirectory;
382 0           $remotedir =~ s|/+|/|g;
383              
384             # Ack! This will fetch all local files and directories, regardless
385             # whether it is new or old
386 0           my %args2 = %args;
387 0           delete $args2{-since}; # get really all!
388 0           $pub_files = WE_Frontend::Publish::get_files_to_publish($self, %args2);
389 0           my %local_files = map { ("$remotedir/$_" => 1) } @{ $pub_files->{Files} };
  0            
  0            
390              
391 0           my @files_to_delete;
392 0           foreach my $dir (keys %md5list) {
393 0           foreach my $file (keys %{$md5list{$dir}}) {
  0            
394 0 0         if (!exists $local_files{"$dir/$file"}) {
395 0           (my $remotefile = "$dir/$file") =~ s|^\Q$remotedir\E/?||;
396 0           push @files_to_delete, $remotefile;
397             }
398             }
399             }
400              
401             _save_pwd {
402 0 0   0     chdir $pubhtmldir || die $!;
403              
404             # XXX only create directories if really necessary!
405 0           foreach my $dir (@directories) {
406 0 0         if ($v) { print "Create folder $dir\n" }
  0            
407 0 0         if (!$dryrun) {
408 0           $ftp->mkdir($dir);
409             } else {
410 0           print "Execute mkdir $dir\n";
411             }
412             }
413              
414 0           foreach my $file (@files) {
415 0 0         if (!-r $file) { warn "The local file $pubhtmldir/$file is not readable" }
  0            
416              
417 0           my $message = "Create document $remotedir | $file\n";
418 0           my $copy = 1;
419 0 0         if (exists $md5list{$remotedir}->{$file}) {
420 0           my $md5 = Digest::MD5->new;
421 0 0         open(F, $file) or die "Can't read file $file: $!";
422 0           $md5->addfile(\*F);
423 0           close F;
424 0           my $local_md5 = $md5->hexdigest;
425 0 0         if ($local_md5 eq $md5list{$remotedir}->{$file}) {
426 0           $copy = 0;
427 0 0         if ($v) { print "skipping document $file\n" }
  0            
428             } else {
429 0           $message = "Update document $file\n";
430             }
431             }
432              
433 0 0         if ($copy) {
434 0 0         if ($v) { print $message }
  0            
435 0 0         if (!$dryrun) {
436 0 0         $ftp->put($file, $file) or warn "Can't put $pubhtmldir/$file to remote host $livehost";
437             } else {
438 0           print "Execute put $file to $file\n";
439             }
440 0           push @published_files, $file;
441             }
442             }
443              
444 0           };
445              
446             # see, which files are left to delete.
447 0           my @deleted_on_remote;
448             my @moved_to_trash_on_remote;
449 0 0 0       if ($deleteold) {
    0          
    0          
450 0           foreach my $file (@files_to_delete) {
451 0 0         if ($v) { print "deleting remote file $file\n"; };
  0            
452 0 0         if (!$dryrun) {
453 0 0         $ftp->delete($file) or warn "Can't delete $pubhtmldir/$file on remote host $livehost\n";
454             } else {
455 0           print "Execute delete $file\n";
456             }
457 0           push @deleted_on_remote, $file;
458             }
459             } elsif ($movetotrash) {
460 0           require File::Basename;
461 0           foreach my $file (@files_to_delete) {
462 0 0         if ($v) { print "move remote file $file to $trashdirectory\n"; };
  0            
463 0           my $basefile = File::Basename::basename($file);
464 0 0         if (!$dryrun) {
465 0 0         $ftp->rename($file, "$trashdirectory/$basefile") or warn "Can't rename $file to $trashdirectory/$basefile on remote host $livehost\n";
466             } else {
467 0           print "Execute rename $file to $trashdirectory/$basefile\n";
468             }
469 0           push @moved_to_trash_on_remote, $file;
470             }
471             } elsif (@files_to_delete && $v) {
472 0           print "The following files are outdated on the remote:\n",
473             join(", ", @files_to_delete), "\n";
474             }
475              
476 0           my $ret = {Directories => \@directories,
477             Files => \@files,
478             PublishedFiles => \@published_files,
479             DeletedOnRemote => \@deleted_on_remote,
480             MovedToTrashOnRemote => \@moved_to_trash_on_remote,
481             };
482 0           return $ret;
483             }
484              
485             1;
486              
487             __END__