Skip to content
This repository was archived by the owner on Oct 19, 2018. It is now read-only.

4 FileUploader with HyperActions Example

Mitch VanDuyn edited this page Jan 19, 2017 · 1 revision

Invoking Server Side HyperActions From The Client

This example shows how HyperActions can be run on the client or the server. In this case we have two Actions - CreateCloudTempUrl and CopyFromTempUrl - that are authorized to run on the server after being invoked on the client, while the Upload Action is designed to run on the client. However Action invocation is the same regardless of where the Action actually executes.

The policy regulation allow_remote_operation authorizes an Action to be invoked from the client, as long as the provided block returns a truthy value when the request is made. If no block is given true is assumed.

So for example

  allow_remote_operation { acting_user }

would only allow access to these operations if there is a logged in user (i.e. acting_user is non-nil.) While

  allow_remote_operation { acting_user.admin? }

would only allow access if the logged in user is an administrator.

Like other regulations these methods can be grouped into a Policy File to keep authorization rules separate from the Operation's implementation.

class ApplicationPolicy # or perhaps OperationPolicy
  # outside of an Action class definition you need to include the class(es) that you
  # are regulating
  allow_remote_operation(FileLoader::CreateCloudTempUrl, FileLoader::CopyFromTempUrl) { acting_user }
end

**NOTE NOTE NOTE ** Mitch Says: Reading the above makes me realize that we want to do something like this:

allow_remote_operation(FileLoader::RemoteOperations) { acting_user }

In other words set up a constant with the list of classes that are involved. Or perhaps like this:

FileLoader.authorized_for { acting_user } where authorized_for looks about like this:

module FileLoader
  def self.authorized_for(&block)
    CreateCloudTempUrl.allow_remote_operation &block
    CopyFromTempUrl.allow_remote_operation &block
  end
end

Actions Run As Instances

An Action is like a Class method, but unlike a Class Method each invocation it is executed in its own instance. This allows the use of internal helper methods and instance variables to keep the code very clean and tidy, and of course allows several instances to be running at once.

Example

# uploads the HTML file object named file_name directly to cloud Storage
# optional update_progress callback will be sent the progress (0.00-1.00)
module FileUploader
  # e.g. FileUploader::Upload(file: Element['file'], file: Element)
  class Upload < HyperAction
    param :file
    param :file_name
    param update_progress: -> {}, type: Proc

    # How it works:
    # First we get the server to request a temporary cloud file location to be
    # created.  We have SECONDS_TO_UPLOAD seconds to begin the upload.
    # The upload goes directly from the browser to cloud storage so is very
    # fast, and does not load the server.

    # Once the file is uploaded, the server will request a transfer from the
    # temporary location to a permanent location that will typically be
    # configured for read only access.  Again except to begin the transfer this
    # does not effect the server.

    def execute
      CreateCloudTempUrl(file_name: params.file_name).then do |url, uuid|
        HTTP.put(url, params_for_put).then do
          CopyFromTempUrl(file_name: uuid)
        end
      end
    end

    def params_for_put
      xhr = `new XMLHttpRequest();`
      `#{xhr}.upload.addEventListener("progress", #{wrapped_progress_handler}, false);`
      {
        data: params.file,
        cache: false,
        contentType: false,
        processData: false,
        xhr: -> { xhr }
      }
    end

    def wrapped_progress_handler
      lambda do |evt|
        compute_length = `#{evt}.lengthComputable`
        return unless compute_length
        params.update_progress((`#{evt}.loaded / #{evt}.total` * 100).round(2))
      end
    end
  end

  # private server side methods and operations

  def self.fog_config
    @fog_config ||= YAML.load_file("#{Rails.root}/config/fog.yml")[Rails.env]
  end

  def self.config
    @config ||= fog_config.symbolize_keys
  end

  def self.credentials
    @credentials ||= fog_config['credentials'].symbolize_keys
  end

  def self.client
    @client ||= Fog::Storage.new(credentials)
  end

  def directory
    @directory ||= FileUploader.client.directories.get(config[:directory])
  end

  def self.temp_directory
    @temp_directory ||= client.directories.get(config[:temp_directory])
  end

  SECONDS_TO_UPLOAD = 300

  # creates a temporary cloud bucket and returns the url to upload to, and the
  # internal uuid for transfering from temporary to permanent location after the
  # load is complete
  class CreateCloudTempUrl < HyperAction
    param :file_name, type: String

    allow_operation { acting_user }

    def execute
      [url, uuid]
    end

    # execute scope: :server_only, allow_client_access_for: ->() { FileLoader:authorized? }
    #  [url, uuid]
    # end

    # note you have to define an authorized method

    def url
      FileUploader.client.get_object_https_url(
        FileUploader.config[:temp_directory], uuid,
        Time.now.to_i + FileUploader::SECONDS_TO_UPLOAD, method: 'PUT'
      )
    end

    def uuid
      return @uuid if @uuid
      loop do
        @uuid = "#{SecureRandom.hex}#{File.extname(params.file_name)}"
        break unless uuid_exists?
      end
      FileUploader.temp_directory.files.create(
        key: uuid, access_control_allow_origin: '*',
        metadata: { original_file_name: params.file_name }
      )
      @uuid
    end

    def uuid_exists?
      FileUploader.directory.files.get(uuid)
    end
  end

  # copies the file referenced by the uuid param from the temporary
  # upload location to the permanent public accessible cloud location
  class CopyFromTempUrl < HyperAction
    param :uuid

    restricted_server_operation { acting_user }

    execute scope: :server_only, allow_client_access_for: ->() { FileLoader:authorized? }
        FileUploader.client.copy_object(
        FileUploader.config[:temp_directory], params.uuid,
        FileUploader.config[:directory], params.uuid,
        'Content-Type' => temp_file.content_type
      )
      temp_file.destroy
      url
    end

    def temp_file
      @temp_file = FileUploader.temp_directory.files.get(params.uuid)
    end

    def url
      "#{FileUploader.config[:https_cdn]}/#{params.uuid}"
    end
  end
end
Clone this wiki locally