about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-11-12 18:36:12 -0800
committerEric Wong <normalperson@yhbt.net>2009-11-12 18:36:12 -0800
commit743dcad9f6eef3714b1ff63f06c7844c8d869a54 (patch)
treeeeb1c2efe976f81b69cb78d1754d8c77b0dd29a3
parent726d7214083631f384470ef4751328f70afdbd03 (diff)
downloadupr-743dcad9f6eef3714b1ff63f06c7844c8d869a54.tar.gz
add JSON generator
It currently only barely works with Rails, but the plan is to
add full Rack long-polling/streaming support.
-rw-r--r--lib/upr.rb1
-rw-r--r--lib/upr/json.rb136
2 files changed, 137 insertions, 0 deletions
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