safrano 0.6.7 → 0.6.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/odata/batch.rb +39 -33
- data/lib/odata/collection_media.rb +90 -78
- data/lib/odata/entity.rb +1 -2
- data/lib/odata/model_ext.rb +6 -7
- data/lib/safrano/multipart.rb +14 -12
- data/lib/safrano/rack_app.rb +14 -123
- data/lib/safrano/request.rb +126 -0
- data/lib/safrano/version.rb +1 -1
- metadata +18 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d1ee63cc31ce50cff784481ce75f5f5581c2b9ff406a18efb516908bbf7c65d
|
4
|
+
data.tar.gz: 5770021472ccce8efb445114d4b4834b4fb0ff31b58f6444654cfb16b5aeb860
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1be48927ce7f3b5350ea903fa6ebb514cd6c09a1eafb064b7760c56bb396f02e59cab6e2a2e091ab8c526c77d45e9ff18838728d34d7500b29f611ba98e858b
|
7
|
+
data.tar.gz: 5877c1e41a0bbbcb76d948914a703a24ea8cdf52215815b7c4051c3cfb3807463adfa0144ccce4861dfd988abf8776a002de6353d6343205f3767af02b9ca090
|
data/lib/odata/batch.rb
CHANGED
@@ -8,54 +8,63 @@ require_relative './common_logger'
|
|
8
8
|
module Safrano
|
9
9
|
# Support for OData multipart $batch Requests
|
10
10
|
class Request
|
11
|
-
def create_batch_app
|
12
|
-
Batch::MyOApp.new(self)
|
13
|
-
end
|
14
|
-
|
15
11
|
def parse_multipart
|
16
12
|
@mimep = MIME::Media::Parser.new
|
17
13
|
@boundary = media_type_params['boundary']
|
18
14
|
@mimep.hook_multipart(media_type, @boundary)
|
19
15
|
@mimep.parse_str(body)
|
20
16
|
end
|
17
|
+
|
18
|
+
# The top-level (full_req) Request is used like
|
19
|
+
# a Rack App
|
20
|
+
# With this method we get the response of part-requests
|
21
|
+
# app.call(env) --> full_req.bach_call(part_req)
|
22
|
+
|
23
|
+
def batch_call(part_req)
|
24
|
+
Safrano::Batch::PartRequest.new(part_req, self).process
|
25
|
+
end
|
26
|
+
|
27
|
+
# needed for changeset transaction
|
28
|
+
def db
|
29
|
+
@service.collections.first.db
|
30
|
+
end
|
21
31
|
end
|
22
32
|
|
23
33
|
module Batch
|
24
|
-
#
|
25
|
-
class
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def initialize(full_req)
|
34
|
+
# Part-Request part of a Batch-Request
|
35
|
+
class PartRequest < Safrano::Request
|
36
|
+
def initialize(part_req, full_req)
|
37
|
+
batch_env(part_req, full_req)
|
38
|
+
@env['HTTP_HOST'] = full_req.env['HTTP_HOST']
|
39
|
+
super(@env, full_req.service_base)
|
31
40
|
@full_req = full_req
|
32
|
-
@
|
41
|
+
@part_req = part_req
|
33
42
|
end
|
34
43
|
|
35
44
|
# redefined for $batch
|
36
45
|
def before
|
37
46
|
headers 'Cache-Control' => 'no-cache'
|
38
|
-
@
|
39
|
-
headers 'DataServiceVersion' => @
|
47
|
+
@service = @full_req.service
|
48
|
+
headers 'DataServiceVersion' => @service.data_service_version
|
40
49
|
end
|
41
50
|
|
42
|
-
def
|
43
|
-
env = batch_env(part_req)
|
44
|
-
env['HTTP_HOST'] = @full_req.env['HTTP_HOST']
|
51
|
+
def process
|
45
52
|
began_at = Rack::Utils.clock_time
|
46
|
-
|
53
|
+
|
47
54
|
@response = Safrano::Response.new
|
48
55
|
|
49
|
-
if part_req.level == 2
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
56
|
+
if @part_req.level == 2
|
57
|
+
@in_changeset = true
|
58
|
+
@content_id = @part_req.content_id
|
59
|
+
@content_id_references = @part_req.content_id_references
|
53
60
|
end
|
54
61
|
|
55
62
|
before
|
63
|
+
|
56
64
|
dispatch
|
57
65
|
|
58
66
|
status, header, body = @response.finish
|
67
|
+
|
59
68
|
# Logging of sub-requests with ODataCommonLogger.
|
60
69
|
# A bit hacky but working
|
61
70
|
# TODO: test ?
|
@@ -80,19 +89,19 @@ module Safrano
|
|
80
89
|
converted_headers
|
81
90
|
end
|
82
91
|
|
83
|
-
def batch_env(mime_req)
|
92
|
+
def batch_env(mime_req, full_req)
|
84
93
|
@env = ::Rack::MockRequest.env_for(mime_req.uri,
|
85
94
|
method: mime_req.http_method,
|
86
95
|
input: mime_req.content)
|
87
96
|
# Logging of sub-requests
|
88
|
-
@env[Rack::RACK_ERRORS] =
|
97
|
+
@env[Rack::RACK_ERRORS] = full_req.env[Rack::RACK_ERRORS]
|
89
98
|
@env.merge! headers_for_env(mime_req.hd)
|
90
99
|
|
91
100
|
@env
|
92
101
|
end
|
93
102
|
end
|
94
103
|
|
95
|
-
#
|
104
|
+
# $batch Handler
|
96
105
|
class HandlerBase
|
97
106
|
TREND = Safrano::Transition.new('', trans: 'transition_end')
|
98
107
|
def allowed_transitions
|
@@ -103,7 +112,8 @@ module Safrano
|
|
103
112
|
Safrano::Transition::RESULT_END
|
104
113
|
end
|
105
114
|
end
|
106
|
-
|
115
|
+
|
116
|
+
# $batch disabled Handler
|
107
117
|
class DisabledHandler < HandlerBase
|
108
118
|
def odata_post(_req)
|
109
119
|
[404, EMPTY_HASH, '$batch is not enabled ']
|
@@ -113,7 +123,8 @@ module Safrano
|
|
113
123
|
[404, EMPTY_HASH, '$batch is not enabled ']
|
114
124
|
end
|
115
125
|
end
|
116
|
-
|
126
|
+
|
127
|
+
# $batch enabled Handler
|
117
128
|
class EnabledHandler < HandlerBase
|
118
129
|
attr_accessor :boundary
|
119
130
|
attr_accessor :mmboundary
|
@@ -121,8 +132,6 @@ module Safrano
|
|
121
132
|
attr_accessor :parts
|
122
133
|
attr_accessor :request
|
123
134
|
|
124
|
-
def initialize; end
|
125
|
-
|
126
135
|
# here we are in the Batch handler object, and this POST should
|
127
136
|
# normally handle a $batch request
|
128
137
|
def odata_post(req)
|
@@ -130,15 +139,12 @@ module Safrano
|
|
130
139
|
|
131
140
|
if @request.media_type == Safrano::MP_MIXED
|
132
141
|
|
133
|
-
batcha = @request.create_batch_app
|
134
142
|
@mult_request = @request.parse_multipart
|
135
143
|
|
136
144
|
@mult_request.prepare_content_id_refs
|
137
|
-
@mult_response = Safrano::Response.new
|
138
145
|
|
139
|
-
|
146
|
+
@mult_request.get_mult_resp(@request)
|
140
147
|
|
141
|
-
[202, resp_hdrs, @mult_response.body[0]]
|
142
148
|
else
|
143
149
|
[415, EMPTY_HASH, 'Unsupported Media Type']
|
144
150
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'rack'
|
4
4
|
require 'fileutils'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'pathname'
|
5
7
|
require_relative './navigation_attribute'
|
6
8
|
|
7
9
|
module Safrano
|
@@ -14,37 +16,60 @@ module Safrano
|
|
14
16
|
Contract::OK
|
15
17
|
end
|
16
18
|
end
|
17
|
-
|
18
19
|
# Simple static File/Directory based media store handler
|
19
20
|
# similar to Rack::Static
|
20
21
|
# with a flat directory structure
|
21
22
|
class Static < Handler
|
22
23
|
def initialize(root: nil, mediaklass:)
|
23
|
-
@root = File.absolute_path(root || Dir.pwd)
|
24
|
-
@
|
24
|
+
@root = Pathname(File.absolute_path(root || Dir.pwd))
|
25
|
+
@files_class = (::Rack.release[0..2] == '2.0') ? ::Rack::File : ::Rack::Files
|
25
26
|
@media_class = mediaklass
|
26
|
-
@media_dir_name = mediaklass.to_s
|
27
|
+
@media_dir_name = Pathname(mediaklass.to_s)
|
28
|
+
@semaphore = Thread::Mutex.new
|
29
|
+
|
27
30
|
register
|
28
31
|
end
|
29
32
|
|
30
33
|
def register
|
31
|
-
@abs_klass_dir =
|
34
|
+
@abs_klass_dir = @root + @media_dir_name
|
35
|
+
@abs_temp_dir = @abs_klass_dir + 'tmp'
|
32
36
|
end
|
33
37
|
|
34
38
|
def create_abs_class_dir
|
35
39
|
FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
|
36
40
|
end
|
37
41
|
|
42
|
+
def create_abs_temp_dir
|
43
|
+
FileUtils.makedirs @abs_temp_dir unless Dir.exist?(@abs_temp_dir)
|
44
|
+
end
|
45
|
+
|
38
46
|
def finalize
|
39
47
|
create_abs_class_dir
|
48
|
+
create_abs_temp_dir
|
49
|
+
end
|
50
|
+
|
51
|
+
# see also ...
|
52
|
+
# File activesupport/lib/active_support/core_ext/file/atomic.rb, line 21
|
53
|
+
def atomic_write(file_name)
|
54
|
+
Tempfile.open('', @abs_temp_dir) do |temp_file|
|
55
|
+
temp_file.binmode
|
56
|
+
return_val = yield temp_file
|
57
|
+
temp_file.close
|
58
|
+
|
59
|
+
# Overwrite original file with temp file
|
60
|
+
File.rename(temp_file.path, file_name)
|
61
|
+
return_val
|
62
|
+
end
|
40
63
|
end
|
41
64
|
|
42
65
|
# minimal working implementation...
|
43
|
-
# Note:
|
66
|
+
# Note: files_app works relative to @root directory
|
44
67
|
def odata_get(request:, entity:)
|
45
68
|
media_env = request.env.dup
|
46
69
|
media_env['PATH_INFO'] = filename(entity)
|
47
|
-
|
70
|
+
# new app instance for each call for thread safety
|
71
|
+
files_app = @files_class.new(@root)
|
72
|
+
fsret = files_app.call(media_env)
|
48
73
|
if fsret.first == 200
|
49
74
|
# provide own content type as we keep it in the media entity
|
50
75
|
fsret[1]['Content-Type'] = entity.content_type
|
@@ -55,28 +80,23 @@ module Safrano
|
|
55
80
|
# this is relative to @root
|
56
81
|
# eg. Photo/1
|
57
82
|
def media_path(entity)
|
58
|
-
|
83
|
+
@media_dir_name + media_directory(entity)
|
59
84
|
end
|
60
85
|
|
61
86
|
# relative to @root
|
62
87
|
# eg Photo/1/1
|
63
88
|
def filename(entity)
|
64
|
-
|
65
|
-
# simple design: one file per directory, and the directory
|
66
|
-
# contains the media entity-id --> implicit link between the media
|
67
|
-
# entity
|
68
|
-
File.join(media_path(entity), Dir.glob('*').max)
|
69
|
-
end
|
89
|
+
media_path(entity) + ressource_version(entity)
|
70
90
|
end
|
71
91
|
|
72
92
|
# /@root/Photo/1
|
73
93
|
def abs_path(entity)
|
74
|
-
|
94
|
+
@root + media_path(entity)
|
75
95
|
end
|
76
96
|
|
77
97
|
# absolute filename
|
78
98
|
def abs_filename(entity)
|
79
|
-
|
99
|
+
@root + filename(entity)
|
80
100
|
end
|
81
101
|
|
82
102
|
# this is relative to abs_klass_dir(entity) eg to /@root/Photo
|
@@ -86,29 +106,31 @@ module Safrano
|
|
86
106
|
entity.media_path_id
|
87
107
|
end
|
88
108
|
|
89
|
-
|
90
|
-
|
109
|
+
# the same as above but absolute
|
110
|
+
def abs_media_directory(entity)
|
111
|
+
@abs_klass_dir + entity.media_path_id
|
112
|
+
end
|
113
|
+
|
114
|
+
# yields the absolute path of media directory
|
115
|
+
# and ensure the directory exists
|
116
|
+
def with_media_directory(entity)
|
117
|
+
mpi = abs_media_directory(entity)
|
91
118
|
Dir.mkdir mpi unless Dir.exist?(mpi)
|
92
|
-
|
93
|
-
yield
|
94
|
-
end
|
119
|
+
yield Pathname(mpi)
|
95
120
|
end
|
96
121
|
|
97
122
|
def odata_delete(entity:)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
end
|
123
|
+
mpi = abs_media_directory(entity)
|
124
|
+
return unless Dir.exist?(mpi)
|
125
|
+
|
126
|
+
mpi.children.each { |oldp| File.delete(oldp) }
|
103
127
|
end
|
104
128
|
|
105
129
|
# Here as well, MVP implementation
|
106
130
|
def save_file(data:, filename:, entity:)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
111
|
-
end
|
131
|
+
with_media_directory(entity) do |d|
|
132
|
+
filename = d + '1'
|
133
|
+
atomic_write(filename) { |f| IO.copy_stream(data, f) }
|
112
134
|
end
|
113
135
|
end
|
114
136
|
|
@@ -116,24 +138,31 @@ module Safrano
|
|
116
138
|
# after each upload, so that clients get informed about new versions
|
117
139
|
# of the same media ressource
|
118
140
|
def ressource_version(entity)
|
119
|
-
|
120
|
-
in_media_directory(entity) do
|
121
|
-
Dir.glob('*').max
|
122
|
-
end
|
123
|
-
end
|
141
|
+
abs_media_directory(entity).children(with_directory = false).max.to_s
|
124
142
|
end
|
125
143
|
|
126
|
-
#
|
127
|
-
def replace_file(data:,
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
144
|
+
# Note: add a new Version and remove the previous one
|
145
|
+
def replace_file(data:, entity:)
|
146
|
+
with_media_directory(entity) do |d|
|
147
|
+
tp = Tempfile.open('', @abs_temp_dir) do |temp_file|
|
148
|
+
temp_file.binmode
|
149
|
+
IO.copy_stream(data, temp_file)
|
150
|
+
temp_file.path
|
151
|
+
end
|
152
|
+
|
153
|
+
# picking new filename and the "move" operation must
|
154
|
+
# be protected
|
155
|
+
@semaphore.synchronize do
|
156
|
+
# new filename = "version" + 1
|
157
|
+
v = ressource_version(entity)
|
158
|
+
filename = d + (v.to_i + 1).to_s
|
159
|
+
|
160
|
+
# Move temp file to original target file
|
161
|
+
File.rename(tp, filename)
|
162
|
+
|
163
|
+
# remove the previous version
|
164
|
+
filename = d + v
|
165
|
+
File.delete(filename)
|
137
166
|
end
|
138
167
|
end
|
139
168
|
end
|
@@ -162,42 +191,25 @@ module Safrano
|
|
162
191
|
StaticTree.path_builder(entity.media_path_ids)
|
163
192
|
end
|
164
193
|
|
165
|
-
|
166
|
-
|
194
|
+
# the same as above but absolute
|
195
|
+
def abs_media_directory(entity)
|
196
|
+
@abs_klass_dir + StaticTree.path_builder(entity.media_path_ids)
|
197
|
+
end
|
198
|
+
|
199
|
+
# yields the absolute path of media directory
|
200
|
+
# and ensure the directory exists
|
201
|
+
def with_media_directory(entity)
|
202
|
+
mpi = abs_media_directory(entity)
|
203
|
+
|
167
204
|
FileUtils.makedirs mpi unless Dir.exist?(mpi)
|
168
|
-
|
205
|
+
yield Pathname(mpi)
|
169
206
|
end
|
170
207
|
|
171
208
|
def odata_delete(entity:)
|
172
|
-
|
173
|
-
|
174
|
-
Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
209
|
+
mpi = abs_media_directory(entity)
|
210
|
+
return unless Dir.exist?(mpi)
|
178
211
|
|
179
|
-
|
180
|
-
# def replace_file(data:, filename:, entity:)
|
181
|
-
# Dir.chdir(abs_klass_dir(entity)) do
|
182
|
-
# in_media_directory(entity) do
|
183
|
-
# Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
|
184
|
-
# File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
185
|
-
# end
|
186
|
-
# end
|
187
|
-
# end
|
188
|
-
# Here as well, MVP implementation
|
189
|
-
def replace_file(data:, filename:, entity:)
|
190
|
-
Dir.chdir(@abs_klass_dir) do
|
191
|
-
in_media_directory(entity) do
|
192
|
-
version = nil
|
193
|
-
Dir.glob('*').sort.each do |oldf|
|
194
|
-
version = oldf
|
195
|
-
File.delete(oldf)
|
196
|
-
end
|
197
|
-
filename = (version.to_i + 1).to_s
|
198
|
-
File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
|
199
|
-
end
|
200
|
-
end
|
212
|
+
mpi.children.each { |oldp| File.delete(oldp) if File.file?(oldp) }
|
201
213
|
end
|
202
214
|
end
|
203
215
|
end
|
data/lib/odata/entity.rb
CHANGED
data/lib/odata/model_ext.rb
CHANGED
@@ -445,9 +445,9 @@ module Safrano
|
|
445
445
|
end # db_schema.each do |col, props|
|
446
446
|
|
447
447
|
# check if key needs casting. Important for later entity-uri generation !
|
448
|
-
|
449
|
-
|
450
|
-
|
448
|
+
return unless primary_key.is_a? Symbol # single key field
|
449
|
+
|
450
|
+
@pk_castfunc = @casted_cols[primary_key]
|
451
451
|
end # build_casted_cols(service)
|
452
452
|
|
453
453
|
def finalize_publishing(service)
|
@@ -516,7 +516,6 @@ module Safrano
|
|
516
516
|
|
517
517
|
@iuk_rgx_parts.transform_values! { |v| /\A#{v}\z/ }
|
518
518
|
|
519
|
-
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
520
519
|
else
|
521
520
|
@pk_names = [primary_key.to_s]
|
522
521
|
@pk_cast_from_string = nil
|
@@ -543,9 +542,9 @@ module Safrano
|
|
543
542
|
end
|
544
543
|
end
|
545
544
|
@iuk_rgx = /\A\s*#{kvpredicate}\s*\z/
|
546
|
-
# @entity_id_url_regexp = /\A\(\s*#{kvpredicate}\s*\)(.*)/.freeze
|
547
|
-
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
548
545
|
end
|
546
|
+
# @entity_id_url_regexp = /\A\(\s*#{kvpredicate}\s*\)(.*)/.freeze
|
547
|
+
@entity_id_url_regexp = KEYPRED_URL_REGEXP
|
549
548
|
end
|
550
549
|
|
551
550
|
def prepare_fields
|
@@ -673,7 +672,7 @@ module Safrano
|
|
673
672
|
|
674
673
|
mid.split(/\s*,\s*/).each do |midpart|
|
675
674
|
mval = nil
|
676
|
-
mpk,
|
675
|
+
mpk, _mrgx = scan_rgx_parts.find do |_pk, rgx|
|
677
676
|
if (md = rgx.match(midpart))
|
678
677
|
mval = md[1]
|
679
678
|
end
|
data/lib/safrano/multipart.rb
CHANGED
@@ -113,8 +113,8 @@ module MIME
|
|
113
113
|
MPS = 'multipart/'
|
114
114
|
MP_RGX1 = %r{^(digest|mixed);\s*boundary="(.*)"}.freeze
|
115
115
|
MP_RGX2 = %r{^(digest|mixed);\s*boundary=(.*)}.freeze
|
116
|
-
# APP_HTTP_RGX = %r{^application/http}.freeze
|
117
116
|
APP_HTTP = 'application/http'
|
117
|
+
|
118
118
|
def new_content
|
119
119
|
@target =
|
120
120
|
if @target_ct.start_with?(MPS) &&
|
@@ -319,7 +319,7 @@ module MIME
|
|
319
319
|
def addline(line)
|
320
320
|
@body_lines << line
|
321
321
|
end
|
322
|
-
end
|
322
|
+
end # Parser
|
323
323
|
|
324
324
|
attr_reader :boundary
|
325
325
|
|
@@ -369,12 +369,12 @@ module MIME
|
|
369
369
|
@hd[CTT_TYPE_LC] = "#{Safrano::MP_MIXED}; boundary=#{@boundary}"
|
370
370
|
end
|
371
371
|
|
372
|
-
def
|
373
|
-
get_response(
|
374
|
-
[@response.hd, @response.unparse(true)]
|
372
|
+
def get_mult_resp(full_req)
|
373
|
+
get_response(full_req)
|
374
|
+
[202, @response.hd, @response.unparse(true)]
|
375
375
|
end
|
376
376
|
|
377
|
-
def get_response(
|
377
|
+
def get_response(full_req)
|
378
378
|
@response = self.class.new(::SecureRandom.uuid)
|
379
379
|
@response.set_multipart_header
|
380
380
|
if @level == 1 # changeset need their own global transaction
|
@@ -382,9 +382,10 @@ module MIME
|
|
382
382
|
# and will be flagged with in_changeset=true
|
383
383
|
# and this will finally be used to skip the transaction
|
384
384
|
# of the changes
|
385
|
-
|
385
|
+
|
386
|
+
full_req.db.transaction do
|
386
387
|
begin
|
387
|
-
@response.content = @content.map { |part| part.get_response(
|
388
|
+
@response.content = @content.map { |part| part.get_response(full_req) }
|
388
389
|
rescue Sequel::Rollback => e
|
389
390
|
# one of the changes of the changeset has failed
|
390
391
|
# --> provide a dummy empty response for the change-parts
|
@@ -394,7 +395,7 @@ module MIME
|
|
394
395
|
end
|
395
396
|
end
|
396
397
|
else
|
397
|
-
@response.content = @content.map { |
|
398
|
+
@response.content = @content.map { |part| part.get_response(full_req) }
|
398
399
|
end
|
399
400
|
@response
|
400
401
|
end
|
@@ -450,6 +451,7 @@ module MIME
|
|
450
451
|
class HttpResp < Media
|
451
452
|
attr_accessor :status
|
452
453
|
attr_accessor :content
|
454
|
+
attr_accessor :rack_resp
|
453
455
|
|
454
456
|
APPLICATION_HTTP_11 = ['Content-Type: application/http',
|
455
457
|
"Content-Transfer-Encoding: binary#{CRLF}",
|
@@ -558,9 +560,9 @@ module MIME
|
|
558
560
|
@content = other.content
|
559
561
|
end
|
560
562
|
|
561
|
-
def get_response(
|
562
|
-
# self.content should be the request
|
563
|
-
rack_resp =
|
563
|
+
def get_response(full_req)
|
564
|
+
# self.content should be the part-request
|
565
|
+
rack_resp = full_req.batch_call(@content)
|
564
566
|
@response = MIME::Content::Application::HttpResp.new
|
565
567
|
@response.status = rack_resp[0]
|
566
568
|
@response.hd = rack_resp[1]
|
data/lib/safrano/rack_app.rb
CHANGED
@@ -6,143 +6,33 @@ require_relative 'request'
|
|
6
6
|
require_relative 'response'
|
7
7
|
|
8
8
|
module Safrano
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
.if_valid do |_context|
|
14
|
-
# cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
|
15
|
-
headers.delete('Content-Type')
|
16
|
-
@response.headers.delete('Content-Type')
|
17
|
-
@response.headers['Content-Type'] = ''
|
18
|
-
[200, EMPTY_HASH, '']
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def odata_delete
|
23
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
24
|
-
.if_valid { |context| context.odata_delete(@request) }
|
25
|
-
end
|
26
|
-
|
27
|
-
def odata_put
|
28
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
29
|
-
.if_valid { |context| context.odata_put(@request) }
|
30
|
-
end
|
31
|
-
|
32
|
-
def odata_patch
|
33
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
34
|
-
.if_valid { |context| context.odata_patch(@request) }
|
35
|
-
end
|
36
|
-
|
37
|
-
def odata_get
|
38
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
39
|
-
.if_valid { |context| context.odata_get(@request) }
|
40
|
-
end
|
41
|
-
|
42
|
-
def odata_post
|
43
|
-
@walker.finalize.tap_error { |err| return err.odata_get(@request) }
|
44
|
-
.if_valid { |context| context.odata_post(@request) }
|
45
|
-
end
|
46
|
-
|
47
|
-
def odata_head
|
48
|
-
[200, EMPTY_HASH, [EMPTY_STRING]]
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# the main Rack server app. Source: the Rack docu/examples and partly
|
53
|
-
# inspired from Sinatra
|
9
|
+
# Note there is a strong 1 to 1 relation between an app instance
|
10
|
+
# and a published service. --> actually means also
|
11
|
+
# we only support on service per App-class because publishing is
|
12
|
+
# made on app class level
|
54
13
|
class ServerApp
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
include MethodHandlers
|
61
|
-
|
62
|
-
def before
|
63
|
-
@request.service_base = self.class.get_service_base
|
64
|
-
|
65
|
-
@request.negotiate_service_version.tap_valid do
|
66
|
-
myhdrs = NOCACHE_HDRS.dup
|
67
|
-
myhdrs[DATASERVICEVERSION] = @request.service.data_service_version
|
68
|
-
headers myhdrs
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# dispatch for all methods requiring parsing of the path
|
73
|
-
# with walker (ie. allmost all excepted HEAD)
|
74
|
-
def dispatch_with_walker
|
75
|
-
@walker = @request.create_odata_walker
|
76
|
-
case @request.request_method
|
77
|
-
when 'GET'
|
78
|
-
odata_get
|
79
|
-
when 'POST'
|
80
|
-
odata_post
|
81
|
-
when 'DELETE'
|
82
|
-
odata_delete
|
83
|
-
when 'OPTIONS'
|
84
|
-
odata_options
|
85
|
-
when 'PUT'
|
86
|
-
odata_put
|
87
|
-
when 'PATCH', 'MERGE'
|
88
|
-
odata_patch
|
89
|
-
else
|
90
|
-
raise Error
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def dispatch_error(err)
|
95
|
-
@response.status, rsph, @response.body = err.odata_get(@request)
|
96
|
-
headers rsph
|
97
|
-
end
|
98
|
-
|
99
|
-
def dispatch
|
100
|
-
req_ret = if @request.request_method !~ METHODS_REGEXP
|
101
|
-
[404, EMPTY_HASH, ['Did you get lost?']]
|
102
|
-
elsif @request.request_method == 'HEAD'
|
103
|
-
odata_head
|
104
|
-
else
|
105
|
-
dispatch_with_walker
|
106
|
-
end
|
107
|
-
@response.status, rsph, @response.body = req_ret
|
108
|
-
headers rsph
|
14
|
+
def initialize
|
15
|
+
# just get back the service base instance object
|
16
|
+
# that was saved on class level and save it here
|
17
|
+
# so it's not needed to call self.class.service
|
18
|
+
@service_base = self.class.get_service_base
|
109
19
|
end
|
110
20
|
|
111
21
|
def call(env)
|
112
|
-
|
113
|
-
dup._call(env)
|
114
|
-
end
|
115
|
-
|
116
|
-
def _call(env)
|
117
|
-
begin
|
118
|
-
@request = Safrano::Request.new(env)
|
119
|
-
@response = Safrano::Response.new
|
120
|
-
|
121
|
-
before.tap_error { |err| dispatch_error(err) }
|
122
|
-
.tap_valid { |_res| dispatch }
|
123
|
-
|
124
|
-
# handle remaining Sequel errors that we couldnt prevent with our
|
125
|
-
# own pre-checks
|
126
|
-
rescue Sequel::Error => e
|
127
|
-
dispatch_error(SequelExceptionError.new(e))
|
128
|
-
end
|
129
|
-
@response.finish
|
130
|
-
end
|
131
|
-
|
132
|
-
# Set multiple response headers with Hash.
|
133
|
-
def headers(hash = nil)
|
134
|
-
@response.headers.merge! hash if hash
|
135
|
-
@response.headers
|
22
|
+
Safrano::Request.new(env, @service_base).process
|
136
23
|
end
|
137
24
|
|
25
|
+
# needed for testing only ? try to remove this
|
138
26
|
def self.enable_batch
|
139
27
|
@service_base.enable_batch
|
140
28
|
end
|
141
29
|
|
30
|
+
# needed for testing only ? try to remove this
|
142
31
|
def self.path_prefix(path_pr)
|
143
32
|
@service_base.path_prefix path_pr
|
144
33
|
end
|
145
34
|
|
35
|
+
# needed for testing only ? try to remove this
|
146
36
|
def self.get_service_base
|
147
37
|
@service_base
|
148
38
|
end
|
@@ -157,6 +47,7 @@ module Safrano
|
|
157
47
|
sbase = Safrano::ServiceBase.new
|
158
48
|
sbase.instance_eval(&block) if block_given?
|
159
49
|
sbase.finalize_publishing
|
50
|
+
# save published service base instance on App-Class level
|
160
51
|
set_servicebase(sbase)
|
161
52
|
end
|
162
53
|
end
|
data/lib/safrano/request.rb
CHANGED
@@ -4,6 +4,49 @@ require 'rack'
|
|
4
4
|
require 'rfc2047'
|
5
5
|
|
6
6
|
module Safrano
|
7
|
+
# handle GET PUT etc
|
8
|
+
module MethodHandlers
|
9
|
+
def odata_options
|
10
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
11
|
+
.if_valid do |_context|
|
12
|
+
# cf. stackoverflow.com/questions/22924678/sinatra-delete-response-headers
|
13
|
+
headers.delete('Content-Type')
|
14
|
+
@response.headers.delete('Content-Type')
|
15
|
+
@response.headers['Content-Type'] = ''
|
16
|
+
[200, EMPTY_HASH, '']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def odata_delete
|
21
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
22
|
+
.if_valid { |context| context.odata_delete(self) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def odata_put
|
26
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
27
|
+
.if_valid { |context| context.odata_put(self) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def odata_patch
|
31
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
32
|
+
.if_valid { |context| context.odata_patch(self) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def odata_get
|
36
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
37
|
+
.if_valid { |context| context.odata_get(self) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def odata_post
|
41
|
+
@walker.finalize.tap_error { |err| return err.odata_get(self) }
|
42
|
+
.if_valid { |context| context.odata_post(self) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def odata_head
|
46
|
+
[200, EMPTY_HASH, [EMPTY_STRING]]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
7
50
|
# monkey patch deactivate Rack/multipart because it does not work on simple
|
8
51
|
# OData $batch requests when the content-length
|
9
52
|
# is not passed
|
@@ -13,6 +56,11 @@ module Safrano
|
|
13
56
|
HEADER_VAL_WITH_PAR = /(?:#{HEADER_VAL_RAW})\s*(?:;#{HEADER_PARAM})*/.freeze
|
14
57
|
ON_CGST_ERROR = (proc { |r| raise(Sequel::Rollback) if r.in_changeset })
|
15
58
|
|
59
|
+
METHODS_REGEXP = Regexp.new('HEAD|OPTIONS|GET|POST|PATCH|MERGE|PUT|DELETE').freeze
|
60
|
+
NOCACHE_HDRS = { 'Cache-Control' => 'no-cache',
|
61
|
+
'Expires' => '-1',
|
62
|
+
'Pragma' => 'no-cache' }.freeze
|
63
|
+
DATASERVICEVERSION = 'DataServiceVersion'
|
16
64
|
# borowed from Sinatra
|
17
65
|
class AcceptEntry
|
18
66
|
attr_accessor :params
|
@@ -87,6 +135,82 @@ module Safrano
|
|
87
135
|
# content-id references map
|
88
136
|
attr_accessor :content_id_references
|
89
137
|
|
138
|
+
include MethodHandlers
|
139
|
+
|
140
|
+
def initialize(env, service_base)
|
141
|
+
super(env)
|
142
|
+
@service_base = service_base
|
143
|
+
end
|
144
|
+
|
145
|
+
# process the request and return finished response
|
146
|
+
def process
|
147
|
+
begin
|
148
|
+
@response = Safrano::Response.new
|
149
|
+
|
150
|
+
before.tap_error { |err| dispatch_error(err) }
|
151
|
+
.tap_valid { |_res| dispatch }
|
152
|
+
|
153
|
+
# handle remaining Sequel errors that we couldnt prevent with our
|
154
|
+
# own pre-checks
|
155
|
+
rescue Sequel::Error => e
|
156
|
+
dispatch_error(SequelExceptionError.new(e))
|
157
|
+
end
|
158
|
+
@response.finish
|
159
|
+
end
|
160
|
+
|
161
|
+
def before
|
162
|
+
negotiate_service_version.tap_valid do
|
163
|
+
myhdrs = NOCACHE_HDRS.dup
|
164
|
+
myhdrs[DATASERVICEVERSION] = @service.data_service_version
|
165
|
+
headers myhdrs
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# dispatch for all methods requiring parsing of the path
|
170
|
+
# with walker (ie. allmost all excepted HEAD)
|
171
|
+
def dispatch_with_walker
|
172
|
+
@walker = create_odata_walker
|
173
|
+
case request_method
|
174
|
+
when 'GET'
|
175
|
+
odata_get
|
176
|
+
when 'POST'
|
177
|
+
odata_post
|
178
|
+
when 'DELETE'
|
179
|
+
odata_delete
|
180
|
+
when 'OPTIONS'
|
181
|
+
odata_options
|
182
|
+
when 'PUT'
|
183
|
+
odata_put
|
184
|
+
when 'PATCH', 'MERGE'
|
185
|
+
odata_patch
|
186
|
+
else
|
187
|
+
raise Error
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def dispatch_error(err)
|
192
|
+
@response.status, rsph, @response.body = err.odata_get(self)
|
193
|
+
headers rsph
|
194
|
+
end
|
195
|
+
|
196
|
+
def dispatch
|
197
|
+
req_ret = if request_method !~ METHODS_REGEXP
|
198
|
+
[404, EMPTY_HASH, ['Did you get lost?']]
|
199
|
+
elsif request_method == 'HEAD'
|
200
|
+
odata_head
|
201
|
+
else
|
202
|
+
dispatch_with_walker
|
203
|
+
end
|
204
|
+
@response.status, rsph, @response.body = req_ret
|
205
|
+
headers rsph
|
206
|
+
end
|
207
|
+
|
208
|
+
# Set multiple response headers with Hash.
|
209
|
+
def headers(hash = nil)
|
210
|
+
@response.headers.merge! hash if hash
|
211
|
+
@response.headers
|
212
|
+
end
|
213
|
+
|
90
214
|
# stores the newly created entity for the current content-id of
|
91
215
|
# the processed request
|
92
216
|
def register_content_id_ref(new_entity)
|
@@ -213,6 +337,7 @@ module Safrano
|
|
213
337
|
MIN_DTSV_PARSE_ERROR = Safrano::BadRequestError.new(
|
214
338
|
'MinDataServiceVersion could not be parsed'
|
215
339
|
).freeze
|
340
|
+
|
216
341
|
def get_minversion
|
217
342
|
if (rqv = env['HTTP_MINDATASERVICEVERSION'])
|
218
343
|
if (m = DATASERVICEVERSION_RGX.match(rqv))
|
@@ -234,6 +359,7 @@ module Safrano
|
|
234
359
|
MAX_LT_MIN_DTSV_ERROR = Safrano::BadRequestError.new(
|
235
360
|
'MinDataServiceVersion is larger as MaxDataServiceVersion'
|
236
361
|
).freeze
|
362
|
+
|
237
363
|
def negotiate_service_version
|
238
364
|
get_maxversion.if_valid do |maxv|
|
239
365
|
get_minversion.if_valid do |minv|
|
data/lib/safrano/version.rb
CHANGED
metadata
CHANGED
@@ -1,43 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safrano
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- oz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '4.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '2.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '4.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rack-cors
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
30
36
|
requirements:
|
31
|
-
- - "
|
37
|
+
- - ">="
|
32
38
|
- !ruby/object:Gem::Version
|
33
39
|
version: '1.1'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '3.0'
|
34
43
|
type: :runtime
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
37
46
|
requirements:
|
38
|
-
- - "
|
47
|
+
- - ">="
|
39
48
|
- !ruby/object:Gem::Version
|
40
49
|
version: '1.1'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '3.0'
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: rfc2047
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|