safrano 0.6.7 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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