############################################################################### # # LocalFS.pm -- file functions acting upon the local filesystem # # A component of the Greenstone digital library software from the New Zealand # Digital Library Project at the University of Waikato, New Zealand. # # Copyright (C) 2013 New Zealand Digital Library Project # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 675 Mass # Ave, Cambridge, MA 02139, USA. # ############################################################################### package FileUtils::LocalFS; # Pragma use strict; ## @function closeFileHandle # sub closeFileHandle { my $fh_ref = shift(@_); close($$fh_ref); return 1; } ## closeFileHandle() ## @function filenameConcatenate() # sub filenameConcatenate { my $first_file = shift(@_); my (@filenames) = @_; # Useful for debugging # -- might make sense to call caller(0) rather than (1)?? # my ($cpackage,$cfilename,$cline,$csubr,$chas_args,$cwantarray) = caller(1); # print STDERR "Calling method: $cfilename:$cline $cpackage->$csubr\n"; # If first_file is not null or empty, then add it back into the list if (defined $first_file && $first_file =~ /\S/) { unshift(@filenames, $first_file); } my $filename = join("/", @filenames); # remove duplicate slashes and remove the last slash if ($ENV{'GSDLOS'} =~ /^windows$/i) { $filename =~ s/[\\\/]+/\\/g; } else { $filename =~ s/[\/]+/\//g; # DB: want a filename abc\de.html to remain like this } $filename =~ s/[\\\/]$//; return $filename; } ## filenameConcatenate() ## @function fileSize() # sub fileSize { my ($filename_full_path) = @_; return -s $filename_full_path; } ## fileStatus() ## @function fileTest() # sub fileTest { my $filename_full_path = shift(@_); # By default tests for existance of file or directory (-e) # Can be made more specific by providing second parameter (e.g. -f or -d) my $test_op = shift(@_) || '-e'; my $exists = 0; if ($ENV{'GSDLOS'} =~ m/^windows$/i) { require Win32; my $filename_short_path = Win32::GetShortPathName($filename_full_path); if (!defined $filename_short_path) { # Was probably still in UTF8 form (not what is needed on Windows) my $unicode_filename_full_path = eval "decode(\"utf8\",\$filename_full_path)"; if (defined $unicode_filename_full_path) { $filename_short_path = Win32::GetShortPathName($unicode_filename_full_path); } } $filename_full_path = $filename_short_path; } if (defined $filename_full_path) { $exists = eval "($test_op \$filename_full_path)"; } # The eval may result in exists being undefined, but we need to return # something return ($exists || 0); } ## fileTest() ## @function linkFile() # sub linkFile { my ($mode, $src, $dest, $ensure_paths_absolute) = @_; # remove trailing slashes from source and destination files $src =~ s/[\\\/]+$//; $dest =~ s/[\\\/]+$//; # Ensure file paths are absolute IF requested to do so # Soft_linking didn't work for relative paths if($mode eq 'HARD' || (defined $ensure_paths_absolute && $ensure_paths_absolute)) { # We need to ensure that the src file is the absolute path # See http://perldoc.perl.org/File/Spec.html if(!File::Spec->file_name_is_absolute( $src )) { # it's relative $src = File::Spec->rel2abs($src); # make absolute } # Might as well ensure that the destination file's absolute path is used if(!File::Spec->file_name_is_absolute( $dest )) { $dest = File::Spec->rel2abs($dest); # make absolute } } # a few sanity checks if (!-e $src) { &FileUtils::printError('Source file does not exist: ' . $src); return 0; } my $dest_dir = &File::Basename::dirname($dest); if (!-e $dest_dir) { &makeAllDirectories($dest_dir); } if ($ENV{'GSDLOS'} =~ /^windows$/i) { # symlink not supported on windows &FileUtils::printWarning('Symlink not supported on windows'); } elsif ($mode eq 'HARD') { if (!eval {link($src, $dest)}) { &FileUtils::printWarning('Unable to create hard link: ' . $dest); } } elsif ($mode eq 'SOFT') { if (!eval {symlink($src, $dest)}) { &FileUtils::printWarning('Unable to create soft link: ' . $src); } } else { &FileUtils::printError('Unknown mode requested: ' . $mode); } if (!-e $dest) { &_printWarning('linkFile', 'Link failed. Attempting to copy instead.'); &File::Copy::copy ($src, $dest); } return (-e $dest); } # /** linkFile() **/ ## @function makeAllDirectories() # # in case anyone cares - I did some testing (using perls Benchmark module) # on this subroutine against File::Path::mkpath (). mk_all_dir() is apparently # slightly faster (surprisingly) - Stefan. # sub makeAllDirectories { my ($dir) = @_; # use / for the directory separator, remove duplicate and # trailing slashes $dir=~s/[\\\/]+/\//g; $dir=~s/[\\\/]+$//; # ensure the directory doesn't already exist if (-e $dir) { return 0; } # make sure the cache directory exists my $dirsofar = ""; my $first = 1; foreach my $dirname (split ("/", $dir)) { $dirsofar .= "/" unless $first; $first = 0; $dirsofar .= $dirname; next if $dirname =~ /^(|[a-z]:)$/i; if (!-e $dirsofar) { my $store_umask = umask(0002); my $mkdir_ok = mkdir ($dirsofar, 0777); umask($store_umask); if (!$mkdir_ok) { &FileUtils::printError('Could not create directory: ' . $dirsofar); return 0; } } } return (-e $dir); } ## makeAllDirectories() ## @function makeDirectory() # sub makeDirectory { my $dir = shift(@_); my $store_umask = umask(0002); my $mkdir_ok = mkdir ($dir, 0777); umask($store_umask); return $mkdir_ok; } ## makeDirectory() ## @function modificationTime() # sub modificationTime { my $path = shift(@_); my @file_status = stat($path); return $file_status[9]; } ## modificationTime() ## @function openFileHandle() # sub openFileHandle { my $path = shift(@_); my $mode = shift(@_); my $fh_ref = shift(@_); my $encoding = shift(@_); my $mode_symbol; if ($mode eq 'w' || $mode eq '>') { $mode_symbol = '>'; $mode = 'writing'; } elsif ($mode eq 'a' || $mode eq '>>') { $mode_symbol = '>>'; $mode = 'appending'; } else { $mode_symbol = '<'; $mode = 'reading'; } if (defined $encoding) { $mode_symbol .= ':' . $encoding; } if (!open($$fh_ref, $mode_symbol, $path)) { &FileUtils::printError('Failed to open file for ' . $mode . ': ' . $path, 1); } return 1; } ## openFileHandle() # /** # */ sub readDirectory { my $path = shift(@_); opendir(DH, $path) or &FileUtils::printError('Failed to open directory for reading: ' . $path, 1); my @files = readdir(DH); close(DH); return \@files; } # /** readDirectory() **/ ## @function removeFiles() # sub removeFiles { my $file = shift(@_); my $result = 0; if (!-e $file && !-l $file) { &FileUtils::printError('File does not exist: ' . $file); } elsif ((!-f $file) && (!-l $file)) { &FileUtils::printError('Not a regular file: ' . $file); } else { $result = unlink($file); if (!$result) { &FileUtils::printError('Failed to remove file: ' . $file); } } return $result; } ## removeFiles() ## @function removeFilesFiltered() # # recursive removal # sub removeFilesFiltered { my ($files, $file_accept_re, $file_reject_re) = @_; # my ($cpackage,$cfilename,$cline,$csubr,$chas_args,$cwantarray) = caller(2); # my ($lcfilename) = ($cfilename =~ m/([^\\\/]*)$/); # print STDERR "** Calling method (2): $lcfilename:$cline $cpackage->$csubr\n"; my @files_array = (ref $files eq "ARRAY") ? @$files : ($files); my $num_removed = 0; foreach my $file (@files_array) { # remove trailing slashes $file =~ s/[\/\\]+$//; if (!-e $file) { print STDERR "util::filtered_rm_r $file does not exist\n"; } # don't recurse down symbolic link elsif ((-d $file) && (!-l $file)) { # get the contents of this directory if (!opendir (INDIR, $file)) { print STDERR "util::filtered_rm_r could not open directory $file\n"; } else { my @filedir = grep (!/^\.\.?$/, readdir (INDIR)); closedir (INDIR); # remove all the files in this directory map {$_="$file/$_";} @filedir; $num_removed += &FileUtils::LocalFS::removeFilesFiltered(\@filedir, $file_accept_re, $file_reject_re); if (!defined $file_accept_re && !defined $file_reject_re) { # remove this directory if (!rmdir $file) { print STDERR "util::filtered_rm_r couldn't remove directory $file\n"; } else { $num_removed++; } } } } else { next if (defined $file_reject_re && ($file =~ m/$file_reject_re/)); if ((!defined $file_accept_re) || ($file =~ m/$file_accept_re/)) { # remove this file $num_removed += &removeFiles($file); } } } return $num_removed; } ## removeFilesFiltered() ## @function removeFilesRecursive() # sub removeFilesRecursive { my $path = shift(@_); # use the more general (but reterospectively written filteredRemove() # function with no accept or reject expressions return FileUtils::LocalFS::removeFilesFiltered($path, undef, undef); } ## removeFilesRecursive() ## @function supportsSymbolicLink # sub supportsSymbolicLink { return 1; } ## supportsSymbolicLink() ## @function synchronizeDirectory() # # updates a copy of a directory in some other part of the filesystem # verbosity settings are: 0=low, 1=normal, 2=high # both $fromdir and $todir should be absolute paths # sub synchronizeDirectory { my ($fromdir, $todir, $verbosity) = @_; $verbosity = 1 unless defined $verbosity; # use / for the directory separator, remove duplicate and # trailing slashes $fromdir=~s/[\\\/]+/\//g; $fromdir=~s/[\\\/]+$//; $todir=~s/[\\\/]+/\//g; $todir=~s/[\\\/]+$//; &mk_all_dir ($todir); # get the directories in ascending order if (!opendir (FROMDIR, $fromdir)) { print STDERR "util::cachedir could not read directory $fromdir\n"; return; } my @fromdir = grep (!/^\.\.?$/, sort(readdir (FROMDIR))); closedir (FROMDIR); if (!opendir (TODIR, $todir)) { print STDERR "util::cacedir could not read directory $todir\n"; return; } my @todir = grep (!/^\.\.?$/, sort(readdir (TODIR))); closedir (TODIR); my $fromi = 0; my $toi = 0; while ($fromi < scalar(@fromdir) || $toi < scalar(@todir)) { # print "fromi: $fromi toi: $toi\n"; # see if we should delete a file/directory # this should happen if the file/directory # is not in the from list or if its a different # size, or has an older timestamp if ($toi < scalar(@todir)) { if (($fromi >= scalar(@fromdir)) || ($todir[$toi] lt $fromdir[$fromi] || ($todir[$toi] eq $fromdir[$fromi] && &differentfiles("$fromdir/$fromdir[$fromi]","$todir/$todir[$toi]", $verbosity)))) { # the files are different &rm_r("$todir/$todir[$toi]"); splice(@todir, $toi, 1); # $toi stays the same } elsif ($todir[$toi] eq $fromdir[$fromi]) { # the files are the same # if it is a directory, check its contents if (-d "$todir/$todir[$toi]") { &synchronizeDirectory("$fromdir/$fromdir[$fromi]", "$todir/$todir[$toi]", $verbosity); } $toi++; $fromi++; next; } } # see if we should insert a file/directory # we should insert a file/directory if there # is no tofiles left or if the tofile does not exist if ($fromi < scalar(@fromdir) && ($toi >= scalar(@todir) || $todir[$toi] gt $fromdir[$fromi])) { &cp_r ("$fromdir/$fromdir[$fromi]", "$todir/$fromdir[$fromi]"); splice (@todir, $toi, 0, $fromdir[$fromi]); $toi++; $fromi++; } } } ## synchronizeDirectory() # /** # */ sub transferFile { my ($mode, $file, $dest) = @_; # remove trailing slashes from source and destination files $file =~ s/[\\\/]+$//; $dest =~ s/[\\\/]+$//; my $tempdest = $dest; if (!-e $file) { &FileUtils::printError('File does not exist: ' . $file); } else { if (-d $tempdest) { my ($filename) = $file =~ /([^\\\/]+)$/; $tempdest .= "/$filename"; } # start by processing any move request if ($mode eq 'MOVE') { &File::Copy::move($file, $tempdest); } # now if we were instead doing a copy, or if the move request above failed # for some reason, we process a copy request if ($mode eq 'COPY' || !-e $tempdest) { &File::Copy::copy($file, $tempdest); } # finally, we check if a successful move command has somehow left the origin # file lying around (rename partially succeeded - for instance when moving # hardlinks) if ($mode eq 'MOVE' && -e $tempdest && -e $file) { unlink($file); } } # Have we successfully moved the file? my $result = 0; if (-e $tempdest) { if ($mode eq 'MOVE') { if (-e $file) { &FileUtils::printError('Failed to remove original file during move: ' . $file); } else { $result = 1; } } else { $result = 1; } } else { &FileUtils::printError('Failed to move/copy file: ' . $file); } return $result; } # /** moveFile() **/ 1;