Catalyst-Controller-AutoAssets

 view release on metacpan or  search on metacpan

lib/Catalyst/Controller/AutoAssets/Handler.pm  view on Meta::CPAN

package Catalyst::Controller::AutoAssets::Handler;
use strict;
use warnings;

# VERSION

use Moose::Role;
use namespace::autoclean;

requires qw(
  asset_request
  write_built_file
);

use Cwd;
use Path::Class 0.32 qw( dir file );
use Fcntl qw( :DEFAULT :flock );
use Carp;
use File::stat qw(stat);
use Catalyst::Utils;
use Time::HiRes qw(gettimeofday tv_interval);
use Storable qw(store retrieve);
use Try::Tiny;
use Data::Dumper::Concise 'Dumper';

require Digest::SHA1;
require MIME::Types;
require Module::Runtime;

has 'Controller' => (
  is => 'ro', required => 1,
  isa => 'Catalyst::Controller::AutoAssets',
  handles => [qw(type _app action_namespace unknown_asset _build_params _module_version)],
);

# Directories to include
has 'include', is => 'ro', isa => 'ScalarRef|Str|ArrayRef[ScalarRef|Str]', required => 1;

# Optional regex to require files to match to be included
has 'include_regex', is => 'ro', isa => 'Maybe[Str]', default => undef;

# Optional regex to exclude files
has 'exclude_regex', is => 'ro', isa => 'Maybe[Str]', default => undef;

# Whether or not to use qr/$regex/i or qr/$regex/
has 'regex_ignore_case', is => 'ro', isa => 'Bool', default => 0;

# Whether or not to make the current asset available via 307 redirect to the
# real, current checksum/fingerprint asset path
has 'current_redirect', is => 'ro', isa => 'Bool', default => 1;

# What string to use for the 'current' redirect
has 'current_alias', is => 'ro', isa => 'Str', default => 'current';

# Whether or not to make the current asset available via a static path
# with no benefit of caching
has 'allow_static_requests', is => 'ro', isa => 'Bool', default => 0;

# What string to use for the 'static' path
has 'static_alias', is => 'ro', isa => 'Str', default => 'static';

# Extra custom response headers for current/static requests 
has 'current_response_headers', is => 'ro', isa => 'HashRef', default => sub {{}};
has 'static_response_headers', is => 'ro', isa => 'HashRef', default => sub {{}};

# Whether or not to set 'Etag' response headers and check 'If-None-Match' request headers
# Very useful when using 'static' paths
has 'use_etags', is => 'ro', isa => 'Bool', default => 0;

# Max number of seconds before recalculating the fingerprint (sha1 checksum)
# regardless of whether or not the mtime has changed. 0 means infinite/disabled
has 'max_fingerprint_calc_age', is => 'ro', isa => 'Int', default => sub {0};

# Max number of seconds to wait to obtain a lock (to be thread safe)
has 'max_lock_wait', is => 'ro', isa => 'Int', default => 120;

has 'cache_control_header', is => 'ro', isa => 'Str', 
  default => sub { 'public, max-age=31536000, s-max-age=31536000' }; # 31536000 = 1 year

# Whether or not to use stored state data across restarts to avoid rebuilding.
has 'persist_state', is => 'ro', isa => 'Bool', default => sub{0};

# Optional shorter checksum
has 'sha1_string_length', is => 'ro', isa => 'Int', default => sub{40};

# directory to use for relative includes (defaults to the Catalyst home dir);
# TODO: coerce from Str
has 'include_relative_dir', isa => 'Path::Class::Dir', is => 'ro', lazy => 1, default => sub { 
  my $self = shift;
  my $home = $self->_app->config->{home};
  $home = $home && -d $home ? $self->_app->config->{home} : cwd();
  return dir( $home );
};



######################################


sub BUILD {}
before BUILD => sub {
  my $self = shift;
  
  # optionally initialize state data from the copy stored on disk for fast
  # startup (avoids having to always rebuild after every app restart):
  $self->_restore_state if($self->persist_state);

  # init includes
  $self->includes;
  
  Catalyst::Exception->throw("Must include at least one file/directory")
    unless (scalar @{$self->includes} > 0);

  # if the user picks something lower than 5 it is probably a mistake (really, anything
  # lower than 8 is probably not a good idea. But the full 40 is probably way overkill)
  Catalyst::Exception->throw("sha1_string_length must be between 5 and 40")
    unless ($self->sha1_string_length >= 5 && $self->sha1_string_length <= 40);

  # init work_dir:
  $self->work_dir->mkpath($self->_app->debug);
  $self->work_dir->resolve;
  
  $self->prepare_asset;
};

lib/Catalyst/Controller/AutoAssets/Handler.pm  view on Meta::CPAN

    $self->asset_request($c, $arg, @args);
  }
  return $c->detach;
}

sub client_current_etag {
  my ( $self, $c, $arg, @args ) = @_;
  
  my $etag = $self->etag_value(@args);
  $c->response->header( Etag => $etag );
  my $client_etag = $c->request->headers->{'if-none-match'};
  return ($client_etag && $client_etag eq $etag) ? 1 : 0;
}

sub etag_value {
  my $self = shift;
  return '"' . join('/',$self->asset_name,@_) . '"';
}


############################


has 'work_dir', is => 'ro', isa => 'Path::Class::Dir', lazy => 1, default => sub {
  my $self = shift;
  my $c = $self->_app;
  
  my $tmpdir = Catalyst::Utils::class2tempdir($c)
    || Catalyst::Exception->throw("Can't determine tempdir for $c");
    
  return dir($tmpdir, "AutoAssets",  $self->action_namespace($c));
};

has 'built_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
  my $self = shift;
  my $filename = 'built_file';
  return file($self->work_dir,$filename);
};

has 'scratch_dir', is => 'ro', isa => 'Path::Class::Dir', lazy => 1, default => sub {
  my $self = shift;
  
  my $Dir = dir($self->work_dir,'_scratch');
  $Dir->rmtree if (-d $Dir);
  $Dir->mkpath;
  
  return $Dir
};

has 'fingerprint_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
  my $self = shift;
  return file($self->work_dir,'fingerprint');
};

has 'lock_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
  my $self = shift;
  return file($self->work_dir,'lockfile');
};


has 'includes', is => 'ro', isa => 'ArrayRef', lazy => 1, default => sub {
  my $self = shift;
  my $rel = $self->include_relative_dir;

  my @list = ((ref $self->include)||'') eq 'ARRAY' ? @{$self->include} : $self->include;
  my $i = 0;
  return [ map {
    my $inc; $i++;
    if((ref($_)||'') eq 'SCALAR') {
      # New support for ScalarRef includes ... we pre-dump them to a temp file
      $inc = file( $self->scratch_dir, join('','_generated_include_file_',$i) );
      $inc->spew(iomode => '>:raw', $$_);
    }
    else {
      $inc = file($_);
    }
    $inc = $rel->file($inc) unless ($inc->is_absolute);
    $inc = dir($inc) if (-d $inc); #<-- convert to Path::Class::Dir
    $inc->resolve
  } @list ];
};




sub get_include_files {
  my $self = shift;
  
  my @excluded = ();
  my @files = ();
  for my $inc (@{$self->includes}) {
    if($inc->is_dir) {
      $inc->recurse(
        preorder => 1,
        depthfirst => 1,
        callback => sub {
          my $child = shift;
          $self->_valid_include_file($child)
            ? push @files, $child->absolute
            : push @excluded, $child->absolute;
        }
      );
    }
    else {
      $self->_valid_include_file($inc) 
        ? push @files, $inc->absolute 
        : push @excluded, $inc->absolute;
    }
  }
  
  # Some handlers (like Directory) need to know about excluded files
  $self->_record_excluded_files(\@excluded);
  
  # force consistent ordering of files:
  return [sort @files];
}

# optional hook for excluded files:
sub _record_excluded_files {}


lib/Catalyst/Controller/AutoAssets/Handler.pm  view on Meta::CPAN


has 'built_mtime', is => 'rw', isa => 'Maybe[Str]', default => sub{undef};
sub get_built_mtime {
  my $self = shift;
  return -f $self->built_file ? $self->built_file->stat->mtime : undef;
}

# inc_mtimes are the mtime(s) of the include files. For directory assets
# this is *only* the mtime of the top directory (see subfile_meta below)
has 'inc_mtimes', is => 'rw', isa => 'Maybe[Str]', default => undef;
sub get_inc_mtime_concat {
  my $self = shift;
  my $list = shift;
  return join('-', map { $_->stat->mtime } @$list );
}


sub calculate_fingerprint {
  my $self = shift;
  my $list = shift;
  # include both the include (source) and built (output) in the fingerprint:
  my $sha1 = $self->file_checksum(@$list,$self->built_file);
  $self->last_fingerprint_calculated(time) if ($sha1);
  return $sha1;
}

sub current_fingerprint {
  my $self = shift;
  return undef unless (-f $self->fingerprint_file);
  my $fingerprint = $self->fingerprint_file->slurp(iomode => '<:raw');
  return $fingerprint;
}

sub save_fingerprint {
  my $self = shift;
  my $fingerprint = shift or die "Expected fingerprint/checksum argument";
  return $self->fingerprint_file->spew(iomode => '>:raw', $fingerprint);
}

sub calculate_save_fingerprint {
  my $self = shift;
  my $fingerprint = $self->calculate_fingerprint(@_) or return 0;
  return $self->save_fingerprint($fingerprint);
}

sub fingerprint_calc_current {
  my $self = shift;
  my $last = $self->last_fingerprint_calculated or return 0;
  return 1 if ($self->max_fingerprint_calc_age == 0); # <-- 0 means infinite
  return 1 if (time - $last < $self->max_fingerprint_calc_age);
  return 0;
}

# -----
# Quick and dirty state persistence for faster startup
has 'persist_state_file', is => 'ro', isa => 'Path::Class::File', lazy => 1, default => sub {
  my $self = shift;
  return file($self->work_dir,'state.dat');
};

has '_persist_attrs', is => 'ro', isa => 'ArrayRef', default => sub{[qw(
 built_mtime
 inc_mtimes
 last_fingerprint_calculated
)]};

sub _persist_state {
  my $self = shift;
  return undef unless ($self->persist_state);
  my $data = { map { $_ => $self->$_ } @{$self->_persist_attrs} };
  $data->{_module_version} = $self->_module_version;
  $data->{_build_params} = $self->_build_params;
  store $data, $self->persist_state_file;
  return $data;
}

sub _restore_state {
  my $self = shift;
  return 0 unless (-f $self->persist_state_file);
  my $data;
  try {
    $data = retrieve $self->persist_state_file;
    if($self->_valid_state_data($data)) {
      $self->$_($data->{$_}) for (@{$self->_persist_attrs});
    }
  }
  catch {
    $self->clear_asset; #<-- make sure no partial state data is used
    $self->_app->log->warn(
      'Failed to restore state from ' . $self->persist_state_file
    );
  };
  return $data;
}

sub _valid_state_data {
  my ($self, $data) = @_;
  
  # Make sure the version and config params hasn't changed
  return (
    $self->_module_version eq $data->{_module_version}
    && Dumper($self->_build_params) eq Dumper($data->{_build_params})
  ) ? 1 : 0;
}
# -----


# force rebuild on next request/prepare_asset
sub clear_asset {
  my $self = shift;
  $self->inc_mtimes(undef);
}

sub _build_required {
  my ($self, $d) = @_;
  return (
    $self->inc_mtimes && $self->built_mtime &&
    $d->{inc_mtimes} && $d->{built_mtime} &&
    $self->inc_mtimes eq $d->{inc_mtimes} &&
    $self->built_mtime eq $d->{built_mtime} &&
    $self->fingerprint_calc_current



( run in 0.957 second using v1.01-cache-2.11-cpan-39bf76dae61 )