package Mojo::Asset::File; use Mojo::Base 'Mojo::Asset'; use Carp 'croak'; use Errno; use Fcntl; use File::Copy (); use File::Spec; use IO::File; use Mojo::Util 'md5_sum'; has [qw/cleanup path/]; has handle => sub { my $self = shift; # Already got a file without handle my $handle = IO::File->new; my $file = $self->path; if ($file && -f $file) { $handle->open("< $file") or croak qq/Can't open file "$file": $!/; return $handle; } # Open existing or temporary file my $base = File::Spec->catfile($self->tmpdir, 'mojo.tmp'); my $name = $file || $base; my $fh; until (sysopen $fh, $name, O_CREAT | O_EXCL | O_RDWR) { croak qq/Can't open file "$name": $!/ if $file || $! != $!{EEXIST}; $name = "$base." . md5_sum(time . $$ . rand 9999999); } $file = $name; $self->path($file); # Enable automatic cleanup $self->cleanup(1); # Open for read/write access $handle->fdopen(fileno($fh), "+>") or croak qq/Can't open file "$name": $!/; return $handle; }; has tmpdir => sub { $ENV{MOJO_TMPDIR} || File::Spec->tmpdir }; # "The only monster here is the gambling monster that has enslaved your # mother! # I call him Gamblor, and it's time to snatch your mother from his neon # claws!" sub DESTROY { my $self = shift; my $path = $self->path; if ($self->cleanup && -f $path) { close $self->handle; unlink $path; } } sub add_chunk { my ($self, $chunk) = @_; # Seek to end $self->handle->sysseek(0, SEEK_END); # Append to file $chunk //= ''; $self->handle->syswrite($chunk, length $chunk); return $self; } sub contains { my ($self, $pattern) = @_; # Seek to start $self->handle->sysseek($self->start_range, SEEK_SET); my $end = $self->end_range // $self->size; my $window_size = length($pattern) * 2; $window_size = $end - $self->start_range if $window_size > $end - $self->start_range; # Read my $read = $self->handle->sysread(my $window, $window_size); my $offset = $read; my $pattern_size = length($pattern); # Moving window search my $range = $self->end_range; while ($offset <= $end) { if (defined $range) { $pattern_size = $end + 1 - $offset; return -1 if $pattern_size <= 0; } $read = $self->handle->sysread(my $buffer, $pattern_size); $offset += $read; $window .= $buffer; my $pos = index $window, $pattern; return $pos if $pos >= 0; return -1 if $read == 0; substr $window, 0, $read, ''; } return -1; } sub get_chunk { my ($self, $start) = @_; # Seek to start $start += $self->start_range; $self->handle->sysseek($start, SEEK_SET); my $end = $self->end_range; my $buffer; # Chunk size my $size = $ENV{MOJO_CHUNK_SIZE} || 131072; # Range support if (defined $end) { my $chunk = $end + 1 - $start; return '' if $chunk <= 0; $chunk = $size if $chunk > $size; $self->handle->sysread($buffer, $chunk); } else { $self->handle->sysread($buffer, $size) } return $buffer; } sub is_file {1} sub move_to { my ($self, $path) = @_; # Windows requires that the handle is closed close $self->handle; delete $self->{handle}; # Move my $src = $self->path; File::Copy::move($src, $path) or croak qq/Can't move file "$src" to "$path": $!/; $self->path($path); # Don't clean up a moved file $self->cleanup(0); return $self; } sub size { my $self = shift; # File size my $file = $self->path; return -s $file if $file; return 0; } sub slurp { my $self = shift; # Seek to start $self->handle->sysseek(0, SEEK_SET); # Slurp my $content = ''; while ($self->handle->sysread(my $buffer, 131072)) { $content .= $buffer } return $content; } 1; __END__ =head1 NAME Mojo::Asset::File - File asset =head1 SYNOPSIS use Mojo::Asset::File; my $file = Mojo::Asset::File->new; $file->add_chunk('foo bar baz'); say $file->slurp; my $file = Mojo::Asset::File->new(path => '/foo/bar/baz.txt'); say $file->slurp; =head1 DESCRIPTION L<Mojo::Asset::File> is a container for file assets. =head1 ATTRIBUTES L<Mojo::Asset::File> inherits all attributes from L<Mojo::Asset> and implements the following new ones. =head2 C<cleanup> my $cleanup = $file->cleanup; $file = $file->cleanup(1); Delete file automatically once it's not used anymore. =head2 C<handle> my $handle = $file->handle; $file = $file->handle(IO::File->new); Actual file handle. =head2 C<path> my $path = $file->path; $file = $file->path('/foo/bar/baz.txt'); Actual file path. =head2 C<tmpdir> my $tmpdir = $file->tmpdir; $file = $file->tmpdir('/tmp'); Temporary directory. =head1 METHODS L<Mojo::Asset::File> inherits all methods from L<Mojo::Asset> and implements the following new ones. =head2 C<add_chunk> $file = $file->add_chunk('foo bar baz'); Add chunk of data. =head2 C<contains> my $position = $file->contains('bar'); Check if asset contains a specific string. =head2 C<get_chunk> my $chunk = $file->get_chunk($start); Get chunk of data starting from a specific position. =head2 C<is_file> my $true = $file->is_file; True. =head2 C<move_to> $file = $file->move_to('/foo/bar/baz.txt'); Move asset data into a specific file. =head2 C<size> my $size = $file->size; Size of asset data in bytes. =head2 C<slurp> my $string = $file->slurp; Read all asset data at once. =head1 SEE ALSO L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>. =cut