File Coverage

blib/lib/File/Catalog.pm
Criterion Covered Total %
statement 41 89 46.0
branch 0 34 0.0
condition 0 6 0.0
subroutine 14 17 82.3
pod n/a
total 55 146 37.6


line stmt bran cond sub pod time code
1             package File::Catalog;
2              
3 1     1   13307 use 5.010;
  1         3  
4 1     1   4 use strict;
  1         1  
  1         20  
5 1     1   4 use warnings FATAL => 'all';
  1         7  
  1         34  
6 1     1   784 use Env;
  1         1859  
  1         3  
7 1     1   340 use Cwd qw(abs_path);
  1         2  
  1         37  
8 1     1   5 use File::Basename qw(dirname basename);
  1         2  
  1         48  
9 1     1   4 use Fcntl ':mode';
  1         0  
  1         217  
10 1     1   399 use Digest::MD5::File qw(file_md5_hex);
  1         43680  
  1         6  
11 1     1   782 use Log::Log4perl qw(:easy);
  1         34000  
  1         4  
12 1     1   811 use Term::ProgressBar;
  1         49535  
  1         37  
13 1     1   510 use UI::Dialog;
  1         1697  
  1         29  
14 1     1   493 use Data::Dumper qw(Dumper);
  1         4501  
  1         58  
15              
16 1     1   342 use File::Catalog::Env qw(nom_local);
  1         2  
  1         41  
17 1     1   321 use File::Catalog::DB;
  1         3  
  1         674  
18              
19             =head1 NAME
20              
21             File::Catalog - The great new File::Catalog!
22              
23             =head1 VERSION
24              
25             Version 0.003
26              
27             =cut
28              
29             our $VERSION = '0.003';
30              
31             # timeout pour operations systeme
32             #my $timeout_court = 30; # secondes
33             #my $timeout_long = 300; # secondes
34              
35             # repertoires ignores
36             my @exclusions_rep = qw(.git .svn CVS _no_ktl RECYCLER $RECYCLE.BIN);
37             my @exclusions_fic = qw(Thumbs.db sync.ffs_db);
38              
39             =head1 SYNOPSIS
40              
41             Quick summary of what the module does.
42              
43             Perhaps a little code snippet.
44              
45             use File::Catalog;
46              
47             my $foo = File::Catalog->new();
48             ...
49              
50             =head1 EXPORT
51              
52             A list of functions that can be exported. You can delete this section
53             if you don't export anything, such as for a purely object-oriented module.
54              
55             =head1 SUBROUTINES/METHODS
56              
57             =head2 initialiser
58              
59             =cut
60              
61             sub initialiser {
62 0     0     my ($class, %options) = @_;
63 0           my $self = {};
64 0           bless $self, $class;
65 0           $self->{i} = 0;
66 0           $self->{moi} = basename($0);
67              
68             # init log
69 0 0         my $opt_debug = (exists $options{debug}) ? $options{debug} : undef;
70 0 0         Log::Log4perl->easy_init(($opt_debug) ? $DEBUG : $INFO);
71 0           DEBUG "debug: ON";
72              
73             # taille max en kio pour calcul md5
74             # (juste plus grand qu'une photo)
75 0 0         $self->{no_md5} = (exists $options{no_md5}) ? $options{no_md5} : 0;
76             $self->{taille_limite_md5} =
77             (exists $options{taille_limite_md5})
78             ? $options{taille_limite_md5}
79 0 0         : 1024 * 10;
80              
81             # rep tmp
82             $self->{tmp} =
83             (exists $options{tmp} and defined $options{tmp})
84             ? $options{tmp}
85 0 0 0       : File::Catalog::Env::tmp();
86 0           DEBUG "tmp: $self->{tmp}";
87              
88             # archives
89 0 0         $self->{archives} = (exists $options{archives}) ? $options{archives} : 0;
90 0           DEBUG "archives: $self->{archives}";
91 0 0         if ($self->{archives}) {
92              
93             # saisie du mot de passe si besoin
94             my $opt_pwd =
95 0 0         (exists $options{password}) ? $options{password} : undef;
96 0 0 0       if (defined($opt_pwd) and !$opt_pwd) {
97             my $d = new UI::Dialog(
98             backtitle => $self->{moi},
99 0           title => 'Mot de passe'
100             );
101 0           $opt_pwd = $d->password(text => 'Tu peux taper ton mot de passe. Je ne regarde pas...');
102             }
103 0           $self->{password} = $opt_pwd;
104 0 0         DEBUG "password: $self->{password}" if $opt_pwd;
105              
106             # 7zip
107 0           $self->{exe7z} = File::Catalog::Env::exe7z();
108 0 0         $self->{opt7z} = (defined $opt_pwd) ? "-p$opt_pwd" : "";
109 0 0         DEBUG "exe7z: $self->{exe7z}" if $opt_pwd;
110             }
111              
112             # bd par defaut
113             #my $serveur = File::Catalog::Env::serveur();
114 0 0         my $fic_bd = (exists $options{bd}) ? $options{bd} : undef;
115 0 0         $fic_bd = ".ktl.sqlite" unless defined $fic_bd;
116 0           $self->{fic_bd} = $fic_bd;
117 0           DEBUG "fic_bd: $self->{fic_bd}";
118             $self->{db} =
119 0           File::Catalog::DB->connect($self->{fic_bd}, $options{extension});
120              
121             # contexte archive
122 0           $self->{ctx_arch} = 0;
123              
124             # retour
125 0           return $self;
126             }
127              
128             =head2 terminer
129              
130             =cut
131              
132             # fermeture
133             sub terminer {
134 0     0     my ($self) = @_;
135              
136 0           $self->{db}->disconnect();
137             }
138              
139             =head2 parcourir_repertoire
140              
141             =cut
142              
143             # parcours
144             sub parcourir_repertoire {
145 0     0     my ($self, $repertoire, $volume) = @_;
146 0           my $err = 0;
147              
148             # volume courant
149 0 0         if (!defined $volume) {
150 0           chomp($volume = `df -P $repertoire | tail -n 1`);
151 0           $volume =~ s|.+%\s+||;
152              
153             # (re)initialisation de la liste des fichiers catalogues
154 0           $self->{repficcat} = [];
155             }
156 0           DEBUG "$volume: $repertoire";
157              
158             # listage repertoire
159 0 0         if (opendir my $fh_rep, $repertoire) {
160 0           my @lst_fic = grep { !/^\.\.?$/ } readdir $fh_rep;
  0            
161 0 0         closedir $fh_rep or die "Pb fermeture repertoire $repertoire !\n";
162              
163             # analyse
164 0           foreach my $entree (@lst_fic) {
165 0           $err = $self->analyser_entree("$repertoire/$entree", $volume);
166             }
167             }
168             else {
169 0           WARN "Echec ouverture repertoire $repertoire !\n";
170             }
171              
172 0           return $err;
173             }
174              
175             =head2 analyser_entree
176              
177             =cut
178              
179             # analyse
180             sub analyser_entree {
181             my ($self, $entree, $volume, $force_archive) = @_;
182             my $rep_tmp = $self->{tmp};
183             (my $repfic = $entree) =~ s|^$rep_tmp/||;
184             my $rep = dirname $repfic;
185             my $fic = basename $repfic;
186             return 0 if $fic ~~ @exclusions_fic;
187             my $archive = ($fic =~ /\.(gz|tar|tgz|zip|bz2|tbz2|7z)$/);
188             my @stat = lstat $entree;
189             my $inode = $stat[1];
190             my $volinode = "$volume/$inode";
191             my $rows;
192             my $err = 0;
193              
194             #--- table Fichier
195             $rows =
196             $self->{db}->execute('lister_volinodes', $repfic)->fetchall_arrayref();
197             if (my $row = shift @$rows) {
198              
199             # si maj volinode
200             if ($row->[0] ne $volinode) {
201              
202             # maj table Fichier
203             $self->{db}->execute('updateF', $volinode, $repfic);
204             push @{ $self->{repficcat} }, $repfic;
205             }
206             else {
207             DEBUG "$repfic deja vu";
208             }
209             }
210             else {
211              
212             # insertion table Fichier
213             DEBUG "insertF: " . $repfic;
214             my @autres_infos = $self->{db}->{trigger}->($fic);
215             my $ok_trigger = shift @autres_infos;
216             if ($ok_trigger) {
217             $self->{db}->execute('insertF', $repfic, $rep, $fic, $volinode, @autres_infos);
218             push @{ $self->{repficcat} }, $repfic;
219             }
220             else {
221             $volinode = undef;
222             }
223             }
224              
225             #--- table Inode
226             my $md5;
227             my $majmd5 = 0;
228             if ($volinode) {
229             $rows = $self->{db}->execute('lire_volinode', $volinode)->fetchall_arrayref();
230             if (my $row = shift @$rows) {
231              
232             # si maj infos
233             if (
234             $row->[3] != $stat[2] # mode
235             or $row->[4] != $stat[7] # taille
236              
237             #or $row->[5] != $stat[8] # acces
238             or $row->[6] != $stat[9] # modif
239             or $row->[7] != $stat[10] # creation
240             )
241             {
242             my $old_md5 = $row->[8];
243              
244             # maj table Inode
245             $md5 = $self->md5sum($entree, \@stat) unless $self->{no_md5};
246             $self->{db}
247             ->execute("updateI", $stat[2], $stat[7], $stat[8], $stat[9], $stat[10], $archive, $md5, $volinode);
248             $majmd5 = (defined $md5 and ($old_md5 ne $md5));
249             DEBUG "majmd5: " . $majmd5;
250             }
251             else {
252             DEBUG "inode $inode deja vu";
253             }
254             }
255             else {
256              
257             # insertion table Inode
258             $md5 = $self->md5sum($entree, \@stat) unless $self->{no_md5};
259             DEBUG "insertI: " . $volinode;
260             $self->{db}->execute(
261             'insertI', $volinode, $volume, $inode, $stat[2], $stat[7],
262             $stat[8], $stat[9], $stat[10], $archive, $md5
263             );
264             $majmd5 = 1;
265             }
266             }
267              
268             #--- progression
269             if ( S_ISREG($stat[2])
270             and defined($self->{barre})
271             and !$self->{ctx_arch})
272             {
273             $self->{barre}->update(++$self->{nbfic});
274             }
275              
276             #--- archive
277             if ($md5 and ($self->{archives} or $force_archive) and $archive) {
278              
279             DEBUG "archive: " . $md5;
280              
281             # nb occurrences md5sum
282             my $count = $self->{db}->nb_occurrences_md5sum($md5);
283             DEBUG "count = $count";
284             if ($count > 1 or !$majmd5) {
285             DEBUG "archive $fic deja analysee";
286             }
287             else {
288              
289             #INFO "analyse archive $fic";
290             #DEBUG "timeout: $timeout_court";
291              
292             #ZZZ essayer de trouver la taille necessaire
293             #ZZZ pour eviter de saturer l'espace temporaire
294             #my $err = timeout $timeout_court => sub {
295             my $entree7z = nom_local($entree);
296             my $cmd = "$self->{exe7z} $self->{opt7z} t \"$entree7z\" > $rep_tmp/$md5.log 2>&1";
297             DEBUG $cmd;
298             $err = system($cmd) >> 8;
299              
300             #};
301             #if ($@) {
302             #WARN "[PB] test timed-out : $entree";
303             #$err = 5;
304             #}
305             if ($err) {
306             WARN "[PB] archive non reconnue ($err) : $entree";
307             my @log = `cat $rep_tmp/$md5.log`;
308             foreach (@log) {
309             ERROR $_;
310             }
311             }
312             else {
313             $err = $self->analyser_archive($entree, $md5);
314             }
315             }
316             }
317              
318             # fin si repertoire exclu
319             return $err if $fic ~~ @exclusions_rep;
320              
321             # repertoire
322             $err = $self->parcourir_repertoire($entree, $volume) if S_ISDIR $stat[2];
323              
324             return $err;
325             }
326              
327             =head2 lister_resultat
328              
329             =cut
330              
331             # liste des fichiers catalogues
332             # par la commande precedente
333             sub lister_resultat {
334             my ($self) = @_;
335              
336             return $self->{repficcat};
337             }
338              
339             =head2 analyser_archive
340              
341             =cut
342              
343             sub analyser_archive {
344             my ($self, $archive, $md5) = @_;
345             my $err = 0;
346              
347             # debut contexte archive
348             $self->{ctx_arch}++;
349              
350             # md5
351             if (!$md5) {
352             my @stat = lstat $archive;
353             $md5 = $self->md5sum($archive, \@stat);
354             }
355              
356             # tar, zip, bz2, 7z : 7za x -o{Directory} + rm -rf
357             # tgz, tar.gz : gunzip -c + 7za x -o{Directory} + rm -rf
358             # tbz2, tar.bz2 : bunzip2 -c + 7za x -o{Directory} + rm -rf
359             my $archive7z = nom_local($archive);
360             my $rep_tmp = $self->{tmp};
361             my $reptmp = "$rep_tmp/$md5";
362             my $reptmp7z = nom_local($reptmp);
363              
364             #INFO "analyse sous $reptmp";
365              
366             # extraction
367             if ($archive =~ /\.(tar\.gz|tgz|tar\.bz2|tbz2)$/) {
368              
369             # cas d'une archive (b|g)zippee
370             my $dec = ($archive =~ /\.t?gz$/) ? "gunzip" : "bunzip2";
371             my $cmd = "$dec -c \"$archive\" > $reptmp.tar 2>$reptmp.log";
372             DEBUG $cmd;
373             $err = system($cmd) >> 8;
374             if ($err) {
375             ERROR "[PB] gunzip ($err) : $archive";
376             }
377             else {
378              
379             # extraction avec tar
380             mkdir $reptmp;
381             my $cmd = "tar xf $reptmp7z.tar -C $reptmp7z > $reptmp.log 2>&1";
382             DEBUG $cmd;
383             $err = system($cmd) >> 8;
384             }
385             }
386             else {
387              
388             # extraction avec 7zip
389             my $cmd = "$self->{exe7z} $self->{opt7z} x -y -o\"$reptmp7z\" \"$archive7z\" > $reptmp.log 2>&1";
390             DEBUG $cmd;
391             $err = system($cmd) >> 8;
392             }
393              
394             if (!-d $reptmp) {
395              
396             # cas reptmp inexistant
397             ERROR "[PB] repertoire $reptmp inexistant ($archive) !";
398             }
399             if ($err or !-d $reptmp) {
400              
401             # affichage du log en cas d'erreur
402             ERROR "[PB] extraction archive ($err) : $archive";
403             my @log = `cat $reptmp.log`;
404             foreach (@log) {
405             ERROR $_;
406             }
407             }
408             else {
409              
410             # parcours de l'arborescence temporaire
411             $self->parcourir_repertoire($reptmp, $md5);
412             }
413              
414             # nettoyage
415             my $cmd = "rm -rf $reptmp*";
416             DEBUG $cmd;
417             my $cr = system($cmd) >> 8;
418              
419             # fin contexte archive
420             $self->{ctx_arch}--;
421              
422             # retour
423             return $err || $cr;
424             }
425              
426             =head2 purger_entree
427              
428             =cut
429              
430             # purge d'une entree du catalogue
431             # les fichiers et leur inode sont supprimes du catalogue
432             # les fichiers ne sont pas supprimes du disque
433             # on n'accede a aucun moment au disque
434             sub purger_entree {
435             my ($self, $entree) = @_;
436              
437             DEBUG "purge: $entree";
438              
439             # recherche de l'entree dans le catalogue
440             my $lst = $self->{db}->lister_fichiers($entree);
441              
442             foreach my $elt (@$lst) {
443             my ($md5sum, $type, $repfic) = @{$elt};
444              
445             # volinode de ce repfic
446             my ($volinode, $archive) =
447             @{ $self->{db}->lire_volinode_archive($repfic) };
448              
449             # nombre de fichiers supplementaires pour ce volinode
450             my $nbfic = $self->{db}->nb_occurrences_volinode($volinode) - 1;
451             DEBUG sprintf "autres occurrences de ce fichier = %d", $nbfic;
452              
453             # si nbfic
454             if ($nbfic) {
455             INFO sprintf("%s ! reste %d occurrence%s de ce fichier", $md5sum, $nbfic, ($nbfic > 1) ? "s" : "");
456             }
457             else {
458              
459             # alors supprimer enregistrement table Inode
460             DEBUG "deleteI: " . $volinode;
461             $self->{db}->execute('deleteI', $volinode);
462             }
463              
464             # delete repfic
465             # supprimer enregistrement table Fichier
466             DEBUG "deleteF: " . $repfic;
467             $self->{db}->execute('deleteF', $repfic);
468              
469             # progression
470             if (defined $self->{barre}) {
471             $self->{barre}->update(++$self->{nbfic});
472             }
473              
474             # archive : TODO ...
475              
476             }
477              
478             }
479              
480             =head2 md5sum
481              
482             =cut
483              
484             # calcul du md5
485             sub md5sum {
486             my ($self, $fic, $refStat) = @_;
487              
488             if (!$refStat) {
489             my @stat = lstat($fic);
490             $refStat = \@stat;
491             }
492             my $md5;
493             if (S_ISREG $refStat->[2]) {
494             if ($refStat->[7] < $self->{taille_limite_md5} * 1024) {
495              
496             # taille inferieure a la limite
497             $md5 = file_md5_hex($fic);
498             }
499             else {
500              
501             # taille bornee a la limite
502             my $tmp =
503             $self->{tmp} . "/" . $self->{moi} . "_" . $$ . "_" . $self->{i}++;
504             my $cmd = "dd if=\"$fic\" of=$tmp bs=1024 count=$self->{taille_limite_md5} 2>/dev/null";
505             DEBUG $cmd;
506             my $err = system($cmd) >> 8;
507             $md5 = file_md5_hex($tmp);
508             unlink $tmp;
509             }
510             }
511             return $md5;
512             }
513              
514             =head1 AUTHOR
515              
516             Patrick Hingrez, C<< >>
517              
518             =head1 BUGS
519              
520             Please report any bugs or feature requests to C, or through
521             the web interface at L. I will be notified, and then you'll
522             automatically be notified of progress on your bug as I make changes.
523              
524              
525              
526              
527             =head1 SUPPORT
528              
529             You can find documentation for this module with the perldoc command.
530              
531             perldoc File::Catalog
532              
533              
534             You can also look for information at:
535              
536             =over 4
537              
538             =item * RT: CPAN's request tracker (report bugs here)
539              
540             L
541              
542             =item * AnnoCPAN: Annotated CPAN documentation
543              
544             L
545              
546             =item * CPAN Ratings
547              
548             L
549              
550             =item * Search CPAN
551              
552             L
553              
554             =back
555              
556              
557             =head1 ACKNOWLEDGEMENTS
558              
559              
560             =head1 LICENSE AND COPYRIGHT
561              
562             Copyright 2015 Patrick Hingrez.
563              
564             This program is free software; you can redistribute it and/or modify it
565             under the terms of the the Artistic License (2.0). You may obtain a
566             copy of the full license at:
567              
568             L
569              
570             Any use, modification, and distribution of the Standard or Modified
571             Versions is governed by this Artistic License. By using, modifying or
572             distributing the Package, you accept this license. Do not use, modify,
573             or distribute the Package, if you do not accept this license.
574              
575             If your Modified Version has been derived from a Modified Version made
576             by someone other than you, you are nevertheless required to ensure that
577             your Modified Version complies with the requirements of this license.
578              
579             This license does not grant you the right to use any trademark, service
580             mark, tradename, or logo of the Copyright Holder.
581              
582             This license includes the non-exclusive, worldwide, free-of-charge
583             patent license to make, have made, use, offer to sell, sell, import and
584             otherwise transfer the Package with respect to any patent claims
585             licensable by the Copyright Holder that are necessarily infringed by the
586             Package. If you institute patent litigation (including a cross-claim or
587             counterclaim) against any party alleging that the Package constitutes
588             direct or contributory patent infringement, then this Artistic License
589             to you shall terminate on the date that such litigation is filed.
590              
591             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
592             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
593             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
594             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
595             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
596             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
597             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
598             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
599              
600              
601             =cut
602              
603             1; # End of File::Catalog