1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
|
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
# = Sinatra::Streaming
#
# Sinatra 1.3 introduced the +stream+ helper. This addon improves the
# streaming API by making the stream object imitate an IO object, turning
# it into a real Deferrable and making the body play nicer with middleware
# unaware of streaming.
#
# == IO-like behavior
#
# This is useful when passing the stream object to a library expecting an
# IO or StringIO object.
#
# get '/' do
# stream do |out|
# out.puts "Hello World!", "How are you?"
# out.write "Written #{out.pos} bytes so far!\n"
# out.putc(65) unless out.closed?
# out.flush
# end
# end
#
# == Better Middleware Handling
#
# Blocks passed to #map! or #map will actually be applied when streaming
# takes place (as you might have suspected, #map! applies modifications
# to the current body, while #map creates a new one):
#
# class StupidMiddleware
# def initialize(app) @app = app end
#
# def call(env)
# status, headers, body = @app.call(env)
# body.map! { |e| e.upcase }
# [status, headers, body]
# end
# end
#
# use StupidMiddleware
#
# get '/' do
# stream do |out|
# out.puts "still"
# sleep 1
# out.puts "streaming"
# end
# end
#
# Even works if #each is used to generate an Enumerator:
#
# def call(env)
# status, headers, body = @app.call(env)
# body = body.each.map { |s| s.upcase }
# [status, headers, body]
# end
#
# Note that both examples violate the Rack specification.
#
# == Setup
#
# In a classic application:
#
# require "sinatra"
# require "sinatra/streaming"
#
# In a modular application:
#
# require "sinatra/base"
# require "sinatra/streaming"
#
# class MyApp < Sinatra::Base
# helpers Sinatra::Streaming
# end
module Streaming
def stream(*)
stream = super
stream.extend Stream
stream.app = self
env['async.close'].callback { stream.close } if env.key? 'async.close'
stream
end
module Stream
attr_accessor :app, :lineno, :pos, :transformer, :closed
alias tell pos
alias closed? closed
def self.extended(obj)
obj.closed = false
obj.lineno = 0
obj.pos = 0
obj.callback { obj.closed = true }
obj.errback { obj.closed = true }
end
def <<(data)
raise IOError, 'not opened for writing' if closed?
@transformer ||= nil
data = data.to_s
data = @transformer[data] if @transformer
@pos += data.bytesize
super(data)
end
def each
# that way body.each.map { ... } works
return self unless block_given?
super
end
def map(&block)
# dup would not copy the mixin
clone.map!(&block)
end
def map!(&block)
@transformer ||= nil
if @transformer
inner = @transformer
outer = block
block = proc { |value| outer[inner[value]] }
end
@transformer = block
self
end
def write(data)
self << data
data.to_s.bytesize
end
alias syswrite write
alias write_nonblock write
def print(*args)
args.each { |arg| self << arg }
nil
end
def printf(format, *args)
print(format.to_s % args)
end
def putc(c)
print c.chr
end
def puts(*args)
args.each { |arg| self << "#{arg}\n" }
nil
end
def close_read
raise IOError, 'closing non-duplex IO for reading'
end
def closed_read?
true
end
def closed_write?
closed?
end
def external_encoding
Encoding.find settings.default_encoding
rescue NameError
settings.default_encoding
end
def settings
app.settings
end
def rewind
@pos = @lineno = 0
end
def not_open_for_reading(*)
raise IOError, 'not opened for reading'
end
alias bytes not_open_for_reading
alias eof? not_open_for_reading
alias eof not_open_for_reading
alias getbyte not_open_for_reading
alias getc not_open_for_reading
alias gets not_open_for_reading
alias read not_open_for_reading
alias read_nonblock not_open_for_reading
alias readbyte not_open_for_reading
alias readchar not_open_for_reading
alias readline not_open_for_reading
alias readlines not_open_for_reading
alias readpartial not_open_for_reading
alias sysread not_open_for_reading
alias ungetbyte not_open_for_reading
alias ungetc not_open_for_reading
private :not_open_for_reading
def enum_not_open_for_reading(*)
not_open_for_reading if block_given?
enum_for(:not_open_for_reading)
end
alias chars enum_not_open_for_reading
alias each_line enum_not_open_for_reading
alias each_byte enum_not_open_for_reading
alias each_char enum_not_open_for_reading
alias lines enum_not_open_for_reading
undef enum_not_open_for_reading
def dummy(*) end
alias flush dummy
alias fsync dummy
alias internal_encoding dummy
alias pid dummy
undef dummy
def seek(*)
0
end
alias sysseek seek
def sync
true
end
def tty?
false
end
alias isatty tty?
end
end
helpers Streaming
end
|