File Coverage

blib/lib/Catalyst/Controller/SimpleCAS.pm
Criterion Covered Total %
statement 67 210 31.9
branch 0 82 0.0
condition 0 24 0.0
subroutine 23 42 54.7
pod 14 14 100.0
total 104 372 27.9


line stmt bran cond sub pod time code
1             package Catalyst::Controller::SimpleCAS;
2 1     1   656 use strict;
  1         2  
  1         23  
3 1     1   3 use warnings;
  1         1  
  1         31  
4              
5             # ABSTRACT: General-purpose content-addressed storage (CAS) for Catalyst
6              
7             our $VERSION = '1.001';
8              
9 1     1   408 use Moose;
  1         298340  
  1         6  
10 1     1   5298 use Types::Standard qw(:all);
  1         48183  
  1         8  
11 1     1   24452 use namespace::autoclean;
  1         5922  
  1         4  
12              
13 1     1   59 BEGIN { extends 'Catalyst::Controller' }
14             with 'Catalyst::Controller::SimpleCAS::Role::TextTranscode';
15              
16 1     1   494506 use Catalyst::Controller::SimpleCAS::Content;
  1         3  
  1         45  
17              
18 1     1   9 use Module::Runtime;
  1         1  
  1         6  
19 1     1   53 use Try::Tiny;
  1         2  
  1         70  
20 1     1   4 use Catalyst::Utils;
  1         2  
  1         22  
21 1     1   4 use Path::Class qw(file dir);
  1         1  
  1         51  
22 1     1   4 use JSON;
  1         1  
  1         9  
23 1     1   116 use MIME::Base64;
  1         1  
  1         37  
24 1     1   640 use String::Random;
  1         2303  
  1         45  
25              
26 1     1   7 use Scalar::Util 'blessed';
  1         1  
  1         672  
27              
28             has store_class => ( is => 'ro', default => sub {
29             '+File'
30             });
31              
32             has store_path => ( is => 'ro', lazy => 1, default => sub {
33             my $self = shift;
34             my $c = $self->_app;
35             # Default Cas Store path if none was supplied in the config:
36             return dir( Catalyst::Utils::home($c), 'cas_store' )->stringify;
37             });
38              
39             has store_args => ( is => 'ro', isa => 'HashRef', lazy => 1, default => sub {
40             my $self = shift;
41             return {
42             store_dir => $self->store_path,
43             };
44             });
45              
46             has Store => (
47             does => 'Catalyst::Controller::SimpleCAS::Store',
48             is => 'ro',
49             lazy => 1,
50             default => sub {
51             my $self = shift;
52             my $class = $self->store_class;
53             if ($class =~ m/^\+([\w:]+)/) {
54             $class = 'Catalyst::Controller::SimpleCAS::Store::'.$1;
55             }
56             Module::Runtime::require_module($class);
57             return $class->new(
58             simplecas => $self,
59             %{$self->store_args},
60             );
61             },
62             handles => [qw(
63             file_checksum
64             calculate_checksum
65             )],
66             );
67              
68              
69             ### ----------------------------------------------------------------------
70             ### New sugar/convenience methods:
71             ###
72             sub fetch {
73 0     0 1   my ($self, $cas_id) = @_;
74 0           $self->uri_find_Content($cas_id)
75             }
76              
77             sub fetch_fh {
78 0     0 1   my ($self, $cas_id) = @_;
79 0 0         my $checksum = $self->_find_prune_checksum($cas_id) or return undef;
80 0           $self->Store->fetch_content_fh($checksum)
81             }
82              
83             sub add {
84 0     0 1   my $self = shift;
85 0           my $cnt = shift;
86            
87 0           my $content = '';
88            
89 0 0         if(my $type = ref $cnt) {
90 0 0 0       if($type eq 'SCALAR') {
    0          
91 0           $content = $$cnt;
92             }
93             elsif(blessed $cnt && $cnt->can('getline')) {
94 0           while(my $line = $cnt->getline) {
95 0           $content .= $line;
96             }
97             }
98             else {
99 0           die "Bad content argument $cnt!";
100             }
101             }
102             else {
103 0           $content = $cnt;
104             }
105            
106             # Is this a file name?
107 0 0 0       return $self->Store->add_content_file($content) if (
      0        
108             length($content) < 1024 &&
109             !($content =~ /\n/) &&
110             -f $content
111             );
112              
113 0           return $self->Store->add_content($content)
114             }
115             ###
116             ### ----------------------------------------------------------------------
117              
118              
119             #has 'fetch_url_path', is => 'ro', isa => 'Str', default => '/simplecas/fetch_content/';
120              
121             sub Content {
122 0     0 1   my $self = shift;
123 0           my $checksum = shift;
124 0           my $filename = shift; #<-- optional
125 0           return Catalyst::Controller::SimpleCAS::Content->new(
126             Store => $self->Store,
127             checksum => $checksum,
128             filename => $filename
129             );
130             }
131              
132             # Accepts a free-form string and tries to extract a Cas checksum string from it. If the
133             # checksum exists, thr pruned checksum string is returned
134             sub _find_prune_checksum {
135 0     0     my $self = shift;
136 0 0         my $uri = shift or return undef;
137 0           my @parts = split(/\//,$uri);
138            
139 0           while (scalar @parts > 0) {
140 0           my $checksum = shift @parts;
141 0 0         next unless ($checksum =~ /^[0-9a-f]{40}$/);
142 0 0         return $checksum if ($self->Store->content_exists($checksum));
143             }
144 0           return undef;
145             }
146              
147             # Accepts a free-form string and tries to extract a Cas checksum and
148             # filename from it. If it is successfully, and the checksum exists in
149             # the Store, returns the Content object
150             sub uri_find_Content {
151 0     0 1   my $self = shift;
152 0 0         my $uri = shift or return undef;
153 0           my @parts = split(/\//,$uri);
154            
155 0           while (scalar @parts > 0) {
156 0           my $checksum = shift @parts;
157 0 0         next unless ($checksum =~ /^[0-9a-f]{40}$/);
158 0 0         my $filename = (scalar @parts == 1) ? $parts[0] : undef;
159 0 0         my $Content = $self->Content($checksum,$filename) or next;
160 0           return $Content;
161             }
162 0           return undef;
163             }
164              
165 1     1 1 6 sub base :Chained :PathPrefix :CaptureArgs(0) {}
  1     0   1  
  1         7  
166              
167             sub fetch_content :Chained('base') :Args {
168 0     0 1 0 my ($self, $c, $checksum, $filename) = @_;
169            
170 0         0 my $disposition = 'inline;filename="' . $checksum . '"';
171            
172 0 0       0 if ($filename) {
173 0         0 $filename =~ s/\"/\'/g;
174 0         0 $disposition = 'attachment; filename="' . $filename . '"';
175             }
176            
177 0 0       0 unless($self->Store->content_exists($checksum)) {
178 0         0 $c->res->body('Does not exist');
179 0         0 return;
180             }
181            
182 0 0       0 my $type = $self->Store->content_mimetype($checksum) or die "Error reading mime type";
183            
184             # type overrides for places where File::MimeInfo::Magic is known to guess wrong
185 0 0 0     0 if($type eq 'application/vnd.ms-powerpoint' || $type eq 'application/zip') {
186 0         0 my $Content = $self->Content($checksum,$filename);
187 0         0 my $ext = lc($Content->file_ext);
188 0 0       0 $type =
    0          
    0          
    0          
    0          
189             $ext eq 'doc' ? 'application/msword' :
190             $ext eq 'xls' ? 'application/vnd.ms-excel' :
191             $ext eq 'docx' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
192             $ext eq 'xlsx' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' :
193             $ext eq 'pptx' ? 'application/vnd.openxmlformats-officedocument.presentationml.presentation' :
194             $type;
195             }
196            
197 0         0 $c->response->header('Content-Type' => $type);
198 0         0 $c->response->header('Content-Disposition' => $disposition);
199 0         0 return $c->res->body( $self->Store->fetch_content_fh($checksum) );
200 1     1   1204 }
  1         1  
  1         3  
201              
202              
203             sub upload_content :Chained('base') :Args {
204 0     0 1 0 my ($self, $c) = @_;
205              
206 0 0       0 my $upload = $c->req->upload('Filedata') or die "no upload object";
207 0 0       0 my $checksum = $self->Store->add_content_file_mv($upload->tempname) or die "Failed to add content";
208            
209 0         0 return $c->res->body($checksum);
210 1     1   725 }
  1         1  
  1         3  
211              
212              
213             sub upload_image :Chained('base') :Args {
214 0     0 1 0 my ($self, $c, $maxwidth, $maxheight) = @_;
215              
216 0 0       0 my $upload = $c->req->upload('Filedata') or die "no upload object";
217              
218 0         0 my ($type,$subtype) = split(/\//,$upload->type);
219            
220 0         0 my $resized = \0;
221 0         0 my $shrunk = \0;
222            
223 0         0 my ($checksum,$width,$height,$orig_width,$orig_height);
224              
225 0 0       0 if($self->_is_image_resize_available) {
226             # When Image::Resize is available:
227 0         0 ($checksum,$width,$height,$resized,$orig_width,$orig_height)
228             = $self->add_resize_image($upload->tempname,$type,$subtype,$maxwidth,$maxheight);
229             }
230             else {
231             # Fall-back calculates new image size without actually resizing it. The img
232             # tag will still be smaller, but the image file will be original dimensions
233 0         0 ($checksum,$width,$height,$shrunk,$orig_width,$orig_height)
234             = $self->add_size_info_image($upload->tempname,$type,$subtype,$maxwidth,$maxheight);
235             }
236            
237            
238 0         0 unlink $upload->tempname;
239            
240             #my $tag = '<img src="/simplecas/fetch_content/' . $checksum . '"';
241             #$tag .= ' width=' . $width . ' height=' . $height if ($width and $height);
242             #$tag .= '>';
243            
244             # TODO: fix this API!!!
245            
246 0         0 my $packet = {
247             success => \1,
248             checksum => $checksum,
249             height => $height,
250             width => $width,
251             resized => $resized,
252             shrunk => $shrunk,
253             orig_width => $orig_width,
254             orig_height => $orig_height,
255             filename => $self->safe_filename($upload->filename),
256             };
257            
258 0         0 return $self->_json_response($c, $packet);
259 1     1   818 }
  1         2  
  1         4  
260              
261             sub _is_image_resize_available {
262 0     0     my $flag = 1;
263 0     0     try { Module::Runtime::require_module('Image::Resize') }
264 0     0     catch { $flag = 0 };
  0            
265 0           $flag
266             }
267              
268              
269             sub add_resize_image :Private {
270 0     0 1 0 my ($self,$file,$type,$subtype,$maxwidth,$maxheight) = @_;
271            
272 0 0       0 my $checksum = $self->Store->add_content_file($file) or die "Failed to add content";
273            
274 0         0 my $resized = \0;
275            
276 0         0 my ($width,$height) = $self->Store->image_size($checksum);
277 0         0 my ($orig_width,$orig_height) = ($width,$height);
278 0 0       0 if (defined $maxwidth) {
279            
280 0         0 my ($newwidth,$newheight) = ($width,$height);
281            
282 0 0       0 if($width > $maxwidth) {
283 0         0 my $ratio = $maxwidth/$width;
284 0         0 $newheight = int($ratio * $height);
285 0         0 $newwidth = $maxwidth;
286             }
287            
288 0 0 0     0 if(defined $maxheight and $newheight > $maxheight) {
289 0         0 my $ratio = $maxheight/$newheight;
290 0         0 $newwidth = int($ratio * $newwidth);
291 0         0 $newheight = $maxheight;
292             }
293            
294 0 0 0     0 unless ($newwidth == $width && $newheight == $height) {
295            
296 0         0 my $image = Image::Resize->new($self->Store->checksum_to_path($checksum));
297 0         0 my $gd = $image->resize($newwidth,$newheight);
298            
299 0         0 my $method = 'png';
300 0 0       0 $method = $subtype if ($gd->can($subtype));
301            
302 0         0 my $tmpfile = file(
303             Catalyst::Utils::class2tempdir($self->_app,1),
304             String::Random->new->randregex('[a-z0-9A-Z]{15}')
305             );
306            
307 0         0 $tmpfile->spew( $gd->$method );
308            
309 0         0 my $newchecksum = $self->Store->add_content_file_mv($tmpfile->stringify);
310            
311 0         0 ($checksum,$width,$height) = ($newchecksum,$newwidth,$newheight);
312 0         0 $resized = \1;
313             }
314             }
315            
316 0         0 return ($checksum,$width,$height,$resized,$orig_width,$orig_height);
317 1     1   941 }
  1         1  
  1         3  
318              
319              
320             # New method, uses the same API as 'add_resize_image' above, but doesn't
321             # do any actual resizing (just calculates smaller height/width for better
322             # display). This method is used when Image::Resize is not available.
323             # Added for RapidApp Github Issue #42
324             sub add_size_info_image :Private {
325 0     0 1 0 my ($self,$file,$type,$subtype,$maxwidth,$maxheight) = @_;
326              
327 0 0       0 my $checksum = $self->Store->add_content_file($file) or die "Failed to add content";
328              
329 0         0 my $shrunk = \0;
330              
331 0         0 my ($width,$height) = $self->Store->image_size($checksum);
332              
333 0         0 my ($orig_width,$orig_height) = ($width,$height);
334 0 0       0 if (defined $maxwidth) {
335            
336 0         0 my ($newwidth,$newheight) = ($width,$height);
337            
338 0 0       0 if($width > $maxwidth) {
339 0         0 my $ratio = $maxwidth/$width;
340 0         0 $newheight = int($ratio * $height);
341 0         0 $newwidth = $maxwidth;
342             }
343            
344 0 0 0     0 if(defined $maxheight and $newheight > $maxheight) {
345 0         0 my $ratio = $maxheight/$newheight;
346 0         0 $newwidth = int($ratio * $newwidth);
347 0         0 $newheight = $maxheight;
348             }
349            
350 0 0 0     0 unless ($newwidth == $width && $newheight == $height) {
351 0         0 ($width,$height) = ($newwidth,$newheight);
352 0         0 $shrunk = \1;
353             }
354             }
355              
356 0         0 return ($checksum,$width,$height,$shrunk,$orig_width,$orig_height);
357 1     1   803 }
  1         1  
  1         3  
358              
359              
360             sub upload_file :Chained('base') :Args {
361 0     0 1 0 my ($self, $c) = @_;
362            
363 0 0       0 my $upload = $c->req->upload('Filedata') or die "no upload object";
364 0 0       0 my $checksum = $self->Store->add_content_file_mv($upload->tempname) or die "Failed to add content";
365 0         0 my $Content = $self->Content($checksum,$upload->filename);
366            
367 0         0 my $packet = {
368             success => \1,
369             filename => $self->safe_filename($upload->filename),
370             checksum => $Content->checksum,
371             mimetype => $Content->mimetype,
372             css_class => $Content->filelink_css_class,
373             };
374            
375 0         0 return $self->_json_response($c, $packet);
376 1     1   727 }
  1         1  
  1         3  
377              
378              
379             sub safe_filename {
380 0     0 1   my $self = shift;
381 0           my $filename = shift;
382            
383 0           my @parts = split(/[\\\/]/,$filename);
384 0           return pop @parts;
385             }
386              
387              
388             sub upload_echo_base64 :Chained('base') :Args {
389 0     0 1   my ($self, $c) = @_;
390              
391 0 0         my $upload = $c->req->upload('Filedata') or die "no upload object";
392            
393 0           my $base64 = encode_base64($upload->slurp,'');
394            
395 0           my $packet = {
396             success => \1,
397             echo_content => $base64
398             };
399            
400 0           return $self->_json_response($c, $packet);
401 1     1   755 }
  1         1  
  1         4  
402              
403              
404             has '_json_view_name', is => 'ro', isa => Maybe[Str], lazy => 1, default => sub {
405             my $self = shift;
406             my $c = $self->_app;
407             my %views = map {$_=>1} $c->views;
408            
409             # If we're in a RapidApp application (or the RapidApp::JSON view is available),
410             # use it. This is needed to do the special embedded iframe encoding when the
411             # RequestContentType => 'text/x-rapidapp-form-response' header is present. This
412             # is set from the RapidApp/ExtJS client when doing uploads for things like 'Insert Image'
413             my $vn = 'RapidApp::JSON';
414            
415             $views{$vn} ? $vn : undef
416             };
417              
418              
419             sub _json_response {
420 0     0     my ($self, $c, $packet) = @_;
421            
422 0           $c->stash->{jsonData} = encode_json($packet);
423            
424 0 0         if(my $vn = $self->_json_view_name) {
425 0 0         my $view = $c->view( $vn ) or die "No such view name '$vn'";
426 0           $c->forward( $view );
427             }
428             else {
429 0           $c->res->content_type('application/json; charset=utf-8');
430 0           $c->res->body( $c->stash->{jsonData} );
431             }
432             }
433              
434             1;
435              
436              
437             __END__
438              
439             =head1 NAME
440              
441             Catalyst::Controller::SimpleCAS - General-purpose content-addressed storage (CAS) for Catalyst
442              
443             =head1 SYNOPSIS
444              
445             use Catalyst::Controller::SimpleCAS;
446             ...
447              
448             See the SYNOPSIS of L<Catalyst::Plugin::SimpleCAS> for the standard use/examples.
449              
450             =head1 DESCRIPTION
451              
452             This controller provides a simple content-addressed storage backend for Catalyst applications. The
453             concept of content-addressed storage ("CAS") is to store arbitrary content in a simple indexed
454             key/value database where the "key" is the SHA1 checksum of the "value". This is the same design
455             and theory used by Git.
456              
457             This module was originally developed for and within L<RapidApp> before being extracted into its
458             own module. This module provides server-side functionality which can be used for any Catalyst
459             application, however, it is up to the developer to write the associated front-end interfaces to
460             consume its API (unless you are using RapidApp to begin with). RapidApp already has a number of
461             built-in features and interfaces which rely on this module for backend storage, including,
462             C<cas_link> (file attachment columns) and C<cas_img> (image columns) column profiles, as well as
463             the ability to insert images and file links directly within rich template content and C<html>
464             columns using WYSIWYG editors.
465              
466             The type of content this module is designed to store are simple files (with some extra handling
467             for images specifically). For the purposes of security, we rely on the assumption that knowing the
468             checksum of the content is equivalent to being authorized to view that content. So the checksums
469             are also considered the authorization tokens to access the data, so keeping the checksums themselves
470             secure is the only way to keep the associated data/content secret. If you understand what this means
471             B<AND> you feel that this is insufficient security, don't use this module (or, extend it and add
472             whatever additional security/authorization/permission checks you feel are necessary)
473              
474             Starting in version 1.000 of this module, L<Catalyst::Plugin::SimpleCAS> is now provided and is the
475             way RapidApp consumes and uses this module, and is the standard way to use this module in any
476             Catalyst application, for most scenarios. The plugin simply injects a single controller instance of
477             C<Catalyst::Controller::SimpleCAS> as 'SimpleCAS' which is all that is needed for most setups. The
478             only reason to use the controller class directly would be if you needed multiple controllers in the
479             same app, or if you wanted to subclass or do something else fancy.
480              
481             The ATTRUBUTES listed below can be configured in your Catalyst config in the normal manner using the
482             C'<Controller::SimpleCAS'> config key (assuming you used L<Catalyst::Plugin::SimpleCAS> with the
483             default C<controller_namespace> of 'SimpleCAS'). No options are required, with the defaults being
484             sufficient in most cases (including the way this module is used by L<RapidApp>).
485              
486             =head1 ATTRIBUTES
487              
488             =head2 store_class
489              
490             Object class to use for the Store backend. Defaults to
491             C<Catalyst::Controller::SimpleCAS::Store::File>
492              
493             =head2 store_path
494              
495             Directory/path to be used by the Store. Defaults to C<cas_store/> within the Catalyst home directory.
496             This is a convenience param to supply to the Store, which becomes C<store_dir> for the default
497             L<Catalyst::Controller::SimpleCAS::Store::File> store class.
498              
499             The rationale behind the name 'store_path' instead of 'store_dir' as it becomes in the default store
500             is the notion that a single "path" argument is all that most Stores need, and different stores may
501             treat this value as something other than a filesystem directory, so it was intentionally given the
502             more ambiguous name. For most users that will use basic/default options, these details aren't important.
503              
504             =head2 store_args
505              
506             Optional options (HashRef) to supply when contructing the Store. This is only needed for custom
507             Stores which need more options beyond store_path.
508              
509             =head2 Store
510              
511             Actual object instance of the Store. By default this object is built using the C<store_class> (by
512             calling C<new()>) with the C<store_path> supplied to the constructor.
513              
514             =head2 _json_view_name
515              
516             Name of an optional Catalyst View to forward to to render JSON responses, with the pre-encoded
517             JSON set in the stash key 'jsonData'. If not set, the encoded JSON is simply set in response body
518             with the Content-Type set to C<application/json>.
519              
520             If the view name C<RapidApp::View> is loaded (which is the case when L<RapidApp> is loaded),
521             it is used as the default. This is needed to support special round-trip encodings for
522             "Insert Image" and other ExtJS-based upload interfaces.
523              
524              
525             =head1 PUBLIC ACTIONS
526              
527             =head2 upload_content
528              
529             Upload new content to the CAS and return the sha1 checksum in the body to be able to access it later.
530             Because of the CAS design, the system automatically deduplicates, and will only ever store
531             a single copy of a given unique piece of content in the Store.
532              
533             =head2 fetch_content
534              
535             Fetch existing content from the CAS according its sha1 checksum.
536              
537             Example:
538              
539             GET /simplecas/fetch_content/fdb379f7e9c8d0a1fcd3b5ee4233d88c5a4a023e
540              
541             The system attempts to identify the content type and sets the MIME type accordingly. Additionally,
542             an optional filename argument can be also be supplied in the URL
543              
544             GET /simplecas/fetch_content/fdb379f7e9c8d0a1fcd3b5ee4233d88c5a4a023e/somefile.txt
545              
546             The main reason this is supported is simply for more human-friendly URLs. The name is not stored
547             or validated in any way. If supplied, this does nothing other than being used to set the
548             content-disposition:
549              
550             Content-Disposition: attachment; filename="somefile.txt"
551              
552             When there is no filename second arg supplied, the content-disposition is set like this:
553              
554             Content-Disposition: inline;filename="fdb379f7e9c8d0a1fcd3b5ee4233d88c5a4a023e"
555              
556             =head2 upload_file
557              
558             Works like C<upload_content>, but returns a JSON packet with additional metadata/information in
559             the body.
560              
561             =head2 upload_image
562              
563             Works like C<upload_file>, but with some image-specific functionality, including client-supplied
564             max width and height values supplied as the first and second args, respectively. For example,
565             a POST I<upload> with I<Filedata> containing an image, and declared max size of 800x600 uses a
566             URL like:
567              
568             POST /simplecas/upload_image/800/600
569              
570             When the image is larger than the max width or height, I<if> the optional dependency
571             L<Image::Resize> is available (which requires L<GD>) it is used to resize the image, preserving
572             height/width proportions accordingly, and the new, resized image is what is stored in the CAS.
573             Otherwise, the image is not resized, but resized dimensions are returned in the JSON packet
574             so the client can generate an C<img> tag for display.
575              
576             Originally, L<Image::Resize> was a standard dependency, but this can be a PITA to get installed
577             with all of the dependencies of L<GD>.
578              
579             =head2 upload_echo_base64
580              
581             This does nothing but accept a standard POST/Filedata upload and return it as base64 in a JSON
582             packet within the JSON/object key C<echo_content>.
583              
584             =head2 base
585              
586             This is the base action of the Catalyst Chain behind this asset controller. So
587             far it still is a fixed position, but we will allow in a later version to set
588             the Chained base to any other action via configuration.
589              
590             You could override specific URLs inside the SimpleCAS with own controllers,
591             you just chain to this base controller, but we would strongly advice to put
592             those outside functionalities next to this controller.
593              
594             =head1 METHODS
595              
596             =head2 fetch
597              
598             Convenience method to fetch the content (as a raw string/scalar) associated with a cas_id string
599             which can be simply be the 40-character checksum by itself, or the checksum with a filename
600             as generated by RapidApp's C<cas_link> and C<cas_img> column profiles.
601              
602             This method is provided as sugar for the purposes of interacting with the CAS from backend
603             scripts/code, rather than via HTTP requests to the controller actions.
604              
605             =head2 fetch_fh
606              
607             Like C<fetch> but returns the content as a filehandle (i.e. L<IO::File>, or whatever IO object
608             the given Store returns).
609              
610             =head2 add
611              
612             Convenience method to add content to the CAS and return the checksum. Content argument can be
613             supplied as a simple Scalar (i.e. raw string/data), a ScalarRef, a filehandle (i.e. an object
614             which derives from L<IO::Handle> or otherwise is an object with an appropriate C<'getlines'>
615             method, or a filesystem path.
616              
617             This method is provided as sugar for the purposes of interacting with the CAS from backend
618             scripts/code, rather than via HTTP requests to the controller actions.
619              
620             =head2 Content
621              
622             Not usually called directly
623              
624             =head2 add_resize_image
625              
626             Not usually called directly
627              
628             =head2 add_size_info_image
629              
630             Not usually called directly
631              
632             =head2 safe_filename
633              
634             Not usually called directly
635              
636             =head2 uri_find_Content
637              
638             Not usually called directly
639              
640             =head2 calculate_checksum
641              
642             =head2 file_checksum
643              
644             =head1 SEE ALSO
645              
646             =over
647              
648             =item *
649              
650             L<Catalyst::Plugin::SimpleCAS>
651              
652             =item *
653              
654             L<Catalyst>
655              
656             =item *
657              
658             L<Catalyst::Controller>
659              
660             =item *
661              
662             L<RapidApp>
663              
664             =back
665              
666             =head1 AUTHOR
667              
668             Henry Van Styn <vanstyn@cpan.org>
669              
670             =head1 COPYRIGHT AND LICENSE
671              
672             This software is copyright (c) 2014 by IntelliTree Solutions llc.
673              
674             This is free software; you can redistribute it and/or modify it under
675             the same terms as the Perl 5 programming language system itself.
676              
677             =cut