flowjs/flow.js

View on GitHub
samples/Ruby backend in Sinatra.md

Summary

Maintainability
Test Coverage
# Ruby backend in Sinatra

@rmontgomery429 has provided this sample implementation in ruby.

1. This is constructed here as a modular sinatra app but you app does not necessarily need to be modular.
2. I've included the use of the sinatra-cross_origin gem which we required for our use case. Your use case may be different and this may not be required.
3. I have not tested this specific gist of the app, but we do have a version of this tested and working in production.
4. This solution does not take into account any kind of file.io race conditions or any other permissions issues. 
5. I provided this as a reference example not as copy/paste production ready code. Your mileage may vary. :)

The basic idea is that you capture chunks of files, save them as part1, part2, partN, and when you've recieved all the files you combine them into the final single file.

```ruby
##
# Gemfile
gem 'sinatra', '~> 1.4.5'
gem 'sinatra-cross_origin', '~> 0.3.1'

##
# config.ru
require 'sinatra'
set :root, File.dirname(__FILE__)

require './flow_app'
require './flow_controller'

get '/' do
  404
end

run Rack::URLMap.new(
  "/" => Sinatra::Application,
  "/flow" => FlowApp.new,
)

##
# flow_app.rb
class FlowApp < Sinatra::Base
  register Sinatra::CrossOrigin

  get "/" do
    cross_origin
    FlowController.new(params).get
  end

  post "/" do
    cross_origin
    FlowController.new(params).post!
  end

  options "/" do
    cross_origin
    200
  end
end

##
# flow_controller.rb
class FlowController
  attr_reader :params

  def initialize(params)
    @params = params
  end

  def get
    File.exists?(chunk_file_path) ? 200 : 204
  end

  def post!
    save_file!
    combine_file! if last_chunk?
    200
  rescue
    500
  end

private

  ##
  # Move the temporary Sinatra upload to the chunk file location
  def save_file!
    # Ensure required paths exist
    FileUtils.mkpath chunk_file_directory
    # Move the temporary file upload to the temporary chunk file path
    FileUtils.mv params['file'][:tempfile], chunk_file_path, force: true
  end

  ##
  # Determine if this is the last chunk based on the chunk number.
  def last_chunk?
    params[:flowChunkNumber].to_i == params[:flowTotalChunks].to_i
  end

  ##
  # ./tmp/flow/abc-123/upload.txt.part1
  def chunk_file_path
    File.join(chunk_file_directory, "#{params[:flowFilename]}.part#{params[:flowChunkNumber]}")
  end

  ##
  # ./tmp/flow/abc-123
  def chunk_file_directory
    File.join "tmp", "flow", params[:flowIdentifier]
  end

  ##
  # Build final file
  def combine_file!
    # Ensure required paths exist
    FileUtils.mkpath final_file_directory
    # Open final file in append mode
    File.open(final_file_path, "a") do |f|
      file_chunks.each do |file_chunk_path|
        # Write each chunk to the permanent file
        f.write File.read(file_chunk_path)
      end
    end
    # Cleanup chunk file directory and all chunk files
    FileUtils.rm_rf chunk_file_directory
  end

  ##
  # /final/resting/place/upload.txt
  def final_file_path
    File.join final_file_directory, params[:flowFilename]
  end

  ##
  # /final/resting/place
  def final_file_directory
    File.join "", "final", "resting", "place"
  end

  ##
  # Get all file chunks sorted by cardinality of their part number
  def file_chunks
    Dir["#{chunk_file_directory}/*.part*"].sort_by {|f| f.split(".part")[1].to_i }
  end
end
```