fiedl/your_platform

View on GitHub
app/controllers/attachment_downloads_controller.rb

Summary

Maintainability
A
35 mins
Test Coverage
class AttachmentDownloadsController < ActionController::Base
  include CurrentUser
  include CheckAuthorization
  layout false

  expose :attachment
  expose :version, -> {
    # This method secures the version parameter from a DoS attack.
    # See: http://brakemanscanner.org/docs/warning_types/denial_of_service/
    AttachmentUploader.valid_versions.detect { |v| v.to_s == params[:version] }
  }
  expose :file_path, -> {
    if version
      attachment.file.versions[version].current_path
    else
      attachment.file.current_path
    end
  }
  expose :content_type, -> {
    if version
      attachment.file.versions[version].content_type
    else
      attachment.content_type
    end
  }

  # GET "/attachments/:id(/:version)/*basename.:extension"
  #
  # This action allows to download a file, which is not in the public/ directory
  # but at a secured location. That way, access control for uploaded files cannot
  # be circumvented by downloading files directly from the public folder.
  #
  # https://github.com/carrierwaveuploader/carrierwave/wiki/How-To%3A-Secure-Upload
  #
  def show
    # Thumbnails should be authorized quicky to avoid delay.
    if version.to_s == 'thumb'
      authorize! :download_thumb, attachment
    else
      authorize! :download, attachment
    end

    # I'm not sure why this has to be set manually. Just passing the `type`
    # to `send_file` stopped working with rails 5.
    #
    # TODO: Re-check when updating to rails 5.1.
    # In order to test, just open a pdf attachment in development
    # after commenting out this line:
    #
    response.headers["Content-Type"] = content_type

    send_file file_path, x_sendfile: true, disposition: 'inline',
        range: (attachment.video?), type: content_type
  end


  private

  # Only log the request if it's a regular file, no thumbnail.
  #
  def log_request
    super unless params[:version].in? %w(thumb medium)
  end

  # Do not log the download as an administrative activity.
  #
  def log_activity
  end

  # We need to modify `send_file` to allow streaming.
  #
  def send_file(path, options = {})
    if options[:range]
      send_file_with_range(path, options)
    else
      super(path, options)
    end
  end

  # To stream videos, we have to handle the requested byte range.
  #
  # For a summary, see http://stackoverflow.com/a/37570158/2066546.
  #
  # In order to find out the correct headers, we've put a test-video.mp4 into the public folder
  # and inspected the requests. For example, the first requests results in a 200 to check if the
  # video is actually there. Then, the byte range is requested and the corresponding data is sent.
  #
  # Sending the correct data and extracting the byte range from the request header
  # is taken from http://stackoverflow.com/q/22581727/2066546.
  #
  # Possible alternative, but did not work with Rails 4.2:
  # https://github.com/adamcooke/send_file_with_range
  #
  def send_file_with_range(path, options = {})
    if File.exist?(path)
      size = File.size(path)
      if !request.headers["Range"]
        status_code = 200 # 200 OK
        offset = 0
        length = File.size(path)
      else
        status_code = 206 # 206 Partial Content
        bytes = Rack::Utils.byte_ranges(request.headers, size)[0]
        offset = bytes.begin
        length = bytes.end - bytes.begin
      end
      response.header["Accept-Ranges"] = "bytes"
      response.header["Content-Range"] = "bytes #{bytes.begin}-#{bytes.end}/#{size}" if bytes

      send_data IO.binread(path, length, offset), options
    else
      raise ActionController::MissingFile, "Cannot read file #{path}."
    end
  end

end