From 743dcad9f6eef3714b1ff63f06c7844c8d869a54 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 12 Nov 2009 18:36:12 -0800 Subject: add JSON generator It currently only barely works with Rails, but the plan is to add full Rack long-polling/streaming support. --- lib/upr.rb | 1 + lib/upr/json.rb | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 lib/upr/json.rb diff --git a/lib/upr.rb b/lib/upr.rb index 151e3a8..f0cd950 100644 --- a/lib/upr.rb +++ b/lib/upr.rb @@ -10,6 +10,7 @@ module Upr autoload :Monitor, 'upr/monitor' autoload :Params, 'upr/params' autoload :InputWrapper, 'upr/input_wrapper' + autoload :JSON, 'upr/json' # Initializes a new instance of Upr::InputWrapper. Usage in config.ru: # diff --git a/lib/upr/json.rb b/lib/upr/json.rb new file mode 100644 index 0000000..adcf47e --- /dev/null +++ b/lib/upr/json.rb @@ -0,0 +1,136 @@ +# -*- encoding: binary -*- +begin + require 'json' +rescue LoadError + raise LoadError, "either json or json-pure is required" +end + +module Upr + + # JSON protocol based on Lighttpd's mod_uploadprogress + # http://trac.lighttpd.net/trac/wiki/Docs:ModUploadProgress + class JSON < Struct.new(:frequency, :backend, :upload_id) + + include Params + + # We use this in case length is nil when clients send chunked uploads + INT_MAX = 0x7fffffff + + SLEEP_CLASS = defined?(Actor) ? Actor : Kernel + + RESP_HEADERS = { + 'Content-Type' => 'application/json', + 'Cache-Control' => 'no-cache', + } + + def initialize(options = {}) + super(options[:frequency] || 1, options[:backend], options[:upload_id]) + + # support :drb for compatibility with mongrel_upload_progress + if options[:drb] + backend and raise ArgumentError, ":backend and :drb are incompatible" + require 'drb' + DRb.start_service + self.backend = DRbObject.new(nil, options[:drb]) + elsif String === backend + # allow people to use strings in case their backend gets + # lazy-loaded (like an ActiveRecord model) + self.backend = eval(backend) + elsif backend.nil? + raise ArgumentError, "backend MUST be specified" + end + + # only for use with rails_proc + upload_id.nil? and self.upload_id = options[:env] + end + + def rails_render_options + env = upload_id + self.upload_id = extract_upload_id(env) + { + :content_type => 'application/json', + :text => _once + } + end + + def _once + if status = backend.read(upload_id) + if status.done? + _json_object(:state => 'done') + elsif status.seen == 0 + _json_object(:state => 'starting') + elsif status.error? + _error_msg("upload failed") + else + _update_msg(status) + end + else + timeout = Time.now + 2 + until status = backend.read(upload_id) + SLEEP_CLASS.sleep(0.1) + return _error_msg("couldn't get status") if Time.now > timeout + end + _json_object(:state => 'starting') + end + end + + # Rack interface reservced for future use with streaming AJAX + def call(env) + if uid = extract_upload_id(env) + _wrap(env, uid) + else + [ 400, RESP_HEADERS.dup, [ _error_msg("upload_id not given") ] ] + end + end + + # Rack interface reservced for future use with streaming AJAX + def each(&block) + sleeper = defined?(Actor) ? Actor : Kernel + timeout = Time.now + 2 + eol = ";\n" + yield _json_object(:state => 'starting') << eol + begin + until status = backend.read(upload_id) + sleeper.sleep(0.1) + break if Time.now > timeout + end + if status + begin + yield _update_msg(status) << eol + break if status.done? + sleeper.sleep(frequency) + end while status = backend.read(upload_id) + yield _json_object(:state => 'done') << eol + else + yield _error_msg("couldn't get status") << eol + end + rescue => e + yield _error_msg(e.message) << eol + end + end + + # Rack interface reservced for future use with streaming AJAX + def _wrap(env, uid) + _self = dup + _self.upload_id = uid + [ 200, RESP_HEADERS.dup, _self ] + end + + def _error_msg(msg) + _json_object(:state => 'error', :status => 400, :message => msg) + end + + def _json_object(options) + # $stderr.syswrite "#{options.inspect} #{$upr.inspect}\n" + options.to_json + end + + def _update_msg(status) + raise "client error" if status.error? + received = status.seen + size = status.length || INT_MAX + _json_object(:state => 'uploading', :size => size, :received => received) + end + + end +end -- cgit v1.2.3-24-ge0c7