Upload large file with rails

Problem

Chắc hẳn ai đã có lần động đến việc upload video đều phải đối mặt với chuyện khi dung lượng video quá lớn, rất có thể ta sẽ không thể upload mà nhận về 1 lỗi 413(request entity is too large) hoặc request time out. Cách phù hợp nhất mình tìm hiểu được để giải quyết chuyện này là tách nhỏ các file và ghép chúng lại khi quá trình tải hoàn tất. Javascript có FileReader API có thể giúp đỡ ta đọc và tách nhỏ file. mặc dù vậy nó không giúp đỡ thực sự tốt cho các trình duyệt. Rất may là Jquerry có 1 file upload plugin có thể xử lý chuyện này khá tốt

Introducing jQuery File Upload Plugin

https://github.com/blueimp/jQuery-File-Upload

Plugin trên có đầy đủ các chức năng mà ta có thể cần để làm việc với file upload như tải nhiều file, kéo thả, confirm, … Trong bài này ta chỉ quan tâm tới việc giúp đỡ tải các file có dung lượng lớn

Về cơ bản, ta tạo 1 form upload dễ làm để người sử dụng có thể chọn file tải lên và cũng cấp 1 thanh progress bar để khách hàng có thể theo dõi tiến trình upload.

<!-- uploads/new.html.erb --> <%= form_tag uploads_url, multipart: true, id: 'fileupload' do %>   <%= label_tag 'File:' %>   <%= file_field_tag :upload %>   <%= submit_tag 'Upload' %> <% end %>  <div id="progress-bar">   <div id="progress"></div> </div> 

Đây là đoạn code để tạo form upload và progress bar. tiếp nữa sẽ là code javascript để chia tệp thành nhiều phần nhỏ hơn và sẵn sàng cho việc upload. Tưu ý option maxChunkSize, nếu không có nó tệp sẽ được tải lên 1 cách bình thường. Trong callback ta lặp lại với đã có lần file đã chọn(trong hoàn cảnh việc upload nhiều file được kích hoạt) và tạo 1 post request để tạo ra 1 Upload record mới và trả về id cùng với uploaded_size như 1 JSON object.

var files = []; $('#fileupload').fileupload({   dataType: 'json',   url: '/chunk_upload',   maxChunkSize: 1000000,   add: function(e, data) {     $.each(data.files, function(index, file) {       $.ajax({         method: 'post',         dataType: 'json',         url: '/uploads',         data: { filename: file.name },         success: function(res) {           data.formData = res;           files.push(data);         }       });     });   },   // more code }); 

Trong khi tải lên sẽ có progressall callback trả lại. ta có thể dựa vào đó để tính toán thanh progress bar hiển thị cho user

$('#fileupload').fileupload({   // same code as above   progressall: function(e, data) {     var done = parseInt(data.loaded * 100) / data.total     $('#progress').css({ width: done + '%'})   } }); 

sau cùng là 1 đoạn js để tải file lên khi người sử dụng nhấn nút upload.

$('#fileupload').on('submit', function(e) {   e.preventDefault();   if (files.length < 1) return;    $.each(files, function(index, file) {     file.submit();   });    files = []; }); 

Ok, ta đã xong phía client, giờ là xử lý phía server. ta sẽ làm 2 bước, đầu tiên là tạo 1 upload record trong create action để generate 1 uniqe uuid, thứ được dùng như 1 filename để tạo path column cho upload record. Nếu thành công ta nối thêm content đọc từ file chunk vào file với đường dẫn từ path column của upload record, nếu không thì báo lỗi.

class UploadsController < ApplicationController   def create     filename = params[:filename]     uuid = SecureRandom.uuid     ext  = File.extname(filename)     dir  = Rails.root.join('tmp', 'upload').to_s     FileUtils.mkdir_p(dir) unless File.exist?(dir)      @upload = Upload.new(       filename: filename,       path: File.join(dir, "#{uuid}#{ext}")     )      if @upload.save       render json: { id: @upload.id, uploaded_size: @upload.uploaded_size }     else       render json: { error: @upload.errors }     end   end    def chunk_create     file    = params[:upload]     @upload = Upload.find_by(id: params[:id])     @upload.uploaded_size += file.size      if @upload.save       File.open(@upload.path, 'ab') { |f| f.write(file.read) }       render json: { id: @upload.id, uploaded_size: @upload.uploaded_size }     else       render json: { error: @upload.errors }, status: 422     end   end end 

Nguồn viblo.asia