`
mikewang
  • 浏览: 10026 次
  • 性别: Icon_minigender_1
  • 来自: 南京
文章分类
社区版块
存档分类
最新评论

在ror下实现多线程下载和断点续传

阅读更多
ror下文件下载是通过send_file完成的,但是如果使用多线程并支持断点续传的客户端(FlashGet等),send_file是不能正确工作的,原因在于,send_file函数没有对HTTP协议的Range头做相应的支持,并且也不支持HTTP/1.1 206 Partial Content相应


我修改了send_file函数,并做了一个plugin 将起解压到vendor/plugins/就可以了
(必要的地方我都写了注释,欢迎大家提出建议和意见)

module ActionController
  module Streaming
    protected
      def send_file(path, options = {})
        raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)

        options[:length]   ||= File.size(path) # 文件长度
        options[:filename] ||= File.basename(path) # 文件名称

        options[:from] ||= 0 # 首偏移量(从哪里开始下载)
        options[:to] ||= options[:length] # 结束位置

        m_send_file_headers! options # 设置必要的 http 头

        @performed_render = false

        if options[:stream]
          render :status => options[:status], :text => Proc.new { |response, output|
            logger.info "Streaming file #{path}" unless logger.nil?
            len = options[:buffer_size] || 8192 # 原来的实现是4K, 不过APUE 上说,8K 要好一些,所有我调整了一下
            File.open(path, 'rb') do |file|
              file.seek(options[:from].to_i, IO::SEEK_SET) if options[:status] == 206 # 如果是多线程下载,则将流定位到首偏移量位置,从此处开始传输
              while buf = file.read(len)
		# 这个可以优化一下,只要从from 发送到 to 字节就可以了,但我测试过,不进行这个判断是可以的,客户自己计算并保存问下载的起始位置
		# 不过我觉得最好还是要给只发送 from 到 to 字节, 虽然有点麻烦
		# 这个功能以后在实现吧
                output.write(buf)
              end
            end
          }
        else
          logger.info "Sending file #{path}" unless logger.nil?
          File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
        end
      end

    private
      def m_send_file_headers!(options)
        options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
        [:length, :type, :disposition].each do |arg|
          raise ArgumentError, ":#{arg} option required" if options[arg].nil?
        end

        disposition = options[:disposition].dup || 'attachment'

        disposition <<= %(; filename="#{options[:filename]}") if options[:filename]

	# 先输出一些通用的HTTP头
        headers.update(
          'Content-Type'              => options[:type].strip,  # fixes a problem with extra '\r' with some browsers
          'Content-Disposition'       => disposition,
          'Content-Transfer-Encoding' => 'binary'
        )

	# 判断是否存在Range头,并使用正则表达式匹配得到from 和 to
	# 如果匹配成功,则表示客户端使用多线程下载,同时一定要将http status 设置为 206
        if request.env['HTTP_RANGE'] =~ /bytes=(\d+)-(\d*)/ then # 注意后一个\d*,有一些多线程客户端并不完全符合http Range 头的要求(例如FlashGet)
          options[:from] = $1
          options[:to] = $2 unless $2.nil? # 如果客户端不标准,就将 to 设置为文件末尾

	  # 匹配成功,设置status 为 206
          options[:status] = 206

	  # 一下3个http 头仅在多线程下载是有用
          headers['Accept-Ranges'] = 'bytes'
          headers['content-Range'] = "bytes #{options[:from]}-#{options[:to]}/#{options[:length]}" # 格式为 bytes from-to/total
          headers['Content-Length'] = options[:to].to_i - options[:from].to_i + 1 # 注意 在多线程下载下,Content-Length 为传输的实际字节数(从0开始算起,所有要+1)
        else
	  # 非多现场下载
          options[:status] = 200 # 请求正常标志
          headers['Content-Length'] = options[:length] # 非多线程下载下,Content-Length为文件长度
        end

        headers['Cache-Control'] = 'private' if headers['Cache-Control'] == 'no-cache'
      end
  end
end


用法和老send_file一样
class FileController < ApplicationController
  def download
#logger.debug request.env['Range'];
#    request.env.each do |key, value|
#      logger.debug key + '--------' + value
#    end
    send_file 'public/jdk.tgz'
  end
end


分享到:
评论
3 楼 lvflying 2007-09-11  
<p>楼主辛苦了,非常感谢!</p>
2 楼 gigix 2007-09-11  
you should make it a plugin and host it in RubyForge, so that others can install it with "gem install" or "script/plugin install"
1 楼 mikewang 2007-09-11  
附件忘了上传了,补上:)

相关推荐

Global site tag (gtag.js) - Google Analytics