package Mojolicious::Plugin::RenderFile;
use Mojo::Base 'Mojolicious::Plugin';

use strict;
use warnings;
use File::Basename;
use Encode qw( encode decode_utf8 );
use Mojo::Util 'quote';

our $VERSION = '0.10';

sub register {
    my ( $self, $app ) = @_;

    $app->helper( 'render_file' => sub {
        my $c        = shift;
        my %args     = @_;

        utf8::decode($args{filename}) if $args{filename} && !utf8::is_utf8($args{filename});
        utf8::decode($args{filepath}) if $args{filepath} && !utf8::is_utf8($args{filepath});

        my $filename            = $args{filename};
        my $status              = $args{status}               || 200;
        my $content_disposition = $args{content_disposition}  || 'attachment';
        my $cleanup             = $args{cleanup} // 0;

        # Content type based on format
        my $content_type;
        $content_type = $c->app->types->type( $args{format} ) if $args{format};
        $content_type ||= 'application/x-download';

        # Create asset
        my $asset;
        if ( my $filepath = $args{filepath} ) {
            unless ( -f $filepath && -r $filepath ) {
                $c->app->log->error("Cannot read file [$filepath]. error [$!]");
                return;
            }

            $filename ||= fileparse($filepath);
            $asset = Mojo::Asset::File->new( path => $filepath );
            $asset->cleanup($cleanup);
        } elsif ( $args{data} ) {
            $filename ||= $c->req->url->path->parts->[-1] || 'download';
            $asset = Mojo::Asset::Memory->new();
            $asset->add_chunk( $args{data} );
        } else {
            $c->app->log->error('You must provide "data" or "filepath" option');
            return;
        }

        # Set response headers
        my $headers = $c->res->content->headers();

        $filename = quote($filename); # quote the filename, per RFC 5987
        $filename = encode("UTF-8", $filename);

        $headers->add( 'Content-Type', $content_type . ';name=' . $filename );
        $headers->add( 'Content-Disposition', $content_disposition . ';filename=' . $filename );

        # Range
        # Partially based on Mojolicious::Static
        if ( my $range = $c->req->headers->range ) {
            my $start = 0;
            my $size  = $asset->size;
            my $end   = $size - 1 >= 0 ? $size - 1 : 0;

            # Check range
            if ( $range =~ m/^bytes=(\d+)-(\d+)?/ && $1 <= $end ) {
                $start = $1;
                $end = $2 if defined $2 && $2 <= $end;

                $status = 206;
                $headers->add( 'Content-Length' => $end - $start + 1 );
                $headers->add( 'Content-Range'  => "bytes $start-$end/$size" );
            } else {
                # Not satisfiable
                return $c->rendered(416);
            }

            # Set range for asset
            $asset->start_range($start)->end_range($end);
        } else {
            $headers->add( 'Content-Length' => $asset->size );
        }

        # Stream content directly from file
        $c->res->content->asset($asset);
        return $c->rendered($status);
    } );
}

1;

=head1 NAME

Mojolicious::Plugin::RenderFile - "render_file" helper for Mojolicious

=head1 SYNOPSIS

    # Mojolicious
    $self->plugin('RenderFile');

    # Mojolicious::Lite
    plugin 'RenderFile';

    # In controller
    $self->render_file('filepath' => '/tmp/files/file.pdf'); # file name will be "file.pdf"

    # Provide any file name
    $self->render_file('filepath' => '/tmp/files/file.pdf', 'filename' => 'report.pdf');

    # Render data from memory as file
    $self->render_file('data' => 'some data here', 'filename' => 'report.pdf');

    # Open file in browser(do not show save dialog)
    $self->render_file(
        'filepath' => '/tmp/files/file.pdf',
        'format'   => 'pdf',                 # will change Content-Type "application/x-download" to "application/pdf"
        'content_disposition' => 'inline',   # will change Content-Disposition from "attachment" to "inline"
        'cleanup'  => 1,                     # delete file after completed
    );

=head1 DESCRIPTION

L<Mojolicious::Plugin::RenderFile> is a L<Mojolicious> plugin that adds "render_file" helper. It does not read file in memory and just streaming it to a client.

=head1 HELPERS

=head2 C<render_file>

    $self->render_file(filepath => '/tmp/files/file.pdf', 'filename' => 'report.pdf' );

With this helper you can easily provide files for download. By default "Content-Type" header is "application/x-download" and "content_disposition" option value is "attachment".
Therefore, a browser will ask where to save file. You can provide "format" option to change "Content-Type" header.


=head3 Supported Options:

=over

=item C<filepath>

Path on the filesystem to the file. You must always pass "filepath" or "data" option

=item C<data>

Binary content which will be transfered to browser. You must always pass "filepath" or "data" option

=item C<filename> (optional)

Browser will use this name for saving the file

=item C<format> (optional)

The "Content-Type" header is based on the MIME type mapping of the "format" option value.  These mappings can be easily extended or changed with L<Mojolicious/"types">.

By default "Content-Type" header is "application/x-download"

=item C<content_disposition> (optional)

Tells browser how to present the file.

"attachment" (default) - is for dowloading

"inline" - is for showing file inline

=item C<cleanup> (optional)

Indicates if the file should be deleted when rendering is complete

=back

This plugin respects HTTP Range headers.

=head1 AUTHOR

Viktor Turskyi <koorchik@cpan.org>

=head1 CONTRIBUTORS

Nils Diewald (Akron)
Danil Greben (SDSWanderer)

=head1 BUGS

Please report any bugs or feature requests to Github L<https://github.com/koorchik/Mojolicious-Plugin-RenderFile>

=head1 SEE ALSO

L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.

Copyright 2011 Viktor Turskyi

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut
