#! /usr/bin/perl -w
use strict;
=head1 Notices
Originally developed for Perforce by VIZIM (www.vizim.com)
Copyright (c) 2014 Perforce Software, Inc. and VIZIM Worldwide, Inc.
All rights reserved.
Please see LICENSE.txt in top-level folder of this distribution for
license information
=cut
use File::Spec::Functions qw(catfile);
use Digest::MD5;
use v5.14.0; # Earliest version testing was performed against
my $APPNAME = "profile.pl";
my $APPWHAT = 'Directory structure profiler; version 1.15';
my $APPNOTICES = "Copyright (c) 2014 Perforce Software, Inc. and VIZIM Worldwide, Inc.
All rights reserved.
See LICENSE.txt for license information.";
=head1 Directory structure profiler
Profile the contents of a directory structure.
The profile indicates directories and files.
Files contain BOM and the content MD5 signature information.
=cut
=head2 Tool focus
The focus of this tool is file existence and content.
Ownership, access rights, time, date, and other metadata is
not output.
Metadata is dependent on the workspace
population characteristics of the tool in use rather than being
a direct result of an import process.
=cut
=head2 Tool output
Files within a directory are listed first.
Subdirectories are then listed recursively.
File and directory names are listed in alphabetical order to make
comparison of results more deterministic.
File details are prefixed with 'F ' while directory details are
prefixed with 'P '.
The prefix values make it easier for comparison tools to resynchronize
at directories.
The primary use of this tool is to compare directory structures.
Typically these directory structures are workspaces created during
import processing.
Comparison is provided by profileCompare.pl.
To help normalize structure comparison the top level directory is
specified by two arguments.
The first argument is a top level directory path that is not listed in
generated output.
The second argument is a directory path that is listed in generated
output.
Either or both of the arguments may be relative paths.
=cut
=head2 Directory separators
Argument path specification is independent of the OS on which this tool
operates.
The tool adjusts directory separators so that they are
consistent and OS appropriate.
=cut
=head2 Case sensitivity
The -u option causes all file and directory names to be converted to upper
case.
This creates a uniformly sorted output that is useful when only content
and existence is important.
This option is only useful on Windows systems.
=cut
=head2 Examples
For example, to compare two workspaces with a common top level workspace
directory name such as (C:\usr\fred\ws) and (C:/usr/joe/ws) you could use:
profile.pl C:\usr\fred ws >fred
profile.pl C:/usr/joe ws > joe
profileCompare.pl fred joe
To compare the workspaces with different top level workspace directory names
such as (C:/usr/fred/ws) and (C:/usr/joe/workspace) you could use:
profile.pl C:/usr/fred/ws . >fred
profile.pl C:/usr/joe/workspace . >joe
profileCompare.pl fred joe
Note the use of '.' as the second argument to remove all parts of the
workspace path from the output.
=cut
sub isDirectoryEmpty($);
sub isDirectoryEmpty($)
{
my $path = $_[0];
return 0 if ! -e $path;
return 0 if ! -d $path;
my $hDir;
return 0 if !opendir($hDir, $path);
my $isEmpty = 1;
while ($_ = readdir($hDir)) {
next if m!^\.{1,2}$!;
my $thisPath = "$path/$_";
next if -d $thisPath && isDirectoryEmpty( $thisPath );
$isEmpty = 0;
last;
}
closedir($hDir);
return $isEmpty;
}
sub FileDetails($)
{
my $path = $_[0];
return 'Does Not Exist' unless -e $path;
if( -d $path ) {
return 'Directory (no files)' if isDirectoryEmpty( $path );
return 'Directory';
}
return 'Directory' if -d $path;
return 'Not A File' unless -f $path;
my $hLocal;
if( ! open( $hLocal, '<:bytes', $path ) ) {
return "Can Not Open - $!";
}
my $type = '';
my $header = '';
read $hLocal, $header, 3, 0;
if( $header =~ m!^\xEF\xBB\xBF! ) {
$type = 'UTF8';
} elsif( $header =~ m!^\xFE\xFF! ) {
$type = 'UTF16BE';
} elsif( $header =~ m!^\xFF\xFE! ) {
$type = 'UTF16LE';
} else {
$type = -z $path ? 'EMPTY' : 'NOBOM';
}
seek $hLocal, 0, 0;
my $ctx = Digest::MD5->new;
$ctx->reset();
$ctx->addfile( $hLocal );
close( $hLocal );
return ($type, uc $ctx->hexdigest() );
}
my $ifUpperNames = 0;
if( defined $ARGV[0] && ($ARGV[0] =~ m!\-+V!) ) {
print "$APPWHAT\n";
exit(0);
}
if( defined $ARGV[0] && ($ARGV[0] =~ m!\-+u!) ) {
$ifUpperNames = 1;
shift @ARGV;
}
if( (defined $ARGV[0] && ($ARGV[0] =~ m!\-+[h\?]!i)) || (scalar @ARGV != 2) ) {
print "$APPWHAT
$APPNOTICES
Usage:
$APPNAME -V
$APPNAME [-h|-?]
$APPNAME [-u] CommonRoot TopDirectory\n";
exit(0);
}
my @list = ();
sub buildList($$);
sub buildList($$)
{
my ($root, $dir) = @_;
my $rootPath = catfile( $ARGV[0], $root, $dir );
die "$rootPath does not exist" if ! -e $rootPath;
die "$rootPath is not a directory" if ! -d $rootPath;
my $hDIR;
opendir( $hDIR, $rootPath ) || die "Can't open $rootPath $!";
my @files = ();
my @dirs = ();
while(readdir($hDIR)) {
next if $_ eq '.' || $_ eq '..' || uc $_ eq '$TF';
my $local = $ifUpperNames ? uc $_ : $_;
my $full = catfile( $rootPath, $_ );
push @files, catfile( $root, $dir, $local ) if -f $full;
push @dirs, $local if -d $full;
}
closedir( $hDIR );
foreach( sort @files ) {
push @list, $_;
}
foreach( sort @dirs ) {
push @list, catfile( $root, $dir, $_ );
buildList( catfile( $root, $dir ), $_ );
}
}
buildList( '', $ARGV[1] );
foreach(@list) {
my $full = catfile( $ARGV[0], $_ );
if( -f $full ) {
print "F $_ ", FileDetails($full), "\n";
} elsif( -d $full ) {
print "P $_ ", FileDetails($full), "\n";
} else {
print "? $_ ", FileDetails($full), "\n";
}
}