File Coverage

lib/App/Followme/UploadSite.pm
Criterion Covered Total %
statement 134 183 73.2
branch 21 60 35.0
condition 3 8 37.5
subroutine 22 26 84.6
pod 2 16 12.5
total 182 293 62.1


line stmt bran cond sub pod time code
1             package App::Followme::UploadSite;
2              
3 1     1   671 use 5.008005;
  1         14  
4 1     1   6 use strict;
  1         2  
  1         22  
5 1     1   5 use warnings;
  1         2  
  1         42  
6              
7 1     1   8 use lib '../..';
  1         2  
  1         14  
8              
9 1     1   145 use base qw(App::Followme::Module);
  1         2  
  1         537  
10              
11 1     1   7 use File::Spec::Functions qw(abs2rel rel2abs splitdir catfile catdir);
  1         2  
  1         66  
12              
13 1     1   6 use App::Followme::FIO;
  1         2  
  1         105  
14 1     1   8 use App::Followme::Web;
  1         2  
  1         92  
15              
16             our $VERSION = "2.02";
17              
18 1     1   7 use constant SEED => 96;
  1         2  
  1         2292  
19              
20             #----------------------------------------------------------------------
21             # Read the default parameter values
22              
23             sub parameters {
24 8     8 1 16 my ($pkg) = @_;
25              
26             return (
27 8         46 verbose => 0,
28             max_errors => 5,
29             remote_url => '',
30             hash_file => 'upload.hash',
31             credentials => 'upload.cred',
32             state_directory => '_state',
33             data_pkg => 'App::Followme::UploadData',
34             upload_pkg => 'App::Followme::UploadFtp',
35             );
36             }
37              
38             #----------------------------------------------------------------------
39             # Upload changed files in a directory tree
40              
41             sub run {
42 0     0 0 0 my ($self, $folder) = @_;
43              
44 0         0 my ($hash, $local) = $self->get_state();
45              
46 0         0 my ($user, $pass) = $self->get_word();
47 0         0 $self->{upload}->open($user, $pass);
48              
49 0         0 eval {
50             chdir($self->{top_directory})
51 0 0       0 or die "Can't cd to $self->{top_directory}";
52              
53 0         0 $self->update_folder($self->{top_directory}, $hash, $local);
54 0         0 $self->clean_files($hash, $local);
55 0         0 $self->{upload}->close();
56              
57 0         0 chdir($folder);
58             };
59              
60 0         0 my $error = $@;
61 0         0 $self->write_hash_file($hash);
62              
63 0 0       0 die $error if $error;
64 0         0 return;
65             }
66              
67             #----------------------------------------------------------------------
68             # ASK_WORD -- Ask for user name and password if file not found
69              
70             sub ask_word {
71 0     0 0 0 my ($self) = @_;
72              
73 0         0 print "\nUser name: ";
74 0         0 my $user = <STDIN>;
75 0         0 chomp ($user);
76              
77 0         0 print "Password: ";
78 0         0 my $pass = <STDIN>;
79 0         0 chomp ($pass);
80              
81 0         0 return ($user, $pass);
82             }
83              
84             #----------------------------------------------------------------------
85             # Delete files on remote site when they are no longer on local site
86              
87             sub clean_files {
88 0     0 0 0 my ($self, $hash, $local) = @_;
89              
90             # Sort files so that files in directories are deleted before
91             # their directories are
92 0         0 my @filenames = sort {length($b) <=> length($a)} keys(%$local);
  0         0  
93              
94 0         0 foreach my $filename (@filenames) {
95 0         0 my $flag;
96 0 0       0 if ($hash->{$filename} eq 'dir') {
97 0         0 $flag = $self->{upload}->delete_directory($filename);
98             } else {
99 0         0 $flag = $self->{upload}->delete_file($filename);
100             }
101              
102 0 0       0 if ($flag) {
103 0         0 delete $hash->{$filename};
104 0 0       0 print "delete $filename\n" if $self->{verbose};
105              
106             } else {
107 0 0       0 die "Too many upload errors\n" if $self->{max_errors} == 0;
108 0         0 $self->{max_errors} --;
109             }
110             }
111              
112 0         0 return;
113             }
114              
115             #----------------------------------------------------------------------
116             # Get the state of the site, contained in the hash file
117              
118             sub get_state {
119 1     1 0 678 my ($self) = @_;
120              
121              
122             my $hash_file = catfile($self->{top_directory},
123             $self->{state_directory},
124 1         7 $self->{hash_file});
125              
126 1 50       24 if (-e $hash_file) {
127 1         6 $self->{target_date} = fio_get_date($hash_file);
128             }
129              
130 1         5 my $hash = $self->read_hash_file($hash_file);
131 1         4 my %local = map {$_ => 1} keys %$hash;
  4         9  
132              
133 1         5 return ($hash, \%local);
134             }
135              
136             #----------------------------------------------------------------------
137             # GET_WORD -- Say the secret word, the duck comes down and you win $100
138              
139             sub get_word {
140 0     0 0 0 my ($self) = @_;
141              
142             my $filename = catfile(
143             $self->{top_directory},
144             $self->{state_directory},
145             $self->{credentials}
146 0         0 );
147              
148 0         0 my ($user, $pass);
149 0 0       0 if (-e $filename) {
150 0         0 ($user, $pass) = $self->read_word($filename);
151             } else {
152 0         0 ($user, $pass) = $self->ask_word();
153 0         0 $self->write_word($filename, $user, $pass);
154             }
155              
156 0         0 return ($user, $pass);
157             }
158              
159             #----------------------------------------------------------------------
160             # Add obfuscation to string
161              
162             sub obfuscate {
163 1     1 0 4 my ($self, $user, $pass) = @_;
164              
165 1         2 my $obstr = '';
166 1         1 my $seed = SEED;
167 1         3 my $str = "$user:$pass";
168              
169 1         4 for (my $i = 0; $i < length($str); $i += 1) {
170 15         26 my $val = ord(substr($str, $i, 1));
171 15         20 $seed = $val ^ $seed;
172 15         34 $obstr .= sprintf("%02x", $seed);
173             }
174              
175 1         2 return $obstr;
176             }
177              
178             #----------------------------------------------------------------------
179             # Read the hash for each file on the site from a file
180              
181             sub read_hash_file {
182 2     2 0 10 my ($self, $filename) = @_;
183              
184 2         4 my %hash;
185 2         7 my $page = fio_read_page($filename);
186              
187 2 50       8 if ($page) {
188 2         10 my @lines = split(/\n/, $page);
189 2         6 foreach my $line (@lines) {
190 8         21 my ($name, $value) = split (/\t/, $line, 2);
191 8 50       15 die "Bad line in hash file: ($name)" unless defined $value;
192              
193 8         20 $hash{$name} = $value;
194             }
195             }
196              
197 2         7 return \%hash;
198             }
199              
200             #----------------------------------------------------------------------
201             # Read the user name and password from a file
202              
203             sub read_word {
204 1     1 0 10 my ($self, $filename) = @_;
205              
206 1   50     5 my $obstr = fio_read_page($filename) || die "Cannot read $filename\n";
207 1         4 chomp($obstr);
208              
209 1         5 my ($user, $pass) = $self->unobfuscate($obstr);
210 1         5 return ($user, $pass);
211             }
212              
213             #----------------------------------------------------------------------
214             # Rewrite the base tag of an html page
215              
216             sub rewrite_base_tag {
217 12     12 0 44 my ($self, $page) = @_;
218              
219             my $base_parser = sub {
220 12     12   77 my ($metadata, @tokens) = @_;
221 12         53 return "<base href=\"$self->{remote_url}\">";
222 12         115 };
223              
224 12         24 my $global = 0;
225 12         21 my $metadata = [];
226 12         40 my $new_page = web_substitute_tags('<base href="*">',
227             $page,
228             $base_parser,
229             $metadata,
230             $global
231             );
232              
233 12         72 return $new_page;
234             }
235              
236             #----------------------------------------------------------------------
237             # Initialize the configuration parameters
238              
239             sub setup {
240 2     2 1 5 my ($self) = @_;
241              
242             # Turn off messages when in quick mode
243 2 50       6 $self->{verbose} = 0 if $self->{quick_mode};
244              
245             # The target date is the date of the hash file, used in quick mode
246             # to select which files to test
247 2         4 $self->{target_date} = 0;
248              
249             # Remove any trailing slash from url
250 2 50       5 if ($self->{remote_url}) {
251 2         4 $self->{remote_url} =~ s/\/$//;
252             }
253              
254 2         4 return;
255             }
256              
257             #----------------------------------------------------------------------
258             # Remove obfuscation from string
259              
260             sub unobfuscate {
261 1     1 0 3 my ($self, $obstr) = @_;
262              
263 1         2 my $str = '';
264 1         2 my $seed = SEED;
265              
266 1         5 for (my $i = 0; $i < length($obstr); $i += 2) {
267 15         24 my $val = hex(substr($obstr, $i, 2));
268 15         24 $str .= chr($val ^ $seed);
269 15         26 $seed = $val;
270             }
271              
272 1         6 return split(/:/, $str, 2);
273             }
274              
275             #----------------------------------------------------------------------
276             # Update an individual file
277              
278             sub update_file {
279 6     6 0 13 my ($self, $file, $hash) = @_;
280              
281 6         11 my $local_file = $file;
282              
283             # If there is a remote url, rewrite it into a new file
284 6 50       16 if ($self->{remote_url}) {
285              
286             # Check extension, skip if not a web file
287 6         16 my ($dir, $basename) = fio_split_filename($file);
288 6         38 my ($ext) = $basename =~ /\.([^\.]*)$/;
289 6 50       22 next if $ext ne $self->{web_extension};
290              
291 6         15 my $page = fio_read_page($file);
292 6 50       32 my $new_page = $self->rewrite_base_tag($page) if $page;
293              
294 6         39 $local_file = rel2abs(catfile($self->{state_directory}, $basename));
295 6         193 fio_write_page($local_file, $new_page);
296             }
297              
298             # Upload the file and return the status of the upload
299              
300 6         14 my $status = 0;
301 6         26 my $remote_file = abs2rel($file, $self->{top_directory});
302 6 50       400 if ($self->{upload}->add_file($local_file, $remote_file)) {
303 6         10 $status = 1;
304              
305             } else {
306 0 0       0 die "Too many upload errors\n" if $self->{max_errors} == 0;
307 0         0 $self->{max_errors} --;
308             }
309              
310             # Remove any temporary file
311 6 50       291 unlink($local_file) if $file ne $local_file;
312 6         39 return $status;
313             }
314              
315             #----------------------------------------------------------------------
316             # Update files in one folder
317              
318             sub update_folder {
319 2     2 0 652 my ($self, $folder, $hash, $local) = @_;
320              
321 2         18 my $index_file = $self->to_file($folder);
322              
323             # Check if folder is new
324              
325 2 100       9 if ($folder ne $self->{top_directory}) {
326 1         6 $folder = abs2rel($folder, $self->{top_directory});
327 1 50       74 delete $local->{$folder} if exists $local->{$folder};
328              
329 1 50 33     5 if (! exists $hash->{$folder} ||
330             $hash->{$folder} ne 'dir') {
331              
332 1 50       7 if ($self->{upload}->add_directory($folder)) {
333 1         4 $hash->{$folder} = 'dir';
334 1 50       4 print "add $folder\n" if $self->{verbose};
335              
336             } else {
337 0 0       0 die "Too many upload errors\n" if $self->{max_errors} == 0;
338 0         0 $self->{max_errors} --;
339             }
340             }
341             }
342              
343             # Check each of the files in the directory
344              
345 2         10 my $files = $self->{data}->build('files', $index_file);
346              
347 2         10 foreach my $filename (@$files) {
348             # Skip check if in quick mode and modification date is old
349              
350 6 50       17 if ($self->{quick_update}) {
351 0 0       0 next if $self->{target_date} > fio_get_date($filename);
352             }
353              
354 6         23 my $file = abs2rel($filename, $self->{top_directory});
355 6 50       417 delete $local->{$file} if exists $local->{$file};
356              
357 6         60 my $value = ${$self->{data}->build('checksum', $filename)};
  6         26  
358              
359             # Add file if new or changed
360              
361 6 50 33     22 if (! exists $hash->{$file} || $hash->{$file} ne $value) {
362 6 50       20 if ($self->update_file($filename)) {
363 6         21 $hash->{$file} = $value;
364 6 50       27 print "add $file\n" if $self->{verbose};
365             }
366             }
367             }
368              
369             # Recursively check each of the subdirectories
370              
371 2         14 my $folders = $self->{data}->build('folders', $folder);
372 2         7 foreach my $subfolder (@$folders) {
373 1         20 $self->update_folder($subfolder, $hash, $local);
374             }
375              
376 2         7 return;
377             }
378              
379             #----------------------------------------------------------------------
380             # Write the hash back to a file
381              
382             sub write_hash_file {
383 1     1 0 1681 my ($self, $hash) = @_;
384              
385 1         3 my @hash_list;
386 1         5 while (my ($name, $value) = each(%$hash)) {
387 4         17 push(@hash_list, "$name\t$value\n");
388             }
389              
390             my $filename = catfile($self->{top_directory},
391             $self->{state_directory},
392 1         5 $self->{hash_file});
393              
394 1         6 fio_write_page($filename, join('', @hash_list));
395              
396 1         4 return;
397             }
398              
399             #----------------------------------------------------------------------
400             # WRITE_WORD -- Write the secret word to a file
401              
402             sub write_word {
403 1     1 0 24 my ($self, $filename, $user, $pass) = @_;
404              
405 1         3 my $obstr = $self->obfuscate ($user, $pass);
406 1         7 fio_write_page($filename, "$obstr\n");
407 1         24 chmod (0600, $filename);
408              
409 1         5 return;
410             }
411              
412             1;
413             __END__
414              
415             =encoding utf-8
416              
417             =head1 NAME
418              
419             App::Followme::UploadSite - Upload changed and new files
420              
421             =head1 SYNOPSIS
422              
423             my $app = App::Followme::UploadSite->new(\%configuration);
424             $app->run($folder);
425              
426             =head1 DESCRIPTION
427              
428             This module uploads changed files to a remote site. The default method to do the
429             uploads is ftp, but that can be changed by changing the parameter upload_pkg.
430             This package computes a checksum for every file in the site. If the checksum has
431             changed since the last time it was run, the file is uploaded to the remote site.
432             If there is a checksum, but no local file, the file is deleted from the remote
433             site. If this module is run in quick mode, only files whose modification date is
434             later then the last time it was run are checked.
435              
436             =head1 CONFIGURATION
437              
438             The following fields in the configuration file are used:
439              
440             =over 4
441              
442             =item credentials
443              
444             The name of the file which holds the user name and password for the remote site
445             in obfuscated form. Te default name is 'upload.cred'.
446              
447             =item hash_file
448              
449             The name of the file containing all the checksums for files on the site. The
450             default name is 'upload.hash'.
451              
452             =item max_errors
453              
454             The number of upload errors the module tolerate before quitting. The default
455             value is 5.
456              
457             =item remote_url
458              
459             The url of the remote website, e.g. http://www.cloudhost.com.
460              
461             =item state_directory
462              
463             The name of the directory containing the credentials and hash file. This
464             directory name is relative to the top directory of the site. The default
465             name is '_state'.
466              
467             =item upload_pkg
468              
469             The name of the package with methods that add and delete files on the remote
470             site. The default is L<App::Followme::UploadFtp>. Other packages can be
471             written, the methods a package must support can be found in
472             L<App::Followme::UploadNone>.
473              
474             =item verbose
475              
476             Print names of uploaded files when not in quick mode
477              
478             =back
479              
480             =head1 LICENSE
481              
482             Copyright (C) Bernie Simon.
483              
484             This library is free software; you can redistribute it and/or modify
485             it under the same terms as Perl itself.
486              
487             =head1 AUTHOR
488              
489             Bernie Simon E<lt>bernie.simon@gmail.comE<gt>
490              
491             =cut