logstash-input-file 4.1.18 → 4.2.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.
@@ -3,22 +3,20 @@
3
3
  module FileWatch module Stat
4
4
  class WindowsPath
5
5
 
6
- attr_reader :identifier, :inode, :modified_at, :size, :inode_struct
6
+ attr_reader :inode, :modified_at, :size, :inode_struct
7
7
 
8
8
  def initialize(source)
9
- @source = source
9
+ @source = source # Pathname
10
10
  @inode = Winhelper.identifier_from_path(@source.to_path)
11
- @dev_major = 0
12
- @dev_minor = 0
13
11
  # in windows the dev hi and low are in the identifier
14
- @inode_struct = InodeStruct.new(@inode, @dev_major, @dev_minor)
12
+ @inode_struct = InodeStruct.new(@inode, 0, 0)
15
13
  restat
16
14
  end
17
15
 
18
16
  def restat
19
- @inner_stat = @source.stat
20
- @modified_at = @inner_stat.mtime.to_f
21
- @size = @inner_stat.size
17
+ stat = @source.stat
18
+ @modified_at = stat.mtime.to_f
19
+ @size = stat.size
22
20
  end
23
21
 
24
22
  def windows?
@@ -26,7 +24,7 @@ module FileWatch module Stat
26
24
  end
27
25
 
28
26
  def inspect
29
- "<WindowsPath size='#{@size}', modified_at='#{@modified_at}', inode='#{@inode}', inode_struct='#{@inode_struct}'>"
27
+ "<#{self.class.name} size=#{@size}, modified_at=#{@modified_at}, inode=#{@inode}, inode_struct=#{@inode_struct}>"
30
28
  end
31
29
  end
32
30
  end end
@@ -7,11 +7,9 @@ module FileWatch module TailMode module Handlers
7
7
  # TODO consider trying to find the renamed file - it will have the same inode.
8
8
  # Needs a rotate scheme rename hint from user e.g. "<name>-YYYY-MM-DD-N.<ext>" or "<name>.<ext>.N"
9
9
  # send the found content to the same listener (stream identity)
10
- logger.trace("info",
11
- "watched_file details" => watched_file.details,
12
- "path" => watched_file.path)
10
+ logger.trace("delete", :path => watched_file.path, :watched_file => watched_file.details)
13
11
  if watched_file.bytes_unread > 0
14
- logger.warn(DATA_LOSS_WARNING, "unread_bytes" => watched_file.bytes_unread, "path" => watched_file.path)
12
+ logger.warn(DATA_LOSS_WARNING, :path => watched_file.path, :unread_bytes => watched_file.bytes_unread)
15
13
  end
16
14
  watched_file.listener.deleted
17
15
  # no need to worry about data in the buffer
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
- require "logstash/util/loggable"
2
+ require 'filewatch/processor'
3
3
  require_relative "handlers/base"
4
4
  require_relative "handlers/create_initial"
5
5
  require_relative "handlers/create"
@@ -18,20 +18,7 @@ module FileWatch module TailMode
18
18
  # :delete - file can't be read
19
19
  # :timeout - file is closable
20
20
  # :unignore - file was ignored, but have now received new content
21
- class Processor
22
- include LogStash::Util::Loggable
23
-
24
- attr_reader :watch, :deletable_filepaths
25
-
26
- def initialize(settings)
27
- @settings = settings
28
- @deletable_filepaths = []
29
- end
30
-
31
- def add_watch(watch)
32
- @watch = watch
33
- self
34
- end
21
+ class Processor < FileWatch::Processor
35
22
 
36
23
  def initialize_handlers(sincedb_collection, observer)
37
24
  @sincedb_collection = sincedb_collection
@@ -91,11 +78,12 @@ module FileWatch module TailMode
91
78
  private
92
79
 
93
80
  def process_closed(watched_files)
94
- # logger.trace("Closed processing")
81
+ logger.trace(__method__.to_s)
95
82
  # Handles watched_files in the closed state.
96
83
  # if its size changed it is put into the watched state
97
- watched_files.select {|wf| wf.closed? }.each do |watched_file|
98
- common_restat_with_delay(watched_file, "Closed") do
84
+ watched_files.each do |watched_file|
85
+ next unless watched_file.closed?
86
+ common_restat_with_delay(watched_file, __method__) do
99
87
  # it won't do this if rotation is detected
100
88
  if watched_file.size_changed?
101
89
  # if the closed file changed, move it to the watched state
@@ -108,13 +96,14 @@ module FileWatch module TailMode
108
96
  end
109
97
 
110
98
  def process_ignored(watched_files)
111
- # logger.trace("Ignored processing")
99
+ logger.trace(__method__.to_s)
112
100
  # Handles watched_files in the ignored state.
113
101
  # if its size changed:
114
102
  # put it in the watched state
115
103
  # invoke unignore
116
- watched_files.select {|wf| wf.ignored? }.each do |watched_file|
117
- common_restat_with_delay(watched_file, "Ignored") do
104
+ watched_files.each do |watched_file|
105
+ next unless watched_file.ignored?
106
+ common_restat_with_delay(watched_file, __method__) do
118
107
  # it won't do this if rotation is detected
119
108
  if watched_file.size_changed?
120
109
  watched_file.watch
@@ -128,11 +117,12 @@ module FileWatch module TailMode
128
117
  def process_delayed_delete(watched_files)
129
118
  # defer the delete to one loop later to ensure that the stat really really can't find a renamed file
130
119
  # because a `stat` can be called right in the middle of the rotation rename cascade
131
- logger.trace("Delayed Delete processing")
132
- watched_files.select {|wf| wf.delayed_delete?}.each do |watched_file|
133
- logger.trace(">>> Delayed Delete", "path" => watched_file.filename)
134
- common_restat_without_delay(watched_file, ">>> Delayed Delete") do
135
- logger.trace(">>> Delayed Delete: file at path found again", "watched_file" => watched_file.details)
120
+ logger.trace(__method__.to_s)
121
+ watched_files.each do |watched_file|
122
+ next unless watched_file.delayed_delete?
123
+ logger.trace(">>> Delayed Delete", :path => watched_file.path)
124
+ common_restat_without_delay(watched_file, __method__) do
125
+ logger.trace(">>> Delayed Delete: file at path found again", :watched_file => watched_file.details)
136
126
  watched_file.file_at_path_found_again
137
127
  end
138
128
  end
@@ -140,33 +130,35 @@ module FileWatch module TailMode
140
130
 
141
131
  def process_restat_for_watched_and_active(watched_files)
142
132
  # do restat on all watched and active states once now. closed and ignored have been handled already
143
- logger.trace("Watched + Active restat processing")
144
- watched_files.select {|wf| wf.watched? || wf.active?}.each do |watched_file|
145
- common_restat_with_delay(watched_file, "Watched")
133
+ logger.trace(__method__.to_s)
134
+ watched_files.each do |watched_file|
135
+ next if !watched_file.watched? && !watched_file.active?
136
+ common_restat_with_delay(watched_file, __method__)
146
137
  end
147
138
  end
148
139
 
149
140
  def process_rotation_in_progress(watched_files)
150
- logger.trace("Rotation In Progress processing")
151
- watched_files.select {|wf| wf.rotation_in_progress?}.each do |watched_file|
141
+ logger.trace(__method__.to_s)
142
+ watched_files.each do |watched_file|
143
+ next unless watched_file.rotation_in_progress?
152
144
  if !watched_file.all_read?
153
145
  if watched_file.file_open?
154
146
  # rotated file but original opened file is not fully read
155
147
  # we need to keep reading the open file, if we close it we lose it because the path is now pointing at a different file.
156
- logger.trace(">>> Rotation In Progress - inode change detected and original content is not fully read, reading all", "watched_file details" => watched_file.details)
148
+ logger.trace(">>> Rotation In Progress - inode change detected and original content is not fully read, reading all", :watched_file => watched_file.details)
157
149
  # need to fully read open file while we can
158
150
  watched_file.set_maximum_read_loop
159
151
  grow(watched_file)
160
152
  watched_file.set_standard_read_loop
161
153
  else
162
- logger.warn(">>> Rotation In Progress - inode change detected and original content is not fully read, file is closed and path points to new content", "watched_file details" => watched_file.details)
154
+ logger.warn(">>> Rotation In Progress - inode change detected and original content is not fully read, file is closed and path points to new content", :watched_file => watched_file.details)
163
155
  end
164
156
  end
165
157
  current_key = watched_file.sincedb_key
166
158
  sdb_value = @sincedb_collection.get(current_key)
167
159
  potential_key = watched_file.stat_sincedb_key
168
160
  potential_sdb_value = @sincedb_collection.get(potential_key)
169
- logger.trace(">>> Rotation In Progress", "watched_file" => watched_file.details, "found_sdb_value" => sdb_value, "potential_key" => potential_key, "potential_sdb_value" => potential_sdb_value)
161
+ logger.trace(">>> Rotation In Progress", :watched_file => watched_file.details, :found_sdb_value => sdb_value, :potential_key => potential_key, :potential_sdb_value => potential_sdb_value)
170
162
  if potential_sdb_value.nil?
171
163
  logger.trace("---------- >>>> Rotation In Progress: rotating as existing file")
172
164
  watched_file.rotate_as_file
@@ -189,13 +181,13 @@ module FileWatch module TailMode
189
181
  sdb_value.clear_watched_file unless sdb_value.nil?
190
182
  potential_sdb_value.set_watched_file(watched_file)
191
183
  else
192
- logger.trace("---------- >>>> Rotation In Progress: rotating from...", "this watched_file details" => watched_file.details, "other watched_file details" => other_watched_file.details)
184
+ logger.trace("---------- >>>> Rotation In Progress: rotating from...", :this_watched_file => watched_file.details, :other_watched_file => other_watched_file.details)
193
185
  watched_file.rotate_from(other_watched_file)
194
186
  sdb_value.clear_watched_file unless sdb_value.nil?
195
187
  potential_sdb_value.set_watched_file(watched_file)
196
188
  end
197
189
  end
198
- logger.trace("---------- >>>> Rotation In Progress: after handling rotation", "this watched_file details" => watched_file.details, "sincedb_value" => (potential_sdb_value || sdb_value))
190
+ logger.trace("---------- >>>> Rotation In Progress: after handling rotation", :this_watched_file => watched_file.details, :sincedb_value => (potential_sdb_value || sdb_value))
199
191
  end
200
192
  end
201
193
 
@@ -206,11 +198,11 @@ module FileWatch module TailMode
206
198
  # and we allow the block to open the file and create a sincedb collection record if needed
207
199
  # some have never been active and some have
208
200
  # those that were active before but are watched now were closed under constraint
209
- logger.trace("Watched processing")
201
+ logger.trace(__method__.to_s)
210
202
  # how much of the max active window is available
211
- to_take = @settings.max_active - watched_files.count{|wf| wf.active?}
203
+ to_take = @settings.max_active - watched_files.count(&:active?)
212
204
  if to_take > 0
213
- watched_files.select {|wf| wf.watched?}.take(to_take).each do |watched_file|
205
+ watched_files.select(&:watched?).take(to_take).each do |watched_file|
214
206
  watched_file.activate
215
207
  if watched_file.initial?
216
208
  create_initial(watched_file)
@@ -223,36 +215,37 @@ module FileWatch module TailMode
223
215
  now = Time.now.to_i
224
216
  if (now - watch.lastwarn_max_files) > MAX_FILES_WARN_INTERVAL
225
217
  waiting = watched_files.size - @settings.max_active
226
- logger.warn(@settings.max_warn_msg + ", files yet to open: #{waiting}")
218
+ logger.warn("#{@settings.max_warn_msg}, files yet to open: #{waiting}")
227
219
  watch.lastwarn_max_files = now
228
220
  end
229
221
  end
230
222
  end
231
223
 
232
224
  def process_active(watched_files)
233
- # logger.trace("Active processing")
225
+ logger.trace(__method__.to_s)
234
226
  # Handles watched_files in the active state.
235
227
  # files have been opened at this point
236
- watched_files.select {|wf| wf.active? }.each do |watched_file|
228
+ watched_files.each do |watched_file|
229
+ next unless watched_file.active?
237
230
  break if watch.quit?
238
231
  path = watched_file.filename
239
232
  if watched_file.grown?
240
- logger.trace("Active - file grew: #{path}: new size is #{watched_file.last_stat_size}, bytes read #{watched_file.bytes_read}")
233
+ logger.trace("#{__method__} file grew: new size is #{watched_file.last_stat_size}, bytes read #{watched_file.bytes_read}", :path => path)
241
234
  grow(watched_file)
242
235
  elsif watched_file.shrunk?
243
236
  if watched_file.bytes_unread > 0
244
- logger.warn("Active - shrunk: DATA LOSS!! truncate detected with #{watched_file.bytes_unread} unread bytes: #{path}")
237
+ logger.warn("potential data loss, file truncate detected with #{watched_file.bytes_unread} unread bytes", :path => path)
245
238
  end
246
239
  # we don't update the size here, its updated when we actually read
247
- logger.trace("Active - file shrunk #{path}: new size is #{watched_file.last_stat_size}, old size #{watched_file.bytes_read}")
240
+ logger.trace("#{__method__} file shrunk: new size is #{watched_file.last_stat_size}, old size #{watched_file.bytes_read}", :path => path)
248
241
  shrink(watched_file)
249
242
  else
250
243
  # same size, do nothing
251
- logger.trace("Active - no change", "watched_file" => watched_file.details)
244
+ logger.trace("#{__method__} no change", :path => path)
252
245
  end
253
246
  # can any active files be closed to make way for waiting files?
254
247
  if watched_file.file_closable?
255
- logger.trace("Watch each: active: file expired: #{path}")
248
+ logger.trace("#{__method__} file expired", :path => path)
256
249
  timeout(watched_file)
257
250
  watched_file.close
258
251
  end
@@ -270,28 +263,28 @@ module FileWatch module TailMode
270
263
  def common_restat(watched_file, action, delay, &block)
271
264
  all_ok = true
272
265
  begin
273
- watched_file.restat
266
+ restat(watched_file)
274
267
  if watched_file.rotation_in_progress?
275
- logger.trace("-------------------- >>>>> restat - rotation_detected", "watched_file details" => watched_file.details, "new sincedb key" => watched_file.stat_sincedb_key)
268
+ logger.trace("-------------------- >>>>> restat - rotation_detected", :watched_file => watched_file.details, :new_sincedb_key => watched_file.stat_sincedb_key)
276
269
  # don't yield to closed and ignore processing
277
270
  else
278
271
  yield if block_given?
279
272
  end
280
273
  rescue Errno::ENOENT
281
274
  if delay
282
- logger.trace("#{action} - delaying the stat fail on: #{watched_file.filename}")
275
+ logger.trace("#{action} - delaying the stat fail on", :filename => watched_file.filename)
283
276
  watched_file.delay_delete
284
277
  else
285
278
  # file has gone away or we can't read it anymore.
286
- logger.trace("#{action} - after a delay, really can't find this file: #{watched_file.filename}")
279
+ logger.trace("#{action} - after a delay, really can't find this file", :path => watched_file.path)
287
280
  watched_file.unwatch
288
- logger.trace("#{action} - removing from collection: #{watched_file.filename}")
281
+ logger.trace("#{action} - removing from collection", :filename => watched_file.filename)
289
282
  delete(watched_file)
290
- deletable_filepaths << watched_file.path
283
+ add_deletable_path watched_file.path
291
284
  all_ok = false
292
285
  end
293
286
  rescue => e
294
- logger.error("#{action} - other error #{watched_file.path}: (#{e.message}, #{e.backtrace.take(8).inspect})")
287
+ logger.error("#{action} - other error", error_details(e, watched_file))
295
288
  all_ok = false
296
289
  end
297
290
  all_ok
@@ -1,26 +1,25 @@
1
1
  # encoding: utf-8
2
2
  require "logstash/util/loggable"
3
+ require "concurrent/atomic/atomic_boolean"
3
4
 
4
5
  module FileWatch
5
6
  class Watch
6
7
  include LogStash::Util::Loggable
7
8
 
8
9
  attr_accessor :lastwarn_max_files
9
- attr_reader :discoverer, :watched_files_collection
10
+ attr_reader :discoverer, :processor, :watched_files_collection
10
11
 
11
- def initialize(discoverer, watched_files_collection, settings)
12
+ def initialize(discoverer, processor, settings)
13
+ @discoverer = discoverer
14
+ @watched_files_collection = discoverer.watched_files_collection
12
15
  @settings = settings
16
+
13
17
  # we need to be threadsafe about the quit mutation
14
18
  @quit = Concurrent::AtomicBoolean.new(false)
15
19
  @lastwarn_max_files = 0
16
- @discoverer = discoverer
17
- @watched_files_collection = watched_files_collection
18
- end
19
20
 
20
- def add_processor(processor)
21
21
  @processor = processor
22
22
  @processor.add_watch(self)
23
- self
24
23
  end
25
24
 
26
25
  def watch(path)
@@ -67,20 +66,16 @@ module FileWatch
67
66
  watched_files = @watched_files_collection.values
68
67
  @processor.process_all_states(watched_files)
69
68
  ensure
70
- @watched_files_collection.remove_paths(@processor.deletable_filepaths)
71
- @processor.deletable_filepaths.clear
69
+ @watched_files_collection.remove_paths(@processor.clear_deletable_paths)
72
70
  end
73
- end # def each
71
+ end
74
72
 
75
73
  def quit
76
74
  @quit.make_true
77
75
  end
78
76
 
79
77
  def quit?
80
- if @settings.exit_after_read
81
- @exit = @watched_files_collection.empty?
82
- end
83
- @quit.true? || @exit
78
+ @quit.true? || (@settings.exit_after_read && @watched_files_collection.empty?)
84
79
  end
85
80
 
86
81
  private
@@ -6,7 +6,7 @@ module FileWatch
6
6
  IO_BASED_STAT = 1
7
7
 
8
8
  attr_reader :bytes_read, :state, :file, :buffer, :recent_states, :bytes_unread
9
- attr_reader :path, :accessed_at, :modified_at, :pathname, :filename
9
+ attr_reader :path, :accessed_at, :pathname, :filename
10
10
  attr_reader :listener, :read_loop_count, :read_chunk_size, :stat
11
11
  attr_reader :loop_count_type, :loop_count_mode
12
12
  attr_accessor :last_open_warning_at
@@ -16,7 +16,7 @@ module FileWatch
16
16
  def initialize(pathname, stat, settings)
17
17
  @settings = settings
18
18
  @pathname = Pathname.new(pathname) # given arg pathname might be a string or a Pathname object
19
- @path = @pathname.to_path
19
+ @path = @pathname.to_path.freeze
20
20
  @filename = @pathname.basename.to_s
21
21
  full_state_reset(stat)
22
22
  watch
@@ -24,10 +24,6 @@ module FileWatch
24
24
  set_accessed_at
25
25
  end
26
26
 
27
- def no_restat_reset
28
- full_state_reset(@stat)
29
- end
30
-
31
27
  def full_state_reset(this_stat = nil)
32
28
  if this_stat.nil?
33
29
  begin
@@ -75,6 +71,7 @@ module FileWatch
75
71
  @size = @stat.size
76
72
  @sdb_key_v1 = @stat.inode_struct
77
73
  end
74
+ private :set_stat
78
75
 
79
76
  def rotate_as_file(bytes_read = 0)
80
77
  # rotation, when a sincedb record exists for new inode, but no watched file to rotate from
@@ -100,19 +97,33 @@ module FileWatch
100
97
  stat_sincedb_key != sincedb_key
101
98
  end
102
99
 
103
- def restat
100
+ # @return true if the file was modified since last stat
101
+ def restat!
102
+ modified_at # to always be able to detect changes
104
103
  @stat.restat
105
104
  if rotation_detected?
106
105
  # switch to new state now
107
106
  rotation_in_progress
107
+ return true
108
108
  else
109
109
  @size = @stat.size
110
110
  update_bytes_unread
111
+ modified_at_changed?
112
+ end
113
+ end
114
+
115
+ def modified_at(update = false)
116
+ if update || @modified_at.nil?
117
+ @modified_at = @stat.modified_at
118
+ else
119
+ @modified_at
111
120
  end
112
121
  end
113
122
 
114
- def modified_at
115
- @stat.modified_at
123
+ # @return whether modified_at changed since it was last read
124
+ # @see #restat!
125
+ def modified_at_changed?
126
+ modified_at != @stat.modified_at
116
127
  end
117
128
 
118
129
  def position_for_new_sincedb_value
@@ -405,14 +416,14 @@ module FileWatch
405
416
  end
406
417
 
407
418
  def details
408
- detail = "@filename='#{filename}', @state='#{state}', @recent_states='#{@recent_states.inspect}', "
409
- detail.concat("@bytes_read='#{@bytes_read}', @bytes_unread='#{@bytes_unread}', current_size='#{current_size}', ")
410
- detail.concat("last_stat_size='#{last_stat_size}', file_open?='#{file_open?}', @initial=#{@initial}")
411
- "<FileWatch::WatchedFile: #{detail}, @sincedb_key='#{sincedb_key}'>"
419
+ detail = "@filename='#{@filename}', @state=#{@state.inspect}, @recent_states=#{@recent_states.inspect}, "
420
+ detail.concat("@bytes_read=#{@bytes_read}, @bytes_unread=#{@bytes_unread}, current_size=#{current_size}, ")
421
+ detail.concat("last_stat_size=#{last_stat_size}, file_open?=#{file_open?}, @initial=#{@initial}")
422
+ "<FileWatch::WatchedFile: #{detail}, sincedb_key='#{sincedb_key}'>"
412
423
  end
413
424
 
414
425
  def inspect
415
- "\"<FileWatch::WatchedFile: @filename='#{filename}', @state='#{state}', @sincedb_key='#{sincedb_key}, size=#{@size}>\""
426
+ "<FileWatch::WatchedFile: @filename='#{@filename}', @state=#{@state.inspect}, current_size=#{current_size}, sincedb_key='#{sincedb_key}'>"
416
427
  end
417
428
 
418
429
  def to_s
@@ -1,89 +1,22 @@
1
1
  # encoding: utf-8
2
- module FileWatch
3
- class WatchedFilesCollection
4
-
5
- def initialize(settings)
6
- @sort_by = settings.file_sort_by # "last_modified" | "path"
7
- @sort_direction = settings.file_sort_direction # "asc" | "desc"
8
- @sort_method = method("#{@sort_by}_#{@sort_direction}".to_sym)
9
- @files = Concurrent::Array.new
10
- @pointers = Concurrent::Hash.new
11
- end
12
2
 
13
- def add(watched_file)
14
- @files << watched_file
15
- @sort_method.call
16
- end
3
+ require 'java'
17
4
 
18
- def remove_paths(paths)
19
- removed_files = Array(paths).map do |path|
20
- index = @pointers.delete(path)
21
- if index
22
- watched_file = @files.delete_at(index)
23
- refresh_pointers
24
- watched_file
25
- end
26
- end
27
- @sort_method.call
28
- removed_files
29
- end
5
+ module FileWatch
6
+ # @see `org.logstash.filewatch.WatchedFilesCollection`
7
+ class WatchedFilesCollection
30
8
 
9
+ # Closes all managed watched files.
10
+ # @see FileWatch::WatchedFile#file_close
31
11
  def close_all
32
- @files.each(&:file_close)
33
- end
34
-
35
- def empty?
36
- @files.empty?
37
- end
38
-
39
- def keys
40
- @pointers.keys
12
+ each_file(&:file_close) # synchronized
41
13
  end
42
14
 
43
- def values
44
- @files
45
- end
46
-
47
- def watched_file_by_path(path)
48
- index = @pointers[path]
49
- return nil unless index
50
- @files[index]
51
- end
52
-
53
- private
54
-
55
- def last_modified_asc
56
- @files.sort! do |left, right|
57
- left.modified_at <=> right.modified_at
58
- end
59
- refresh_pointers
60
- end
15
+ # @return [Enumerable<String>] managed path keys (snapshot)
16
+ alias keys paths
61
17
 
62
- def last_modified_desc
63
- @files.sort! do |left, right|
64
- right.modified_at <=> left.modified_at
65
- end
66
- refresh_pointers
67
- end
68
-
69
- def path_asc
70
- @files.sort! do |left, right|
71
- left.path <=> right.path
72
- end
73
- refresh_pointers
74
- end
18
+ # @return [Enumerable<WatchedFile>] managed files (snapshot)
19
+ alias values files
75
20
 
76
- def path_desc
77
- @files.sort! do |left, right|
78
- right.path <=> left.path
79
- end
80
- refresh_pointers
81
- end
82
-
83
- def refresh_pointers
84
- @files.each_with_index do |watched_file, index|
85
- @pointers[watched_file.path] = index
86
- end
87
- end
88
21
  end
89
22
  end