######################################################################
# $Id$
# Copyright (C) 2001-2003 DeWitt Clinton  All Rights Reserved
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either expressed or
# implied. See the License for the specific language governing
# rights and limitations under the License.
######################################################################

package Cache::FileBackend;

use strict;
use Cache::CacheUtils qw( Assert_Defined Build_Path Freeze_Data Thaw_Data );
use Digest::SHA1 qw( sha1_hex );
use Error;
use File::Path qw( mkpath );
use File::Temp qw( tempfile );


# the file mode for new directories, which will be modified by the
# current umask

my $DIRECTORY_MODE = 0777;


# regex for untainting directory and file paths. since all paths are
# generated by us or come from user via API, a tautological regex
# suffices.

my $UNTAINTED_PATH_REGEX = '^(.*)$';


sub new
{
  my ( $proto, $p_root, $p_depth, $p_directory_umask ) = @_;
  my $class = ref( $proto ) || $proto;
  my $self  = {};
  $self = bless( $self, $class );
  $self->set_root( $p_root );
  $self->set_depth( $p_depth );
  $self->set_directory_umask( $p_directory_umask );
  return $self;
}


sub delete_key
{
  my ( $self, $p_namespace, $p_key ) = @_;

  Assert_Defined( $p_namespace );
  Assert_Defined( $p_key );

  _Remove_File( $self->_path_to_key( $p_namespace, $p_key ) );
}


sub delete_namespace
{
  my ( $self, $p_namespace ) = @_;

  Assert_Defined( $p_namespace );

  _Recursively_Remove_Directory( Build_Path( $self->get_root( ),
                                             $p_namespace ) );
}


sub get_keys
{
  my ( $self, $p_namespace ) = @_;

  Assert_Defined( $p_namespace );

  my @keys;

  foreach my $unique_key ( $self->_get_unique_keys( $p_namespace ) )
  {
    my $key = $self->_get_key_for_unique_key( $p_namespace, $unique_key ) or
      next;

    push( @keys, $key );
  }

  return @keys;

}


sub get_namespaces
{
  my ( $self ) = @_;

  my @namespaces;

  _List_Subdirectories( $self->get_root( ), \@namespaces );

  return @namespaces;
}


sub get_size
{
  my ( $self, $p_namespace, $p_key ) = @_;

  Assert_Defined( $p_namespace );
  Assert_Defined( $p_key );

  if ( -e $self->_path_to_key( $p_namespace, $p_key ) )
  {
    return -s $self->_path_to_key( $p_namespace, $p_key );

  }
  else
  {
    return 0;
  }
}


sub restore
{
  my ( $self, $p_namespace, $p_key ) = @_;

  Assert_Defined( $p_namespace );
  Assert_Defined( $p_key );

  return $self->_read_data( $self->_path_to_key($p_namespace, $p_key) )->[1];
}


sub store
{
  my ( $self, $p_namespace, $p_key, $p_data ) = @_;

  Assert_Defined( $p_namespace );
  Assert_Defined( $p_key );

  $self->_write_data( $self->_path_to_key( $p_namespace, $p_key ),
                      [ $p_key, $p_data ] );

}


sub get_depth
{
  my ( $self ) = @_;

  return $self->{_Depth};
}


sub set_depth
{
  my ( $self, $depth ) = @_;

  $self->{_Depth} = $depth;
}


sub get_root
{
  my ( $self ) = @_;

  return $self->{_Root};
}


sub set_root
{
  my ( $self, $root ) = @_;

  $self->{_Root} = $root;
}


sub get_directory_umask
{
  my ( $self ) = @_;

  return $self->{_Directory_Umask};
}


sub set_directory_umask
{
  my ( $self, $directory_umask ) = @_;

  $self->{_Directory_Umask} = $directory_umask;
}


# Take an human readable key, and create a unique key from it

sub _Build_Unique_Key
{
  my ( $p_key ) = @_;

  Assert_Defined( $p_key );

  return sha1_hex( $p_key );
}


# create a directory with optional mask, building subdirectories as
# needed.

sub _Create_Directory
{
  my ( $p_directory, $p_optional_new_umask ) = @_;

  Assert_Defined( $p_directory );

  my $old_umask = umask( ) if defined $p_optional_new_umask;

  umask( $p_optional_new_umask ) if defined $p_optional_new_umask;

  my $directory = _Untaint_Path( $p_directory );

  $directory =~ s|/$||;

  mkpath( $directory, 0, $DIRECTORY_MODE );

  -d $directory or
    throw Error::Simple( "Couldn't create directory: $directory: $!" );

  umask( $old_umask ) if defined $old_umask;
}



# list the names of the subdirectories in a given directory, without the
# full path

sub _List_Subdirectories
{
  my ( $p_directory, $p_subdirectories_ref ) = @_;

  foreach my $dirent ( _Read_Dirents( $p_directory ) )
  {
    next if $dirent eq '.' or $dirent eq '..';

    my $path = Build_Path( $p_directory, $dirent );

    next unless -d $path;

    push( @$p_subdirectories_ref, $dirent );
  }
}


# read the dirents from a directory

sub _Read_Dirents
{
  my ( $p_directory ) = @_;

  Assert_Defined( $p_directory );

  -d $p_directory or
    return ( );

  local *Dir;

  opendir( Dir, _Untaint_Path( $p_directory ) ) or
    throw Error::Simple( "Couldn't open directory $p_directory: $!" );

  my @dirents = readdir( Dir );

  closedir( Dir ) or
    throw Error::Simple( "Couldn't close directory $p_directory: $!" );

  return @dirents;
}


# read in a file. returns a reference to the data read

sub _Read_File
{
  my ( $p_path ) = @_;

  Assert_Defined( $p_path );

  local *File;

  open( File, _Untaint_Path( $p_path ) ) or
    return undef;

  binmode( File );

  local $/ = undef;

  my $data_ref;

  $$data_ref = <File>;

  close( File );

  return $data_ref;
}


# read in a file. returns a reference to the data read, without
# modifying the last accessed time

sub _Read_File_Without_Time_Modification
{
  my ( $p_path ) = @_;

  Assert_Defined( $p_path );

  -e $p_path or
    return undef;

  my ( $file_access_time, $file_modified_time ) =
    ( stat( _Untaint_Path( $p_path ) ) )[8,9];

  my $data_ref = _Read_File( $p_path );

  utime( $file_access_time, $file_modified_time, _Untaint_Path( $p_path ) );

  return $data_ref;
}


# remove a file

sub _Remove_File
{
  my ( $p_path ) = @_;

  Assert_Defined( $p_path );

  if ( -f _Untaint_Path( $p_path ) )
  {
    # We don't catch the error, because this may fail if two
    # processes are in a race and try to remove the object

    unlink( _Untaint_Path( $p_path ) );
  }
}


# remove a directory

sub _Remove_Directory
{
  my ( $p_directory ) = @_;

  Assert_Defined( $p_directory );

  if ( -d _Untaint_Path( $p_directory ) )
  {
    # We don't catch the error, because this may fail if two
    # processes are in a race and try to remove the object

    rmdir( _Untaint_Path( $p_directory ) );
  }
}


# recursively list the files of the subdirectories, without the full paths

sub _Recursively_List_Files
{
  my ( $p_directory, $p_files_ref ) = @_;

  return unless -d $p_directory;

  foreach my $dirent ( _Read_Dirents( $p_directory ) )
  {
    next if $dirent eq '.' or $dirent eq '..';

    my $path = Build_Path( $p_directory, $dirent );

    if ( -d $path )
    {
      _Recursively_List_Files( $path, $p_files_ref );
    }
    else
    {
      push( @$p_files_ref, $dirent );
    }
  }
}


# recursively list the files of the subdirectories, with the full paths

sub _Recursively_List_Files_With_Paths
{
  my ( $p_directory, $p_files_ref ) = @_;

  foreach my $dirent ( _Read_Dirents( $p_directory ) )
  {
    next if $dirent eq '.' or $dirent eq '..';

    my $path = Build_Path( $p_directory, $dirent );

    if ( -d $path )
    {
      _Recursively_List_Files_With_Paths( $path, $p_files_ref );
    }
    else
    {
      push( @$p_files_ref, $path );
    }
  }
}



# remove a directory and all subdirectories and files

sub _Recursively_Remove_Directory
{
  my ( $p_root ) = @_;

  return unless -d $p_root;

  foreach my $dirent ( _Read_Dirents( $p_root ) )
  {
    next if $dirent eq '.' or $dirent eq '..';

    my $path = Build_Path( $p_root, $dirent );

    if ( -d $path )
    {
      _Recursively_Remove_Directory( $path );
    }
    else
    {
      _Remove_File( _Untaint_Path( $path ) );
    }
  }

  _Remove_Directory( _Untaint_Path( $p_root ) );
}



# walk down a directory structure and total the size of the files
# contained therein.

sub _Recursive_Directory_Size
{
  my ( $p_directory ) = @_;

  Assert_Defined( $p_directory );

  return 0 unless -d $p_directory;

  my $size = 0;

  foreach my $dirent ( _Read_Dirents( $p_directory ) )
  {
    next if $dirent eq '.' or $dirent eq '..';

    my $path = Build_Path( $p_directory, $dirent );

    if ( -d $path )
    {
      $size += _Recursive_Directory_Size( $path );
    }
    else
    {
      $size += -s $path;
    }
  }

  return $size;
}


# Untaint a file path

sub _Untaint_Path
{
  my ( $p_path ) = @_;

  return _Untaint_String( $p_path, $UNTAINTED_PATH_REGEX );
}


# Untaint a string

sub _Untaint_String
{
  my ( $p_string, $p_untainted_regex ) = @_;

  Assert_Defined( $p_string );
  Assert_Defined( $p_untainted_regex );

  my ( $untainted_string ) = $p_string =~ /$p_untainted_regex/;

  if ( not defined $untainted_string || $untainted_string ne $p_string )
  {
    throw Error::Simple( "String $p_string contains possible taint" );
  }

  return $untainted_string;
}


# create a directory with the optional umask if it doesn't already
# exist

sub _Make_Path
{
  my ( $p_path, $p_optional_new_umask ) = @_;

  my ( $volume, $directory, $filename ) = File::Spec->splitpath( $p_path );

  if ( defined $directory and defined $volume )
  {
    $directory = File::Spec->catpath( $volume, $directory, "" );
  }

  if ( defined $directory and not -d $directory )
  {
    _Create_Directory( $directory, $p_optional_new_umask );
  }
}


# return a list of the first $depth letters in the $word

sub _Split_Word
{
  my ( $p_word, $p_depth ) = @_;

  Assert_Defined( $p_word );
  Assert_Defined( $p_depth );

  my @split_word_list;

  for ( my $i = 0; $i < $p_depth; $i++ )
  {
    push ( @split_word_list, substr( $p_word, $i, 1 ) );
  }

  return @split_word_list;
}


# write a file atomically

sub _Write_File
{
  my ( $p_path, $p_data_ref, $p_optional_mode, $p_optional_umask ) = @_;

  Assert_Defined( $p_path );
  Assert_Defined( $p_data_ref );

  my $old_umask = umask if $p_optional_umask;

  umask( $p_optional_umask ) if $p_optional_umask;

  my ( $volume, $directory, $filename ) = File::Spec->splitpath( $p_path );

  # Bugfix for win32
  if ( defined $directory and defined $volume )
  {
    $directory = File::Spec->catpath( $volume, $directory, "" );
  }
  # End bugfix

  my ( $temp_fh, $temp_filename ) = tempfile( DIR => $directory );

  binmode( $temp_fh );

  print $temp_fh $$p_data_ref;

  close( $temp_fh );

  -e $temp_filename or
    throw Error::Simple( "Temp file '$temp_filename' does not exist: $!" );
  
  rename( $temp_filename, _Untaint_Path( $p_path ) ) or
    throw Error::Simple( "Couldn't rename $temp_filename to $p_path: $!" );

  if ( -e $temp_filename ) 
  {
    _Remove_File( $temp_filename );
    warn( "Temp file '$temp_filename' shouldn't still exist" );
  }

  $p_optional_mode ||= 0666 - umask( );

  chmod( $p_optional_mode, _Untaint_Path($p_path) );

  umask( $old_umask ) if $old_umask;
}


sub _get_key_for_unique_key
{
  my ( $self, $p_namespace, $p_unique_key ) = @_;

  return $self->_read_data( $self->_path_to_unique_key( $p_namespace,
                                                        $p_unique_key ) )->[0];
}


sub _get_unique_keys
{
  my ( $self, $p_namespace ) = @_;

  Assert_Defined( $p_namespace );

  my @unique_keys;

  _Recursively_List_Files( Build_Path( $self->get_root( ), $p_namespace ),
                           \@unique_keys );

  return @unique_keys;
}


sub _path_to_key
{
  my ( $self, $p_namespace, $p_key ) = @_;

  Assert_Defined( $p_namespace );
  Assert_Defined( $p_key );

  return $self->_path_to_unique_key( $p_namespace,
                                     _Build_Unique_Key( $p_key ) );
}


sub _path_to_unique_key
{
  my ( $self, $p_namespace, $p_unique_key ) = @_;

  Assert_Defined( $p_unique_key );
  Assert_Defined( $p_namespace );

  return Build_Path( $self->get_root( ),
                     $p_namespace,
                     _Split_Word( $p_unique_key, $self->get_depth( ) ),
                     $p_unique_key );
}

# the data is returned as reference to an array ( key, data )

sub _read_data
{
  my ( $self, $p_path ) = @_;

  Assert_Defined( $p_path );

  my $frozen_data_ref = _Read_File_Without_Time_Modification( $p_path ) or
    return [ undef, undef ];

  my $data_ref = eval{ Thaw_Data( $$frozen_data_ref ) };
  
  if ( $@ || ( ref( $data_ref ) ne 'ARRAY' ) ) 
  {
    unlink _Untaint_Path( $p_path );
    return [ undef, undef ];
  }
  else
  {
    return $data_ref;
  }
}


# the data is passed as reference to an array ( key, data )

sub _write_data
{
  my ( $self, $p_path, $p_data ) = @_;

  Assert_Defined( $p_path );
  Assert_Defined( $p_data );

  _Make_Path( $p_path, $self->get_directory_umask( ) );

  my $frozen_file = Freeze_Data( $p_data );

  _Write_File( $p_path, \$frozen_file );
}


1;


__END__

=pod

=head1 NAME

Cache::FileBackend -- a filesystem based persistance mechanism

=head1 DESCRIPTION

The FileBackend class is used to persist data to the filesystem

=head1 SYNOPSIS

  my $backend = new Cache::FileBackend( '/tmp/FileCache', 3, 000 );

  See Cache::Backend for the usage synopsis.

  $backend->store( 'namespace', 'foo', 'bar' );

  my $bar = $backend->restore( 'namespace', 'foo' );

  my $size_of_bar = $backend->get_size( 'namespace', 'foo' );

  foreach my $key ( $backend->get_keys( 'namespace' ) )
  {
    $backend->delete_key( 'namespace', $key );
  }

  foreach my $namespace ( $backend->get_namespaces( ) )
  {
    $backend->delete_namespace( $namespace );
  }

=head1 METHODS

See Cache::Backend for the API documentation.

=over

=item B<new( $root, $depth, $directory_umask )>

Construct a new FileBackend that writes data to the I<$root>
directory, automatically creates subdirectories I<$depth> levels deep,
and uses the umask of I<$directory_umask> when creating directories.

=back

=head1 PROPERTIES

=over

=item B<(get|set)_root>

The location of the parent directory in which to store the files

=item B<(get|set)_depth>

The branching factor of the subdirectories created to store the files

=item B<(get|set)_directory_umask>

The umask to be used when creating directories

=back

=head1 SEE ALSO

Cache::Backend, Cache::MemoryBackend, Cache::SharedMemoryBackend

=head1 AUTHOR

Original author: DeWitt Clinton <dewitt@unto.net>

Last author:     $Author$

Copyright (C) 2001-2003 DeWitt Clinton

=cut
