safrano 0.3.2 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -37,25 +37,56 @@ end
37
37
 
38
38
  # filter base class and subclass in our OData namespace
39
39
  module OData
40
+ class FilterBase
41
+ # re-useable empty filtering (idempotent)
42
+ EmptyFilter = new
43
+
44
+ def self.factory(filterstr)
45
+ filterstr.nil? ? EmptyFilter : FilterByParse.new(filterstr)
46
+ end
47
+
48
+ def apply_to_dataset(dtcx)
49
+ dtcx
50
+ end
51
+
52
+ # finalize
53
+ def finalize(jh) end
54
+
55
+ def empty?
56
+ true
57
+ end
58
+
59
+ def parse_error?
60
+ false
61
+ end
62
+ end
40
63
  # should handle everything by parsing
41
- class FilterByParse
42
- def initialize(filterstr, jh)
64
+ class FilterByParse < FilterBase
65
+ attr_reader :filterstr
66
+ def initialize(filterstr)
43
67
  @filterstr = filterstr.dup
44
68
  @ast = OData::Filter::Parser.new(@filterstr).build
45
- @jh = jh
46
69
  end
47
70
 
48
- def apply_to_dataset(dtcx)
49
- filtexpr = @ast.sequel_expr(@jh)
50
- dtcx = @jh.dataset(dtcx).where(filtexpr).select_all(@jh.start_model.table_name)
71
+ # this build's up the Sequel Filter Expression, and as a side effect,
72
+ # it also finalizes the join helper that we need for the start dataset join
73
+ # the join-helper is shared by the order-by object and was potentially already
74
+ # partly built on order-by object creation.
75
+ def finalize(jh)
76
+ @filtexpr = @ast.sequel_expr(jh)
51
77
  end
52
78
 
53
- def sequel_expr
54
- @ast.sequel_expr(@jh)
79
+ def apply_to_dataset(dtcx)
80
+ # normally finalize is called before, and thus @filtexpr is set
81
+ dtcx.where(@filtexpr)
55
82
  end
56
83
 
57
84
  def parse_error?
58
- @ast.kind_of? StandardError
85
+ @ast.is_a? StandardError
86
+ end
87
+
88
+ def empty?
89
+ false
59
90
  end
60
91
  end
61
92
  end
@@ -0,0 +1,279 @@
1
+ require 'rack'
2
+ require 'fileutils'
3
+ require_relative './navigation_attribute.rb'
4
+
5
+ module OData
6
+ module Media
7
+ # base class for Media Handler
8
+ class Handler
9
+ end
10
+
11
+ # Simple static File/Directory based media store handler
12
+ # similar to Rack::Static
13
+ # with a flat directory structure
14
+ class Static < Handler
15
+ def initialize(root: nil)
16
+ @root = File.absolute_path(root || Dir.pwd)
17
+ @file_server = ::Rack::File.new(@root)
18
+ end
19
+
20
+ # TODO: testcase and better abs_klass_dir design
21
+ def register(klass)
22
+ abs_klass_dir = File.absolute_path(klass.type_name, @root)
23
+ FileUtils.makedirs abs_klass_dir unless Dir.exist?(abs_klass_dir)
24
+ end
25
+
26
+ # minimal working implementation...
27
+ # Note: @file_server works relative to @root directory
28
+ def odata_get(request:, entity:)
29
+ media_env = request.env.dup
30
+ media_env['PATH_INFO'] = filename(entity)
31
+ fsret = @file_server.call(media_env)
32
+ if fsret.first == 200
33
+ # provide own content type as we keep it in the media entity
34
+ fsret[1]['Content-Type'] = entity.content_type
35
+ end
36
+ fsret
37
+ end
38
+
39
+ # TODO: [perf] this can be precalculated and cached on MediaModelKlass level
40
+ # and passed as argument to save_file
41
+ # eg. /@root/Photo
42
+ def abs_klass_dir(entity)
43
+ File.absolute_path(entity.klass_dir, @root)
44
+ end
45
+
46
+ # this is relative to @root
47
+ # eg. Photo/1
48
+ def media_path(entity)
49
+ File.join(entity.klass_dir, media_directory(entity))
50
+ end
51
+
52
+ # relative to @root
53
+ # eg Photo/1/pommes-topaz.jpg
54
+ def filename(entity)
55
+ Dir.chdir(abs_path(entity)) do
56
+ # simple design: one file per directory, and the directory
57
+ # contains the media entity-id --> implicit link between the media
58
+ # entity
59
+ File.join(media_path(entity), Dir.glob('*').first)
60
+ end
61
+ end
62
+
63
+ # /@root/Photo/1
64
+ def abs_path(entity)
65
+ File.absolute_path(media_path(entity), @root)
66
+ end
67
+
68
+ # this is relative to abs_klass_dir(entity) eg to /@root/Photo
69
+ # simplest implementation is media_directory = entity.media_path_id
70
+ # --> we get a 1 level depth flat directory structure
71
+ def media_directory(entity)
72
+ entity.media_path_id
73
+ end
74
+
75
+ def in_media_directory(entity)
76
+ mpi = media_directory(entity)
77
+ Dir.mkdir mpi unless Dir.exist?(mpi)
78
+ Dir.chdir mpi do
79
+ yield
80
+ end
81
+ end
82
+
83
+ def odata_delete(entity:)
84
+ Dir.chdir(abs_klass_dir(entity)) do
85
+ in_media_directory(entity) do
86
+ Dir.glob('*').each { |oldf| File.delete(oldf) }
87
+ end
88
+ end
89
+ end
90
+
91
+ # Here as well, MVP implementation
92
+ def save_file(data:, filename:, entity:)
93
+ Dir.chdir(abs_klass_dir(entity)) do
94
+ in_media_directory(entity) do
95
+ filename = '1'
96
+ File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
97
+ end
98
+ end
99
+ end
100
+
101
+ # needed for having a changing media ressource "source" metadata
102
+ # after each upload, so that clients get informed about new versions
103
+ # of the same media ressource
104
+ def ressource_version(entity)
105
+ Dir.chdir(abs_klass_dir(entity)) do
106
+ in_media_directory(entity) do
107
+ Dir.glob('*').last
108
+ end
109
+ end
110
+ end
111
+
112
+ # Here as well, MVP implementation
113
+ def replace_file(data:, filename:, entity:)
114
+ Dir.chdir(abs_klass_dir(entity)) do
115
+ in_media_directory(entity) do
116
+ version = nil
117
+ Dir.glob('*').each do |oldf|
118
+ version = oldf
119
+ File.delete(oldf)
120
+ end
121
+ filename = (version.to_i + 1).to_s
122
+ File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ # Simple static File/Directory based media store handler
128
+ # similar to Rack::Static
129
+ # with directory Tree structure
130
+
131
+ class StaticTree < Static
132
+ SEP = '/00/'.freeze
133
+ VERS = '/v'.freeze
134
+
135
+ def self.path_builder(ids)
136
+ ids.map { |id| id.to_s.chars.join('/') }.join(SEP) << VERS
137
+ end
138
+
139
+ # this is relative to abs_klass_dir(entity) eg to /@root/Photo
140
+ # tree-structure
141
+ # media_path_ids = 1 --> 1
142
+ # media_path_ids = 15 --> 1/5
143
+ # media_path_ids = 555 --> 5/5/5
144
+ # media_path_ids = 5,5,5 --> 5/00/5/00/5
145
+ # media_path_ids = 5,00,5 --> 5/00/0/0/00/5
146
+ # media_path_ids = 5,xyz,5 --> 5/00/x/y/z/00/5
147
+ def media_directory(entity)
148
+ StaticTree.path_builder(entity.media_path_ids)
149
+ # entity.media_path_ids.map{|id| id.to_s.chars.join('/')}.join(@sep)
150
+ end
151
+
152
+ def in_media_directory(entity)
153
+ mpi = media_directory(entity)
154
+ FileUtils.makedirs mpi unless Dir.exist?(mpi)
155
+ Dir.chdir(mpi) { yield }
156
+ end
157
+
158
+ def odata_delete(entity:)
159
+ Dir.chdir(abs_klass_dir(entity)) do
160
+ in_media_directory(entity) do
161
+ Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
162
+ end
163
+ end
164
+ end
165
+
166
+ # Here as well, MVP implementation
167
+ # def replace_file(data:, filename:, entity:)
168
+ # Dir.chdir(abs_klass_dir(entity)) do
169
+ # in_media_directory(entity) do
170
+ # Dir.glob('*').each { |oldf| File.delete(oldf) if File.file?(oldf) }
171
+ # File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
172
+ # end
173
+ # end
174
+ # end
175
+ # Here as well, MVP implementation
176
+ def replace_file(data:, filename:, entity:)
177
+ Dir.chdir(abs_klass_dir(entity)) do
178
+ in_media_directory(entity) do
179
+ version = nil
180
+ Dir.glob('*').each do |oldf|
181
+ version = oldf
182
+ File.delete(oldf)
183
+ end
184
+ filename = (version.to_i + 1).to_s
185
+ File.open(filename, 'wb') { |f| IO.copy_stream(data, f) }
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ # special handling for media entity
193
+ module EntityClassMedia
194
+ attr_reader :media_handler
195
+ attr_reader :slug_field
196
+
197
+ # API method for defining the media handler
198
+ # eg.
199
+ # publish_media_model photos do
200
+ # use OData::Media::Static, :root => '/media_root'
201
+ # end
202
+
203
+ def set_default_media_handler
204
+ @media_handler = OData::Media::Static.new
205
+ end
206
+
207
+ def use(klass, args)
208
+ @media_handler = klass.new(**args)
209
+ end
210
+
211
+ # API method for setting the model field mapped to SLUG on upload
212
+ def slug(inp)
213
+ @slug_field = inp
214
+ end
215
+
216
+ def api_check_media_fields
217
+ raise(OData::API::MediaModelError, self) unless db_schema.key?(:content_type)
218
+
219
+ # unless self.db_schema.has_key?(:media_src)
220
+ # raise OData::API::MediaModelError, self
221
+ # end
222
+ end
223
+
224
+ # END API methods
225
+
226
+ def new_media_entity(mimetype:)
227
+ nh = { 'content_type' => mimetype }
228
+ new_from_hson_h(nh)
229
+ end
230
+
231
+ # POST for media entity collection --> Create media-entity linked to
232
+ # uploaded file from payload
233
+ # 1. create new media entity
234
+ # 2. get the pk/id from the new media entity
235
+ # 3. Upload the file and use the pk/id to get an unique Directory/filename
236
+ # assignment to the media entity record
237
+ # NOTE: we will implement this first in a MVP way. There will be plenty of
238
+ # potential future enhancements (performance, scallability, flexibility... with added complexity)
239
+ # 4. create relation to parent if needed
240
+ def odata_create_entity_and_relation(req, assoc, parent)
241
+ req.with_media_data do |data, mimetype, filename|
242
+ ## future enhancement: validate allowed mimetypes ?
243
+ # if (invalid = invalid_media_mimetype(mimetype))
244
+ # ::OData::Request::ON_CGST_ERROR.call(req)
245
+ # return [422, {}, ['Invalid mime type: ', invalid.to_s]]
246
+ # end
247
+
248
+ if req.accept?(APPJSON)
249
+
250
+ new_entity = new_media_entity(mimetype: mimetype)
251
+
252
+ if slug_field
253
+
254
+ new_entity.set_fields({ slug_field => filename },
255
+ data_fields,
256
+ missing: :skip)
257
+ end
258
+
259
+ # to_one rels are create with FK data set on the parent entity
260
+ if parent
261
+ odata_create_save_entity_and_rel(req, new_entity, assoc, parent)
262
+ else
263
+ # in-changeset requests get their own transaction
264
+ new_entity.save(transaction: !req.in_changeset)
265
+ end
266
+
267
+ req.register_content_id_ref(new_entity)
268
+ new_entity.copy_request_infos(req)
269
+
270
+ media_handler.save_file(data: data, entity: new_entity, filename: filename)
271
+
272
+ [201, CT_JSON, new_entity.to_odata_post_json(service: req.service)]
273
+ else # TODO: other formats
274
+ 415
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
@@ -1,18 +1,32 @@
1
1
  require 'odata/error.rb'
2
2
 
3
- # Ordering with ruby expression
4
- module OrderWithRuby
5
- # this module requires the @fn attribute to exist where it is used
6
- def fn=(fnam)
7
- @fn = fnam
8
- @fn_tab = fnam.split('/').map(&:to_sym)
9
- end
10
- end
11
-
12
3
  # all ordering related classes in our OData module
13
4
  module OData
14
5
  # base class for ordering
15
- class Order
6
+ class OrderBase
7
+ # re-useable empty ordering (idempotent)
8
+ EmptyOrder = new
9
+
10
+ # input : the OData order string
11
+ # returns a Order object that should have a apply_to(cx) method
12
+ def self.factory(orderstr, jh)
13
+ orderstr.nil? ? EmptyOrder : MultiOrder.new(orderstr, jh)
14
+ end
15
+
16
+ def empty?
17
+ true
18
+ end
19
+
20
+ def parse_error?
21
+ false
22
+ end
23
+
24
+ def apply_to_dataset(dtcx)
25
+ dtcx
26
+ end
27
+ end
28
+
29
+ class Order < OrderBase
16
30
  attr_reader :oarg
17
31
  def initialize(ostr, jh)
18
32
  ostr.strip!
@@ -21,23 +35,14 @@ module OData
21
35
  build_oarg if @orderp
22
36
  end
23
37
 
24
- class << self
25
- attr_reader :regexp
26
- end
27
-
28
- # input : the filter string
29
- # returns a filter object that should have a apply_to(cx) method
30
- def self.new_by_parse(orderstr, jh)
31
- Order.new_full_match_complexpr(orderstr, jh)
32
- end
33
-
34
- # handle with Sequel
35
- def self.new_full_match_complexpr(orderstr, jh)
36
- ComplexOrder.new(orderstr, jh)
38
+ def empty?
39
+ false
37
40
  end
38
41
 
39
42
  def apply_to_dataset(dtcx)
40
- dtcx
43
+ # Warning, we need order_append, simply order(oarg) overwrites
44
+ # previous one !
45
+ dtcx.order_append(@oarg)
41
46
  end
42
47
 
43
48
  def build_oarg
@@ -60,26 +65,31 @@ module OData
60
65
  end
61
66
 
62
67
  # complex ordering logic
63
- class ComplexOrder < Order
68
+ class MultiOrder < Order
64
69
  def initialize(orderstr, jh)
65
70
  super
66
71
  @olist = []
67
72
  @jh = jh
68
- return unless orderstr
69
-
70
- @olist = orderstr.split(',').map do |ostr|
71
- oo = Order.new(ostr, @jh)
72
- oo.oarg
73
- end
73
+ @orderstr = orderstr.dup
74
+ @olist = orderstr.split(',').map { |ostr| Order.new(ostr, @jh) }
74
75
  end
75
76
 
76
77
  def apply_to_dataset(dtcx)
77
- @olist.each { |oarg|
78
- # Warning, we need order_append, simply order(oarg) overwrites
79
- # previous one !
80
- dtcx = dtcx.order_append(oarg)
81
- }
78
+ @olist.each { |osingl| dtcx = osingl.apply_to_dataset(dtcx) }
82
79
  dtcx
83
80
  end
81
+
82
+ def parse_error?
83
+ @orderstr.split(',').each do |pord|
84
+ pord.strip!
85
+ qualfn, dir = pord.split(/\s/)
86
+ qualfn.strip!
87
+ dir.strip! if dir
88
+ return true unless @jh.start_model.attrib_path_valid? qualfn
89
+ return true unless [nil, 'asc', 'desc'].include? dir
90
+ end
91
+
92
+ false
93
+ end
84
94
  end
85
95
  end