safrano 0.6.7 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a93263d81a1f266029f7aac03d6b7226477f10d27d33255462e08133955ab759
4
- data.tar.gz: 894a0bfab2eeeee57543bee7dd0c1ec93b5bc73e0afc6fe5f979c7b7e7067039
3
+ metadata.gz: dda061616194242041f7308c791bc5689fabaf0627f6f5090df3141fe5a34d8e
4
+ data.tar.gz: bec6791bb225330e1e149bf4166012397f9ffce9df2dbe0be3d7b224c5172212
5
5
  SHA512:
6
- metadata.gz: f0900d4f692a9139226d4fb84703f8a0c47a071b97b0a46c8455016f6a7d0d09caf2b12f32e616c9880a10a0b50e09917ca22afc4af7fc0f4cddac690193b25c
7
- data.tar.gz: 30a623f667830e0dc4342bcf240843d313173ef12c0e877dc1814e779fc7f53459e7ec7e958aedd22b4387c9390907844a0c2c84a639db8d98dc62805203a671
6
+ metadata.gz: 7aef08bdf67609d7e836b955e478f4b0a8cd67a08fc6caf0c9e9b263bac3d59086a6da59967cdacf5b3bb328060cbb20b9cdbe70e28fcacb5ce464e63eeb7e59
7
+ data.tar.gz: 2f65e93d591fc18ca7836a17c61250bc181c1d9aff14bd875e2d2f4803406ca76d577c6cd4c0b5f7e468240ac6d63e9389f7367a5b6cba6bfa298c4c5efd5fcd
@@ -49,7 +49,7 @@ module Safrano
49
49
  else
50
50
  # same as above with GMT offset in minutes
51
51
  # DateTime offset is Rational ; fraction of hours per Day --> *24*60
52
- min_off_s = (min_off = (offset * 60 * 24).to_i) > 0 ? "+#{min_off}" : min_off.to_s
52
+ min_off_s = (min_off = (offset * 60 * 24).to_i).positive? ? "+#{min_off}" : min_off.to_s
53
53
  "/Date(#{strftime('%Q')}#{min_off_s})/"
54
54
  end
55
55
  end
@@ -57,7 +57,7 @@ module Safrano
57
57
  # the json raw data is like this : "HireDate": "\/Date(704678400000)\/"
58
58
  # --> \/\/
59
59
  def to_edm_json
60
- if utc? || (gmt_offset == 0)
60
+ if utc? || gmt_offset.zero?
61
61
  # no offset
62
62
  # %s : seconds since unix epoch
63
63
  # %L : milliseconds 000-999
@@ -66,7 +66,7 @@ module Safrano
66
66
  else
67
67
  # same as above with GMT offset in minutes
68
68
 
69
- min_off_s = (min_off = gmt_offset / 60) > 0 ? "+#{min_off}" : min_off.to_s
69
+ min_off_s = (min_off = gmt_offset / 60).positive? ? "+#{min_off}" : min_off.to_s
70
70
  "/Date(#{strftime('%s%L')}#{min_off_s})/"
71
71
  end
72
72
  end
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
- # Mayonaise
25
- class MyOApp < Safrano::ServerApp
26
- attr_reader :full_req
27
- attr_reader :response
28
- attr_reader :db
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
- @db = full_req.service.collections.first.db
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
- @request.service = @full_req.service
39
- headers 'DataServiceVersion' => @request.service.data_service_version
47
+ @service = @full_req.service
48
+ headers 'DataServiceVersion' => @service.data_service_version
40
49
  end
41
50
 
42
- def batch_call(part_req)
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
- @request = Safrano::Request.new(env)
53
+
47
54
  @response = Safrano::Response.new
48
55
 
49
- if part_req.level == 2
50
- @request.in_changeset = true
51
- @request.content_id = part_req.content_id
52
- @request.content_id_references = part_req.content_id_references
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] = @full_req.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
- # Huile d'olive extra
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
- # jaune d'oeuf
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
- # battre le tout
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
- resp_hdrs, @mult_response.body = @mult_request.get_http_resp(batcha)
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
@@ -194,9 +194,8 @@ module Safrano
194
194
  @child_dataset_method.call
195
195
  end
196
196
 
197
- def each
198
- y = @child_method.call
199
- y.each { |enty| yield enty }
197
+ def each(&block)
198
+ @child_method.call.each(&block)
200
199
  end
201
200
 
202
201
  def to_a
@@ -2,49 +2,73 @@
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
8
10
  module Media
9
11
  # base class for Media Handler
10
12
  class Handler
11
- def check_before_create(data:,
12
- entity:,
13
- filename:)
13
+ def check_before_create(data:, entity:, filename:)
14
14
  Contract::OK
15
15
  end
16
16
  end
17
-
18
17
  # Simple static File/Directory based media store handler
19
18
  # similar to Rack::Static
20
19
  # with a flat directory structure
21
20
  class Static < Handler
22
21
  def initialize(root: nil, mediaklass:)
23
- @root = File.absolute_path(root || Dir.pwd)
24
- @file_server = ::Rack::Files.new(@root)
22
+ super()
23
+ @root = Pathname(File.absolute_path(root || Dir.pwd))
24
+ @files_class = ::Rack.release[0..2] == '2.0' ? ::Rack::File : ::Rack::Files
25
25
  @media_class = mediaklass
26
- @media_dir_name = mediaklass.to_s
26
+ @media_dir_name = Pathname(mediaklass.to_s)
27
+ @semaphore = Thread::Mutex.new
28
+
27
29
  register
28
30
  end
29
31
 
30
32
  def register
31
- @abs_klass_dir = File.absolute_path(@media_dir_name, @root)
33
+ @abs_klass_dir = @root + @media_dir_name
34
+ @abs_temp_dir = @abs_klass_dir.join('tmp')
32
35
  end
33
36
 
34
37
  def create_abs_class_dir
35
38
  FileUtils.makedirs @abs_klass_dir unless Dir.exist?(@abs_klass_dir)
36
39
  end
37
40
 
41
+ def create_abs_temp_dir
42
+ FileUtils.makedirs @abs_temp_dir unless Dir.exist?(@abs_temp_dir)
43
+ end
44
+
38
45
  def finalize
39
46
  create_abs_class_dir
47
+ create_abs_temp_dir
48
+ end
49
+
50
+ # see also ...
51
+ # File activesupport/lib/active_support/core_ext/file/atomic.rb, line 21
52
+ def atomic_write(file_name)
53
+ Tempfile.open('', @abs_temp_dir) do |temp_file|
54
+ temp_file.binmode
55
+ return_val = yield temp_file
56
+ temp_file.close
57
+
58
+ # Overwrite original file with temp file
59
+ File.rename(temp_file.path, file_name)
60
+ return_val
61
+ end
40
62
  end
41
63
 
42
64
  # minimal working implementation...
43
- # Note: @file_server works relative to @root directory
65
+ # Note: files_app works relative to @root directory
44
66
  def odata_get(request:, entity:)
45
67
  media_env = request.env.dup
46
68
  media_env['PATH_INFO'] = filename(entity)
47
- fsret = @file_server.call(media_env)
69
+ # new app instance for each call for thread safety
70
+ files_app = @files_class.new(@root)
71
+ fsret = files_app.call(media_env)
48
72
  if fsret.first == 200
49
73
  # provide own content type as we keep it in the media entity
50
74
  fsret[1]['Content-Type'] = entity.content_type
@@ -55,28 +79,23 @@ module Safrano
55
79
  # this is relative to @root
56
80
  # eg. Photo/1
57
81
  def media_path(entity)
58
- File.join(@media_dir_name, media_directory(entity))
82
+ @media_dir_name + media_directory(entity)
59
83
  end
60
84
 
61
85
  # relative to @root
62
86
  # eg Photo/1/1
63
87
  def filename(entity)
64
- Dir.chdir(abs_path(entity)) do
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
88
+ media_path(entity) + ressource_version(entity)
70
89
  end
71
90
 
72
91
  # /@root/Photo/1
73
92
  def abs_path(entity)
74
- File.absolute_path(media_path(entity), @root)
93
+ @root + media_path(entity)
75
94
  end
76
95
 
77
96
  # absolute filename
78
97
  def abs_filename(entity)
79
- File.absolute_path(filename(entity), @root)
98
+ @root + filename(entity)
80
99
  end
81
100
 
82
101
  # this is relative to abs_klass_dir(entity) eg to /@root/Photo
@@ -86,29 +105,31 @@ module Safrano
86
105
  entity.media_path_id
87
106
  end
88
107
 
89
- def in_media_directory(entity)
90
- mpi = media_directory(entity)
108
+ # the same as above but absolute
109
+ def abs_media_directory(entity)
110
+ @abs_klass_dir + entity.media_path_id
111
+ end
112
+
113
+ # yields the absolute path of media directory
114
+ # and ensure the directory exists
115
+ def with_media_directory(entity)
116
+ mpi = abs_media_directory(entity)
91
117
  Dir.mkdir mpi unless Dir.exist?(mpi)
92
- Dir.chdir mpi do
93
- yield
94
- end
118
+ yield Pathname(mpi)
95
119
  end
96
120
 
97
121
  def odata_delete(entity:)
98
- Dir.chdir(@abs_klass_dir) do
99
- in_media_directory(entity) do
100
- Dir.glob('*').each { |oldf| File.delete(oldf) }
101
- end
102
- end
122
+ mpi = abs_media_directory(entity)
123
+ return unless Dir.exist?(mpi)
124
+
125
+ mpi.children.each { |oldp| File.delete(oldp) }
103
126
  end
104
127
 
105
128
  # Here as well, MVP implementation
106
129
  def save_file(data:, filename:, entity:)
107
- Dir.chdir(@abs_klass_dir) do
108
- in_media_directory(entity) do
109
- filename = '1'
110
- File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
111
- end
130
+ with_media_directory(entity) do |d|
131
+ filename = d.join('1')
132
+ atomic_write(filename) { |f| IO.copy_stream(data, f) }
112
133
  end
113
134
  end
114
135
 
@@ -116,24 +137,31 @@ module Safrano
116
137
  # after each upload, so that clients get informed about new versions
117
138
  # of the same media ressource
118
139
  def ressource_version(entity)
119
- Dir.chdir(@abs_klass_dir) do
120
- in_media_directory(entity) do
121
- Dir.glob('*').max
122
- end
123
- end
140
+ abs_media_directory(entity).children(false).max.to_s
124
141
  end
125
142
 
126
- # Here as well, MVP implementation
127
- def replace_file(data:, filename:, entity:)
128
- Dir.chdir(@abs_klass_dir) do
129
- in_media_directory(entity) do
130
- version = nil
131
- Dir.glob('*').sort.each do |oldf|
132
- version = oldf
133
- File.delete(oldf)
134
- end
135
- filename = (version.to_i + 1).to_s
136
- File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
143
+ # Note: add a new Version and remove the previous one
144
+ def replace_file(data:, entity:)
145
+ with_media_directory(entity) do |d|
146
+ tp = Tempfile.open('', @abs_temp_dir) do |temp_file|
147
+ temp_file.binmode
148
+ IO.copy_stream(data, temp_file)
149
+ temp_file.path
150
+ end
151
+
152
+ # picking new filename and the "move" operation must
153
+ # be protected
154
+ @semaphore.synchronize do
155
+ # new filename = "version" + 1
156
+ v = ressource_version(entity)
157
+ filename = d + (v.to_i + 1).to_s
158
+
159
+ # Move temp file to original target file
160
+ File.rename(tp, filename)
161
+
162
+ # remove the previous version
163
+ filename = d + v
164
+ File.delete(filename)
137
165
  end
138
166
  end
139
167
  end
@@ -162,42 +190,25 @@ module Safrano
162
190
  StaticTree.path_builder(entity.media_path_ids)
163
191
  end
164
192
 
165
- def in_media_directory(entity)
166
- mpi = media_directory(entity)
193
+ # the same as above but absolute
194
+ def abs_media_directory(entity)
195
+ @abs_klass_dir + StaticTree.path_builder(entity.media_path_ids)
196
+ end
197
+
198
+ # yields the absolute path of media directory
199
+ # and ensure the directory exists
200
+ def with_media_directory(entity)
201
+ mpi = abs_media_directory(entity)
202
+
167
203
  FileUtils.makedirs mpi unless Dir.exist?(mpi)
168
- Dir.chdir(mpi) { yield }
204
+ yield Pathname(mpi)
169
205
  end
170
206
 
171
207
  def odata_delete(entity:)
172
- Dir.chdir(@abs_klass_dir) do
173
- in_media_directory(entity) do
174
- Dir.glob('*').sort.each { |oldf| File.delete(oldf) if File.file?(oldf) }
175
- end
176
- end
177
- end
208
+ mpi = abs_media_directory(entity)
209
+ return unless Dir.exist?(mpi)
178
210
 
179
- # Here as well, MVP implementation
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
211
+ mpi.children.each { |oldp| File.delete(oldp) if File.file?(oldp) }
201
212
  end
202
213
  end
203
214
  end
@@ -13,11 +13,28 @@ module Rack
13
13
  MSG_FUNC = case FORMAT.count('%')
14
14
  when 10
15
15
  lambda { |env, length, status, began_at|
16
- format(FORMAT, env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', env['REMOTE_USER'] || '-', Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'), env[REQUEST_METHOD], env[SCRIPT_NAME] + env[PATH_INFO], env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}", env[SERVER_PROTOCOL], status.to_s[0..3], length, Utils.clock_time - began_at)
16
+ format(FORMAT,
17
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
18
+ env['REMOTE_USER'] || '-',
19
+ Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
20
+ env[REQUEST_METHOD],
21
+ env[SCRIPT_NAME] + env[PATH_INFO],
22
+ env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
23
+ env[SERVER_PROTOCOL],
24
+ status.to_s[0..3], length, Utils.clock_time - began_at)
17
25
  }
18
26
  when 11
19
27
  lambda { |env, length, status, began_at|
20
- format(FORMAT, env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-', env['REMOTE_USER'] || '-', Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'), env[REQUEST_METHOD], env[SCRIPT_NAME], env[PATH_INFO], env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}", env[SERVER_PROTOCOL], status.to_s[0..3], length, Utils.clock_time - began_at)
28
+ format(FORMAT,
29
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
30
+ env['REMOTE_USER'] || '-',
31
+ Time.now.strftime('%d/%b/%Y:%H:%M:%S %z'),
32
+ env[REQUEST_METHOD],
33
+ env[SCRIPT_NAME],
34
+ env[PATH_INFO],
35
+ env[QUERY_STRING].empty? ? '' : "?#{env[QUERY_STRING]}",
36
+ env[SERVER_PROTOCOL],
37
+ status.to_s[0..3], length, Utils.clock_time - began_at)
21
38
  }
22
39
  end
23
40
 
@@ -92,7 +92,7 @@ module Safrano
92
92
  # wrapper
93
93
  # for OData Entity and Collections, return them directly
94
94
  # for others, ie ComplexType, Prims etc, return the ResultDefinition-subclass wrapped result
95
- def self.do_execute_func_result(result, _req, _apply_query_params = false)
95
+ def self.do_execute_func_result(result, _req, apply_query_params: false)
96
96
  new(result)
97
97
  end
98
98
  end
@@ -129,7 +129,7 @@ module Safrano
129
129
 
130
130
  # wrapper
131
131
  # for OData Entity return them directly
132
- def self.do_execute_func_result(result, _req, apply_query_params = false)
132
+ def self.do_execute_func_result(result, _req, apply_query_params: false)
133
133
  # note: Sequel entities instances seem to be thread safe, so we can
134
134
  # safely add request-dependant data (eg. req.params) there
135
135
  apply_query_params ? result : result.inactive_query_params
@@ -143,7 +143,7 @@ module Safrano
143
143
 
144
144
  # wrapper
145
145
  # for OData Entity Collection return them directly
146
- def self.do_execute_func_result(result, req, apply_query_params = false)
146
+ def self.do_execute_func_result(result, req, apply_query_params: false)
147
147
  coll = Safrano::OData::Collection.new(@klassmod)
148
148
  # instance_exec has other instance variables; @values would be nil in the block below
149
149
  # need to pass a local copy
@@ -241,12 +241,8 @@ module Safrano
241
241
  def odata_h
242
242
  ret = { METAK => { TYPEK => self.class.type_name } }
243
243
 
244
- @values.each do |k, v|
245
- ret[k] = if v.respond_to? :odata_h
246
- v.odata_h
247
- else
248
- v
249
- end
244
+ @values.each do |key, val|
245
+ ret[key] = val.respond_to?(:odata_h) ? val.odata_h : val
250
246
  end
251
247
  ret
252
248
  end