RoR – 在rails中上传大文件

我有一个rails webapp,允许用户上传video,它们存储在NFS安装目录中。

当前设置适用于较小的文件,但我也需要支持大文件上传(最高4GB)。 当我尝试上传4gb文件时,它最终会发生,但从UX的角度来看很糟糕:上传开始并根据XHR’进度’事件显示进度,但是在100%之后,仍然需要等待很长时间(5分钟以上) )在服务器响应请求之前。

最初我认为这与将文件从某个临时目录复制到最终的NFS安装目录有关。 但现在我不太确定。 在我的路由添加日志记录后,我发现文件上传进度达到100%与控制器操作中的代码运行之间有大约3分钟的等待时间(在我将文件移动到NAS之前进行任何处理之前) 。

我想知道以下内容:

  • 在上传完成后和我的操作被调用之前的3分钟等待期间发生了什么?
  • 有没有办法让我考虑在此期间发生的任何事情,以便客户端在上传完成后立即获得响应,以便他们不会超时?
  • 如何在Rails中处理大文件上传? 这似乎是一个常见的问题,但我似乎无法找到任何东西。

(注意:当我发现这个问题时,我最初使用CarrierWave进行上传。我删除了它,只是直接在我的模型中使用FileUtils处理文件保存,以确保等待时间不是因为某些CarrierWave魔法发生在场景,但得到完全相同的结果。)

ruby -v:1.9.3p362

rails -v:3.2.11

您可以考虑使用MiniProfiler来更好地了解花费的时间。

需要在后台处理大文件上传。 任何控制器或数据库访问都应该只标记文件已上载,然后排队后台处理作业以移动它,以及可能需要执行的任何其他操作。

http://mattgrande.com/2009/08/11/delayedjob/

那篇文章有它的要点,每个实现都会有所不同。

我终于找到了我的主要问题的答案: 在上传完成后和我的行动被召唤之前的3分钟等待期间发生了什么?

这篇文章中都清楚地解释了这一点: Rails方式 – 上传文件

“当浏览器上传文件时,它会以一种名为’multipart mime’的格式对内容进行编码(它与您发送电子邮件附件时使用的格式相同)。为了让您的应用程序对该文件执行某些操作,rails已经要做到这一点,需要读取庞大的请求体,并将每一行与几个正则表达式相匹配。这可能会非常慢,并且会占用大量的CPU和内存。“

我尝试了post中提到的modporter Apache模块。 唯一的问题是该模块及其相应的插件是在4年前编写的,并且随着他们的网站不再运行,几乎没有任何一个文档。

使用modporter ,我想将我的NFS挂载目录指定为PorterDir,希望它能将文件直接传递给NAS,而无需从临时目录中进行任何额外的复制。 但是,由于模块似乎忽略了我指定的PorterDir,并且返回了与我的操作完全不同的路径,因此无法实现这一点。 最重要的是,它返回的路径甚至都不存在,所以我不知道我的上传实际发生了什么。

我的解决方法

我必须快速解决问题,所以我现在采用了一种有点hacky的解决方案,其中包括编写相应的JavaScript / Ruby代码以处理分块文件上传。

JS示例:

 var MAX_CHUNK_SIZE = 20000000; // in bytes window.FileUploader = function (opts) { var file = opts.file; var url = opts.url; var current_byte = 0; var success_callback = opts.success; var progress_callback = opts.progress; var percent_complete = 0; this.start = this.resume = function () { paused = false; upload(); }; this.pause = function () { paused = true; }; function upload() { var chunk = file.slice(current_byte, current_byte + MAX_CHUNK_SIZE); var fd = new FormData(); fd.append('chunk', chunk); fd.append('filename', file.name); fd.append('total_size', file.size); fd.append('start_byte', current_byte); $.ajax(url, { type: 'post', data: fd, success: function (data) { current_byte = data.next_byte; upload_id = data.upload_id; if (data.path) { success_callback(data.path); } else { percent_complete= Math.round(current_byte / file.size * 100); if (percent_complete> 100) percent_complete = 100; progress_callback(percent_complete); // update some UI element to provide feedback to user upload(); } } }); } }; 

(原谅任何语法错误,只需在我的头顶输入)

在服务器端,我创建了一个接受文件块的新路由。 在第一个chunk提交时,我根据文件名/大小生成upload_id,并确定我是否已经有一个来自中断上传的部分文件。 如果是这样,我将我需要的下一个起始字节与id一起传回。 如果没有,我存储第一个块并传回id。

具有附加块上载的进程附加部分文件,直到文件大小与原始文件大小匹配。 此时,服务器响应文件的临时路径。

然后,javascript从表单中删除文件输入,并将其替换为隐藏输入,其值是从服务器返回的文件路径,然后发布表单。

然后最后服务器端,我处理移动/重命名文件并保存其最终路径到我的模型。

唷。