tina4ruby 3.11.13 → 3.11.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -80
  3. data/LICENSE.txt +21 -21
  4. data/README.md +137 -137
  5. data/exe/tina4ruby +5 -5
  6. data/lib/tina4/ai.rb +696 -696
  7. data/lib/tina4/api.rb +189 -189
  8. data/lib/tina4/auth.rb +305 -305
  9. data/lib/tina4/auto_crud.rb +244 -244
  10. data/lib/tina4/cache.rb +154 -154
  11. data/lib/tina4/cli.rb +1449 -1449
  12. data/lib/tina4/constants.rb +46 -46
  13. data/lib/tina4/container.rb +74 -74
  14. data/lib/tina4/cors.rb +74 -74
  15. data/lib/tina4/crud.rb +692 -692
  16. data/lib/tina4/database/sqlite3_adapter.rb +165 -165
  17. data/lib/tina4/database.rb +625 -625
  18. data/lib/tina4/database_result.rb +208 -208
  19. data/lib/tina4/debug.rb +8 -8
  20. data/lib/tina4/dev.rb +14 -14
  21. data/lib/tina4/dev_admin.rb +935 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -110
  24. data/lib/tina4/drivers/mongodb_driver.rb +561 -561
  25. data/lib/tina4/drivers/mssql_driver.rb +112 -112
  26. data/lib/tina4/drivers/mysql_driver.rb +90 -90
  27. data/lib/tina4/drivers/odbc_driver.rb +191 -191
  28. data/lib/tina4/drivers/postgres_driver.rb +116 -106
  29. data/lib/tina4/drivers/sqlite_driver.rb +122 -122
  30. data/lib/tina4/env.rb +95 -95
  31. data/lib/tina4/error_overlay.rb +252 -252
  32. data/lib/tina4/events.rb +109 -109
  33. data/lib/tina4/field_types.rb +154 -154
  34. data/lib/tina4/frond.rb +2025 -2025
  35. data/lib/tina4/gallery/auth/meta.json +1 -1
  36. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
  37. data/lib/tina4/gallery/database/meta.json +1 -1
  38. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
  39. data/lib/tina4/gallery/error-overlay/meta.json +1 -1
  40. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
  41. data/lib/tina4/gallery/orm/meta.json +1 -1
  42. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
  43. data/lib/tina4/gallery/queue/meta.json +1 -1
  44. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
  45. data/lib/tina4/gallery/rest-api/meta.json +1 -1
  46. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
  47. data/lib/tina4/gallery/templates/meta.json +1 -1
  48. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
  49. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
  50. data/lib/tina4/graphql.rb +966 -966
  51. data/lib/tina4/health.rb +39 -39
  52. data/lib/tina4/html_element.rb +170 -170
  53. data/lib/tina4/job.rb +80 -80
  54. data/lib/tina4/localization.rb +168 -168
  55. data/lib/tina4/log.rb +203 -203
  56. data/lib/tina4/mcp.rb +696 -696
  57. data/lib/tina4/messenger.rb +587 -587
  58. data/lib/tina4/metrics.rb +793 -793
  59. data/lib/tina4/middleware.rb +445 -445
  60. data/lib/tina4/migration.rb +451 -451
  61. data/lib/tina4/orm.rb +790 -790
  62. data/lib/tina4/public/css/tina4.css +2463 -2463
  63. data/lib/tina4/public/css/tina4.min.css +1 -1
  64. data/lib/tina4/public/images/logo.svg +5 -5
  65. data/lib/tina4/public/js/frond.min.js +2 -2
  66. data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
  67. data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
  68. data/lib/tina4/public/js/tina4.min.js +92 -92
  69. data/lib/tina4/public/js/tina4js.min.js +48 -48
  70. data/lib/tina4/public/swagger/index.html +90 -90
  71. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  72. data/lib/tina4/query_builder.rb +380 -380
  73. data/lib/tina4/queue.rb +366 -366
  74. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  75. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  76. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  77. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  78. data/lib/tina4/rack_app.rb +817 -817
  79. data/lib/tina4/rate_limiter.rb +130 -130
  80. data/lib/tina4/request.rb +268 -255
  81. data/lib/tina4/response.rb +346 -346
  82. data/lib/tina4/response_cache.rb +551 -551
  83. data/lib/tina4/router.rb +406 -406
  84. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  85. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  86. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  87. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  88. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  89. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  90. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  91. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  92. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  93. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  94. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  95. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  96. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  97. data/lib/tina4/scss/tina4css/base.scss +1 -1
  98. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  99. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  100. data/lib/tina4/scss_compiler.rb +178 -178
  101. data/lib/tina4/seeder.rb +567 -567
  102. data/lib/tina4/service_runner.rb +303 -303
  103. data/lib/tina4/session.rb +297 -297
  104. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  105. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  106. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  107. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  108. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  109. data/lib/tina4/shutdown.rb +84 -84
  110. data/lib/tina4/sql_translation.rb +158 -158
  111. data/lib/tina4/swagger.rb +124 -124
  112. data/lib/tina4/template.rb +894 -894
  113. data/lib/tina4/templates/base.twig +26 -26
  114. data/lib/tina4/templates/errors/302.twig +14 -14
  115. data/lib/tina4/templates/errors/401.twig +9 -9
  116. data/lib/tina4/templates/errors/403.twig +29 -29
  117. data/lib/tina4/templates/errors/404.twig +29 -29
  118. data/lib/tina4/templates/errors/500.twig +38 -38
  119. data/lib/tina4/templates/errors/502.twig +9 -9
  120. data/lib/tina4/templates/errors/503.twig +12 -12
  121. data/lib/tina4/templates/errors/base.twig +37 -37
  122. data/lib/tina4/test_client.rb +159 -159
  123. data/lib/tina4/testing.rb +340 -340
  124. data/lib/tina4/validator.rb +174 -174
  125. data/lib/tina4/version.rb +1 -1
  126. data/lib/tina4/webserver.rb +312 -312
  127. data/lib/tina4/websocket.rb +343 -343
  128. data/lib/tina4/websocket_backplane.rb +190 -190
  129. data/lib/tina4/wsdl.rb +564 -564
  130. data/lib/tina4.rb +458 -458
  131. data/lib/tina4ruby.rb +4 -4
  132. metadata +3 -3
data/lib/tina4/request.rb CHANGED
@@ -1,255 +1,268 @@
1
- # frozen_string_literal: true
2
- require "uri"
3
- require "json"
4
-
5
- module Tina4
6
- # A Hash subclass that supports indifferent access (both string and symbol keys).
7
- # Used by Request#params so that params[:id] and params["id"] both work.
8
- class IndifferentHash < Hash
9
- def [](key)
10
- super(convert_key(key))
11
- end
12
-
13
- def []=(key, value)
14
- super(convert_key(key), value)
15
- end
16
-
17
- def fetch(key, *args, &block)
18
- super(convert_key(key), *args, &block)
19
- end
20
-
21
- def key?(key)
22
- super(convert_key(key))
23
- end
24
- alias has_key? key?
25
- alias include? key?
26
-
27
- def delete(key, &block)
28
- super(convert_key(key), &block)
29
- end
30
-
31
- private
32
-
33
- def convert_key(key)
34
- key.is_a?(Symbol) ? key.to_s : key
35
- end
36
- end
37
-
38
- class Request
39
- attr_reader :env, :method, :path, :query_string, :content_type,
40
- :path_params, :ip
41
- attr_accessor :user
42
-
43
- # Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.
44
- TINA4_MAX_UPLOAD_SIZE = Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))
45
-
46
- class PayloadTooLarge < StandardError; end
47
-
48
- def initialize(env, path_params = {})
49
- @env = env
50
- @method = env["REQUEST_METHOD"]
51
- @path = env["PATH_INFO"] || "/"
52
- @query_string = env["QUERY_STRING"] || ""
53
- @content_type = env["CONTENT_TYPE"] || ""
54
- @path_params = path_params
55
-
56
- # Check upload size limit
57
- content_length = (env["CONTENT_LENGTH"] || 0).to_i
58
- if content_length > TINA4_MAX_UPLOAD_SIZE
59
- raise PayloadTooLarge,
60
- "Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
61
- end
62
-
63
- # Client IP with X-Forwarded-For support
64
- @ip = extract_client_ip
65
-
66
- # Lazy-initialized fields (nil = not yet computed)
67
- @headers = nil
68
- @cookies = nil
69
- @session = nil
70
- @body_raw = nil
71
- @params = nil
72
- @files = nil
73
- @json_body = nil
74
- @query_hash = nil
75
- @body_parsed = nil
76
- end
77
-
78
- # Full URL reconstruction
79
- def url
80
- scheme = env["rack.url_scheme"] || "http"
81
- host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
82
- port = env["SERVER_PORT"]
83
- url_str = "#{scheme}://#{host}"
84
- url_str += ":#{port}" if port && port != "80" && port != "443"
85
- url_str += @path
86
- url_str += "?#{@query_string}" unless @query_string.empty?
87
- url_str
88
- end
89
-
90
- # Lazy accessors
91
- def headers
92
- @headers ||= extract_headers
93
- end
94
-
95
- def cookies
96
- @cookies ||= parse_cookies
97
- end
98
-
99
- def session
100
- @session ||= Tina4::Session.new(@env)
101
- end
102
-
103
- # Raw body string
104
- def body
105
- @body_raw ||= read_body
106
- end
107
-
108
- # Parsed body (JSON or form data)
109
- def body_parsed
110
- @body_parsed ||= parse_body
111
- end
112
-
113
- # Parsed query string as hash
114
- def query
115
- @query_hash ||= parse_query_to_hash(@query_string)
116
- end
117
-
118
- def files
119
- @files ||= extract_files
120
- end
121
-
122
- # Merged params: query + body + path_params (path_params highest priority)
123
- # Supports both string and symbol key access (indifferent access).
124
- def params
125
- @params ||= build_params
126
- end
127
-
128
- # Look up a param by symbol or string key (indifferent access shortcut).
129
- def param(key, default = nil)
130
- params[key.to_s] || params[key.to_sym] || default
131
- end
132
-
133
- def [](key)
134
- params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
135
- end
136
-
137
- def header(name)
138
- headers[name.to_s.downcase.gsub("-", "_")]
139
- end
140
-
141
- def json_body
142
- @json_body ||= begin
143
- JSON.parse(body)
144
- rescue JSON::ParserError, TypeError
145
- {}
146
- end
147
- end
148
-
149
- def bearer_token
150
- auth = header("authorization") || ""
151
- auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
152
- end
153
-
154
- private
155
-
156
- def extract_client_ip
157
- # Check X-Forwarded-For first (proxy/load balancer)
158
- forwarded = @env["HTTP_X_FORWARDED_FOR"]
159
- if forwarded && !forwarded.empty?
160
- # Take the first (original client) IP
161
- forwarded.split(",").first.strip
162
- else
163
- @env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
164
- end
165
- end
166
-
167
- def extract_headers
168
- h = {}
169
- @env.each do |key, value|
170
- if key.start_with?("HTTP_")
171
- h[key[5..-1].downcase] = value
172
- end
173
- end
174
- h
175
- end
176
-
177
- def parse_cookies
178
- cookie_str = @env["HTTP_COOKIE"]
179
- return {} unless cookie_str && !cookie_str.empty?
180
-
181
- result = {}
182
- cookie_str.split(";").each do |pair|
183
- key, value = pair.strip.split("=", 2)
184
- result[key] = value if key
185
- end
186
- result
187
- end
188
-
189
- def read_body
190
- input = @env["rack.input"]
191
- return "" unless input
192
- input.rewind if input.respond_to?(:rewind)
193
- data = input.read || ""
194
- input.rewind if input.respond_to?(:rewind)
195
- data
196
- end
197
-
198
- def parse_body
199
- if @content_type.include?("application/json")
200
- json_body
201
- elsif @content_type.include?("application/x-www-form-urlencoded")
202
- parse_query_to_hash(body)
203
- else
204
- {}
205
- end
206
- end
207
-
208
- def build_params
209
- p = IndifferentHash.new
210
-
211
- # Query string params
212
- query.each { |k, v| p[k.to_s] = v }
213
-
214
- # Body params
215
- body_parsed.each { |k, v| p[k.to_s] = v }
216
-
217
- # Path params (highest priority)
218
- @path_params.each { |k, v| p[k.to_s] = v }
219
- p
220
- end
221
-
222
- def parse_query_to_hash(qs)
223
- result = {}
224
- return result if qs.nil? || qs.empty?
225
- qs.split("&").each do |pair|
226
- key, value = pair.split("=", 2)
227
- result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
228
- end
229
- result
230
- end
231
-
232
- def extract_files
233
- result = {}
234
- return result unless @content_type.include?("multipart/form-data")
235
- begin
236
- form_hash = @env["rack.request.form_hash"]
237
- if form_hash
238
- form_hash.each do |key, value|
239
- if value.is_a?(Hash) && value[:tempfile]
240
- result[key] = {
241
- filename: value[:filename],
242
- type: value[:type],
243
- tempfile: value[:tempfile],
244
- size: value[:tempfile].size
245
- }
246
- end
247
- end
248
- end
249
- rescue StandardError
250
- # Multipart parsing failed
251
- end
252
- result
253
- end
254
- end
255
- end
1
+ # frozen_string_literal: true
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Tina4
6
+ # A Hash subclass that supports indifferent access (both string and symbol keys).
7
+ # Used by Request#params so that params[:id] and params["id"] both work.
8
+ class IndifferentHash < Hash
9
+ def [](key)
10
+ super(convert_key(key))
11
+ end
12
+
13
+ def []=(key, value)
14
+ super(convert_key(key), value)
15
+ end
16
+
17
+ def fetch(key, *args, &block)
18
+ super(convert_key(key), *args, &block)
19
+ end
20
+
21
+ def key?(key)
22
+ super(convert_key(key))
23
+ end
24
+ alias has_key? key?
25
+ alias include? key?
26
+
27
+ def delete(key, &block)
28
+ super(convert_key(key), &block)
29
+ end
30
+
31
+ private
32
+
33
+ def convert_key(key)
34
+ key.is_a?(Symbol) ? key.to_s : key
35
+ end
36
+ end
37
+
38
+ class Request
39
+ attr_reader :env, :method, :path, :query_string, :content_type,
40
+ :path_params, :ip
41
+ attr_accessor :user
42
+
43
+ # Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.
44
+ TINA4_MAX_UPLOAD_SIZE = Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))
45
+
46
+ class PayloadTooLarge < StandardError; end
47
+
48
+ def initialize(env, path_params = {})
49
+ @env = env
50
+ @method = env["REQUEST_METHOD"]
51
+ @path = env["PATH_INFO"] || "/"
52
+ @query_string = env["QUERY_STRING"] || ""
53
+ @content_type = env["CONTENT_TYPE"] || ""
54
+ @path_params = path_params
55
+
56
+ # Check upload size limit
57
+ content_length = (env["CONTENT_LENGTH"] || 0).to_i
58
+ if content_length > TINA4_MAX_UPLOAD_SIZE
59
+ raise PayloadTooLarge,
60
+ "Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
61
+ end
62
+
63
+ # Client IP with X-Forwarded-For support
64
+ @ip = extract_client_ip
65
+
66
+ # Lazy-initialized fields (nil = not yet computed)
67
+ @headers = nil
68
+ @cookies = nil
69
+ @session = nil
70
+ @body_raw = nil
71
+ @params = nil
72
+ @files = nil
73
+ @json_body = nil
74
+ @query_hash = nil
75
+ @body_parsed = nil
76
+ end
77
+
78
+ # Full URL reconstruction
79
+ def url
80
+ scheme = env["rack.url_scheme"] || "http"
81
+ host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
82
+ port = env["SERVER_PORT"]
83
+ url_str = "#{scheme}://#{host}"
84
+ url_str += ":#{port}" if port && port != "80" && port != "443"
85
+ url_str += @path
86
+ url_str += "?#{@query_string}" unless @query_string.empty?
87
+ url_str
88
+ end
89
+
90
+ # Lazy accessors
91
+ def headers
92
+ @headers ||= extract_headers
93
+ end
94
+
95
+ def cookies
96
+ @cookies ||= parse_cookies
97
+ end
98
+
99
+ def session
100
+ @session ||= Tina4::Session.new(@env)
101
+ end
102
+
103
+ # Raw body string
104
+ def body
105
+ @body_raw ||= read_body
106
+ end
107
+
108
+ # Parsed body (JSON or form data)
109
+ def body_parsed
110
+ @body_parsed ||= parse_body
111
+ end
112
+
113
+ # Parsed query string as hash
114
+ def query
115
+ @query_hash ||= parse_query_to_hash(@query_string)
116
+ end
117
+
118
+ def files
119
+ @files ||= extract_files
120
+ end
121
+
122
+ # Merged params: query + body + path_params (path_params highest priority)
123
+ # Supports both string and symbol key access (indifferent access).
124
+ def params
125
+ @params ||= build_params
126
+ end
127
+
128
+ # Look up a param by symbol or string key (indifferent access shortcut).
129
+ def param(key, default = nil)
130
+ params[key.to_s] || params[key.to_sym] || default
131
+ end
132
+
133
+ def [](key)
134
+ params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
135
+ end
136
+
137
+ def header(name)
138
+ headers[name.to_s.downcase.gsub("-", "_")]
139
+ end
140
+
141
+ def json_body
142
+ @json_body ||= begin
143
+ JSON.parse(body)
144
+ rescue JSON::ParserError, TypeError
145
+ {}
146
+ end
147
+ end
148
+
149
+ def bearer_token
150
+ auth = header("authorization") || ""
151
+ auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
152
+ end
153
+
154
+ private
155
+
156
+ def extract_client_ip
157
+ # Check X-Forwarded-For first (proxy/load balancer)
158
+ forwarded = @env["HTTP_X_FORWARDED_FOR"]
159
+ if forwarded && !forwarded.empty?
160
+ # Take the first (original client) IP
161
+ forwarded.split(",").first.strip
162
+ else
163
+ @env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
164
+ end
165
+ end
166
+
167
+ def extract_headers
168
+ h = {}
169
+ @env.each do |key, value|
170
+ if key.start_with?("HTTP_")
171
+ h[key[5..-1].downcase] = value
172
+ end
173
+ end
174
+ h
175
+ end
176
+
177
+ def parse_cookies
178
+ cookie_str = @env["HTTP_COOKIE"]
179
+ return {} unless cookie_str && !cookie_str.empty?
180
+
181
+ result = {}
182
+ cookie_str.split(";").each do |pair|
183
+ key, value = pair.strip.split("=", 2)
184
+ result[key] = value if key
185
+ end
186
+ result
187
+ end
188
+
189
+ def read_body
190
+ input = @env["rack.input"]
191
+ return "" unless input
192
+ input.rewind if input.respond_to?(:rewind)
193
+ data = input.read || ""
194
+ input.rewind if input.respond_to?(:rewind)
195
+ data
196
+ end
197
+
198
+ def parse_body
199
+ if @content_type.include?("application/json")
200
+ json_body
201
+ elsif @content_type.include?("application/x-www-form-urlencoded")
202
+ parse_query_to_hash(body)
203
+ elsif @content_type.include?("multipart/form-data")
204
+ # Extract form fields from Rack's parsed multipart data.
205
+ # Files are handled separately by extract_files.
206
+ result = {}
207
+ form_hash = @env["rack.request.form_hash"] rescue nil
208
+ if form_hash
209
+ form_hash.each do |key, value|
210
+ # Skip file entries (handled by extract_files)
211
+ next if value.is_a?(Hash) && value[:tempfile]
212
+ result[key] = value
213
+ end
214
+ end
215
+ result
216
+ else
217
+ {}
218
+ end
219
+ end
220
+
221
+ def build_params
222
+ p = IndifferentHash.new
223
+
224
+ # Query string params
225
+ query.each { |k, v| p[k.to_s] = v }
226
+
227
+ # Body params
228
+ body_parsed.each { |k, v| p[k.to_s] = v }
229
+
230
+ # Path params (highest priority)
231
+ @path_params.each { |k, v| p[k.to_s] = v }
232
+ p
233
+ end
234
+
235
+ def parse_query_to_hash(qs)
236
+ result = {}
237
+ return result if qs.nil? || qs.empty?
238
+ qs.split("&").each do |pair|
239
+ key, value = pair.split("=", 2)
240
+ result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
241
+ end
242
+ result
243
+ end
244
+
245
+ def extract_files
246
+ result = {}
247
+ return result unless @content_type.include?("multipart/form-data")
248
+ begin
249
+ form_hash = @env["rack.request.form_hash"]
250
+ if form_hash
251
+ form_hash.each do |key, value|
252
+ if value.is_a?(Hash) && value[:tempfile]
253
+ result[key] = {
254
+ filename: value[:filename],
255
+ type: value[:type],
256
+ tempfile: value[:tempfile],
257
+ size: value[:tempfile].size
258
+ }
259
+ end
260
+ end
261
+ end
262
+ rescue StandardError
263
+ # Multipart parsing failed
264
+ end
265
+ result
266
+ end
267
+ end
268
+ end