File Coverage

lib/App/Followme/UploadSite.pm
Criterion Covered Total %
statement 135 184 73.3
branch 21 60 35.0
condition 3 8 37.5
subroutine 22 26 84.6
pod 2 16 12.5
total 183 294 62.2


line stmt bran cond sub pod time code
1             package App::Followme::UploadSite;
2              
3 1     1   656 use 5.008005;
  1         15  
4 1     1   8 use strict;
  1         3  
  1         26  
5 1     1   5 use warnings;
  1         1  
  1         44  
6              
7 1     1   6 use lib '../..';
  1         2  
  1         5  
8              
9 1     1   152 use base qw(App::Followme::Module);
  1         2  
  1         491  
10              
11 1     1   7 use File::Spec::Functions qw(abs2rel rel2abs splitdir catfile catdir);
  1         2  
  1         57  
12              
13 1     1   6 use App::Followme::FIO;
  1         4  
  1         84  
14 1     1   6 use App::Followme::Web;
  1         2  
  1         154  
15              
16             our $VERSION = "2.03";
17              
18 1     1   8 use constant SEED => 96;
  1         2  
  1         2172  
19              
20             #----------------------------------------------------------------------
21             # Read the default parameter values
22              
23             sub parameters {
24 8     8 1 16 my ($pkg) = @_;
25              
26             return (
27 8         42 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 873 my ($self) = @_;
120              
121              
122             my $hash_file = catfile($self->{top_directory},
123             $self->{state_directory},
124 1         8 $self->{hash_file});
125              
126 1 50       22 if (-e $hash_file) {
127 1         7 $self->{target_date} = fio_get_date($hash_file);
128             }
129              
130 1         3 my $hash = $self->read_hash_file($hash_file);
131 1         4 my %local = map {$_ => 1} keys %$hash;
  4         9  
132              
133 1         4 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 2 my ($self, $user, $pass) = @_;
164              
165 1         2 my $obstr = '';
166 1         3 my $seed = SEED;
167 1         3 my $str = "$user:$pass";
168              
169 1         3 for (my $i = 0; $i < length($str); $i += 1) {
170 15         21 my $val = ord(substr($str, $i, 1));
171 15         20 $seed = $val ^ $seed;
172 15         35 $obstr .= sprintf("%02x", $seed);
173             }
174              
175 1         3 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 9 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         25 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       18 die "Bad line in hash file: ($name)" unless defined $value;
192              
193 8         27 $hash{$name} = $value;
194             }
195             }
196              
197 2         9 return \%hash;
198             }
199              
200             #----------------------------------------------------------------------
201             # Read the user name and password from a file
202              
203             sub read_word {
204 1     1 0 7 my ($self, $filename) = @_;
205              
206 1   50     4 my $obstr = fio_read_page($filename) || die "Cannot read $filename\n";
207 1         4 chomp($obstr);
208              
209 1         3 my ($user, $pass) = $self->unobfuscate($obstr);
210 1         4 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 47 my ($self, $page) = @_;
218              
219             my $base_parser = sub {
220 12     12   27 my ($metadata, @tokens) = @_;
221 12         46 return "<base href=\"$self->{remote_url}\">";
222 12         59 };
223              
224 12         21 my $global = 0;
225 12         21 my $metadata = [];
226 12         37 my $new_page = web_substitute_tags('<base href="*">',
227             $page,
228             $base_parser,
229             $metadata,
230             $global
231             );
232              
233 12         73 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         3 $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         5 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         1 my $seed = SEED;
265              
266 1         4 for (my $i = 0; $i < length($obstr); $i += 2) {
267 15         25 my $val = hex(substr($obstr, $i, 2));
268 15         21 $str .= chr($val ^ $seed);
269 15         28 $seed = $val;
270             }
271              
272 1         5 return split(/:/, $str, 2);
273             }
274              
275             #----------------------------------------------------------------------
276             # Update an individual file
277              
278             sub update_file {
279 6     6 0 11 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       14 if ($self->{remote_url}) {
285              
286             # Check extension, skip if not a web file
287 6         18 my ($dir, $basename) = fio_split_filename($file);
288 6         36 my ($ext) = $basename =~ /\.([^\.]*)$/;
289 6 50       20 if ($ext eq $self->{web_extension}) {
290 6         30 my $page = fio_read_page($file);
291              
292 6 50       18 if ($page) {
293 6         19 $page = $self->rewrite_base_tag($page);
294 6         45 $local_file = rel2abs(catfile($self->{state_directory}, $basename));
295 6         189 fio_write_page($local_file, $page);
296             }
297             }
298             }
299              
300             # Upload the file and return the status of the upload
301              
302 6         15 my $status = 0;
303 6         23 my $remote_file = abs2rel($file, $self->{top_directory});
304 6 50       389 if ($self->{upload}->add_file($local_file, $remote_file)) {
305 6         10 $status = 1;
306              
307             } else {
308 0 0       0 die "Too many upload errors\n" if $self->{max_errors} == 0;
309 0         0 $self->{max_errors} --;
310             }
311              
312             # Remove any temporary file
313 6 50       327 unlink($local_file) if $file ne $local_file;
314 6         38 return $status;
315             }
316              
317             #----------------------------------------------------------------------
318             # Update files in one folder
319              
320             sub update_folder {
321 2     2 0 776 my ($self, $folder, $hash, $local) = @_;
322              
323 2         18 my $index_file = $self->to_file($folder);
324              
325             # Check if folder is new
326              
327 2 100       9 if ($folder ne $self->{top_directory}) {
328 1         15 $folder = abs2rel($folder, $self->{top_directory});
329 1 50       62 delete $local->{$folder} if exists $local->{$folder};
330              
331 1 50 33     5 if (! exists $hash->{$folder} ||
332             $hash->{$folder} ne 'dir') {
333              
334 1 50       6 if ($self->{upload}->add_directory($folder)) {
335 1         11 $hash->{$folder} = 'dir';
336 1 50       7 print "add $folder\n" if $self->{verbose};
337              
338             } else {
339 0 0       0 die "Too many upload errors\n" if $self->{max_errors} == 0;
340 0         0 $self->{max_errors} --;
341             }
342             }
343             }
344              
345             # Check each of the files in the directory
346              
347 2         10 my $files = $self->{data}->build('files', $index_file);
348              
349 2         5 foreach my $filename (@$files) {
350             # Skip check if in quick mode and modification date is old
351              
352 6 50       15 if ($self->{quick_update}) {
353 0 0       0 next if $self->{target_date} > fio_get_date($filename);
354             }
355              
356 6         22 my $file = abs2rel($filename, $self->{top_directory});
357 6 50       414 delete $local->{$file} if exists $local->{$file};
358              
359 6         9 my $value = ${$self->{data}->build('checksum', $filename)};
  6         24  
360              
361             # Add file if new or changed
362              
363 6 50 33     23 if (! exists $hash->{$file} || $hash->{$file} ne $value) {
364 6 50       21 if ($self->update_file($filename)) {
365 6         23 $hash->{$file} = $value;
366 6 50       23 print "add $file\n" if $self->{verbose};
367             }
368             }
369             }
370              
371             # Recursively check each of the subdirectories
372              
373 2         13 my $folders = $self->{data}->build('folders', $folder);
374 2         5 foreach my $subfolder (@$folders) {
375 1         8 $self->update_folder($subfolder, $hash, $local);
376             }
377              
378 2         8 return;
379             }
380              
381             #----------------------------------------------------------------------
382             # Write the hash back to a file
383              
384             sub write_hash_file {
385 1     1 0 2283 my ($self, $hash) = @_;
386              
387 1         2 my @hash_list;
388 1         6 while (my ($name, $value) = each(%$hash)) {
389 4         16 push(@hash_list, "$name\t$value\n");
390             }
391              
392             my $filename = catfile($self->{top_directory},
393             $self->{state_directory},
394 1         5 $self->{hash_file});
395              
396 1         5 fio_write_page($filename, join('', @hash_list));
397              
398 1         4 return;
399             }
400              
401             #----------------------------------------------------------------------
402             # WRITE_WORD -- Write the secret word to a file
403              
404             sub write_word {
405 1     1 0 23 my ($self, $filename, $user, $pass) = @_;
406              
407 1         12 my $obstr = $self->obfuscate ($user, $pass);
408 1         12 fio_write_page($filename, "$obstr\n");
409 1         26 chmod (0600, $filename);
410              
411 1         5 return;
412             }
413              
414             1;
415             __END__
416              
417             =encoding utf-8
418              
419             =head1 NAME
420              
421             App::Followme::UploadSite - Upload changed and new files
422              
423             =head1 SYNOPSIS
424              
425             my $app = App::Followme::UploadSite->new(\%configuration);
426             $app->run($folder);
427              
428             =head1 DESCRIPTION
429              
430             This module uploads changed files to a remote site. The default method to do the
431             uploads is ftp, but that can be changed by changing the parameter upload_pkg.
432             This package computes a checksum for every file in the site. If the checksum has
433             changed since the last time it was run, the file is uploaded to the remote site.
434             If there is a checksum, but no local file, the file is deleted from the remote
435             site. If this module is run in quick mode, only files whose modification date is
436             later then the last time it was run are checked.
437              
438             =head1 CONFIGURATION
439              
440             The following fields in the configuration file are used:
441              
442             =over 4
443              
444             =item credentials
445              
446             The name of the file which holds the user name and password for the remote site
447             in obfuscated form. Te default name is 'upload.cred'.
448              
449             =item hash_file
450              
451             The name of the file containing all the checksums for files on the site. The
452             default name is 'upload.hash'.
453              
454             =item max_errors
455              
456             The number of upload errors the module tolerate before quitting. The default
457             value is 5.
458              
459             =item remote_url
460              
461             The url of the remote website, e.g. http://www.cloudhost.com.
462              
463             =item state_directory
464              
465             The name of the directory containing the credentials and hash file. This
466             directory name is relative to the top directory of the site. The default
467             name is '_state'.
468              
469             =item upload_pkg
470              
471             The name of the package with methods that add and delete files on the remote
472             site. The default is L<App::Followme::UploadFtp>. Other packages can be
473             written, the methods a package must support can be found in
474             L<App::Followme::UploadNone>.
475              
476             =item verbose
477              
478             Print names of uploaded files when not in quick mode
479              
480             =back
481              
482             =head1 LICENSE
483              
484             Copyright (C) Bernie Simon.
485              
486             This library is free software; you can redistribute it and/or modify
487             it under the same terms as Perl itself.
488              
489             =head1 AUTHOR
490              
491             Bernie Simon E<lt>bernie.simon@gmail.comE<gt>
492              
493             =cut