-
Notifications
You must be signed in to change notification settings - Fork 18
4 FileUploader with HyperActions Example
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
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.
# 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