|
| 1 | +# Ruby backend in Sinatra |
| 2 | + |
| 3 | +@rmontgomery429 has provided this sample implementation in ruby. |
| 4 | + |
| 5 | +1. This is constructed here as a modular sinatra app but you app does not necessarily need to be modular. |
| 6 | +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. |
| 7 | +3. I have not tested this specific gist of the app, but we do have a version of this tested and working in production. |
| 8 | +4. This solution does not take into account any kind of file.io race conditions or any other permissions issues. |
| 9 | +5. I provided this as a reference example not as copy/paste production ready code. Your mileage may vary. :) |
| 10 | + |
| 11 | +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. |
| 12 | + |
| 13 | +```ruby |
| 14 | +## |
| 15 | +# Gemfile |
| 16 | +gem 'sinatra', '~> 1.4.5' |
| 17 | +gem 'sinatra-cross_origin', '~> 0.3.1' |
| 18 | + |
| 19 | +## |
| 20 | +# config.ru |
| 21 | +require 'sinatra' |
| 22 | +set :root, File.dirname(__FILE__) |
| 23 | + |
| 24 | +require './flow_app' |
| 25 | +require './flow_controller' |
| 26 | + |
| 27 | +get '/' do |
| 28 | + 404 |
| 29 | +end |
| 30 | + |
| 31 | +run Rack::URLMap.new( |
| 32 | + "/" => Sinatra::Application, |
| 33 | + "/flow" => FlowApp.new, |
| 34 | +) |
| 35 | + |
| 36 | +## |
| 37 | +# flow_app.rb |
| 38 | +class FlowApp < Sinatra::Base |
| 39 | + register Sinatra::CrossOrigin |
| 40 | + |
| 41 | + get "/" do |
| 42 | + cross_origin |
| 43 | + FlowController.new(params).get |
| 44 | + end |
| 45 | + |
| 46 | + post "/" do |
| 47 | + cross_origin |
| 48 | + FlowController.new(params).post! |
| 49 | + end |
| 50 | + |
| 51 | + options "/" do |
| 52 | + cross_origin |
| 53 | + 200 |
| 54 | + end |
| 55 | +end |
| 56 | + |
| 57 | +## |
| 58 | +# flow_controller.rb |
| 59 | +class FlowController |
| 60 | + attr_reader :params |
| 61 | + |
| 62 | + def initialize(params) |
| 63 | + @params = params |
| 64 | + end |
| 65 | + |
| 66 | + def get |
| 67 | + File.exists?(chunk_file_path) ? 200 : 404 |
| 68 | + end |
| 69 | + |
| 70 | + def post! |
| 71 | + save_file! |
| 72 | + combine_file! if last_chunk? |
| 73 | + 200 |
| 74 | + rescue |
| 75 | + 500 |
| 76 | + end |
| 77 | + |
| 78 | +private |
| 79 | + |
| 80 | + ## |
| 81 | + # Move the temporary Sinatra upload to the chunk file location |
| 82 | + def save_file! |
| 83 | + # Ensure required paths exist |
| 84 | + FileUtils.mkpath chunk_file_directory |
| 85 | + # Move the temporary file upload to the temporary chunk file path |
| 86 | + FileUtils.mv params['file'][:tempfile], chunk_file_path, force: true |
| 87 | + end |
| 88 | + |
| 89 | + ## |
| 90 | + # Determine if this is the last chunk based on the chunk number. |
| 91 | + def last_chunk? |
| 92 | + params[:flowChunkNumber].to_i == params[:flowTotalChunks].to_i |
| 93 | + end |
| 94 | + |
| 95 | + ## |
| 96 | + # ./tmp/flow/abc-123/upload.txt.part1 |
| 97 | + def chunk_file_path |
| 98 | + File.join(chunk_file_directory, "#{params[:flowFilename]}.part#{params[:flowChunkNumber]}") |
| 99 | + end |
| 100 | + |
| 101 | + ## |
| 102 | + # ./tmp/flow/abc-123 |
| 103 | + def chunk_file_directory |
| 104 | + File.join "tmp", "flow", params[:flowIdentifier] |
| 105 | + end |
| 106 | + |
| 107 | + ## |
| 108 | + # Build final file |
| 109 | + def combine_file! |
| 110 | + # Ensure required paths exist |
| 111 | + FileUtils.mkpath final_file_directory |
| 112 | + # Open final file in append mode |
| 113 | + File.open(final_file_path, "a") do |f| |
| 114 | + file_chunks.each do |file_chunk_path| |
| 115 | + # Write each chunk to the permanent file |
| 116 | + f.write File.read(file_chunk_path) |
| 117 | + end |
| 118 | + end |
| 119 | + # Cleanup chunk file directory and all chunk files |
| 120 | + FileUtils.rm_rf chunk_file_directory |
| 121 | + end |
| 122 | + |
| 123 | + ## |
| 124 | + # /final/resting/place/upload.txt |
| 125 | + def final_file_path |
| 126 | + File.join final_file_directory, params[:flowFilename] |
| 127 | + end |
| 128 | + |
| 129 | + ## |
| 130 | + # /final/resting/place |
| 131 | + def final_file_directory |
| 132 | + File.join "", "final", "resting", "place" |
| 133 | + end |
| 134 | + |
| 135 | + ## |
| 136 | + # Get all file chunks sorted by cardinality of their part number |
| 137 | + def file_chunks |
| 138 | + Dir["#{chunk_file_directory}/*.part*"].sort_by {|f| f.split(".part")[1].to_i } |
| 139 | + end |
| 140 | +end |
| 141 | +``` |
0 commit comments