rfusefs 1.0.2.RC0 → 1.0.2.RC1

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.
data/README.rdoc CHANGED
@@ -36,23 +36,24 @@ explore the contents under <mountpoint>
36
36
 
37
37
  Happy Filesystem Hacking!
38
38
 
39
- === the hello world filesystem in 16 LOC
39
+ === the hello world filesystem in 14 LOC
40
40
 
41
41
  require 'rfusefs'
42
42
 
43
43
  class HelloDir
44
+
44
45
  def contents(path)
45
46
  ['hello.txt']
46
47
  end
48
+
47
49
  def file?(path)
48
50
  path == '/hello.txt'
49
51
  end
52
+
50
53
  def read_file(path)
51
54
  "Hello, World!\n"
52
55
  end
53
- def size(path)
54
- read_file(path).size
55
- end
56
+
56
57
  end
57
58
 
58
59
  # Usage: #{$0} mountpoint [mount_options]
data/Rakefile CHANGED
@@ -2,6 +2,9 @@
2
2
  require "bundler/gem_tasks"
3
3
  require 'yard'
4
4
  require 'rspec/core/rake_task'
5
+ require 'rake/clean'
6
+
7
+ CLOBBER.include [ "pkg/","doc/" ]
5
8
 
6
9
  YARD::Rake::YardocTask.new do |t|
7
10
  # Need this because YardocTask does not read the gemspec
data/lib/fuse/fusedir.rb CHANGED
@@ -1,11 +1,68 @@
1
1
  module FuseFS
2
2
 
3
+ # Helper for filesystem accounting
4
+ class StatsHelper
5
+
6
+ # @return [Integer] size of filesystem in bytes
7
+ attr_accessor :max_space
8
+ # @return [Integer] maximum number of (virtual) inodes
9
+ attr_accessor :max_nodes
10
+
11
+ # If set true, adjustments that cause space/nodes to exceed
12
+ # the maximums will raise ENOSPC (no space left on device)
13
+ # @return [Boolean]
14
+ attr_accessor :strict
15
+
16
+ # @return [Integer] used space in bytes
17
+ attr_reader :space
18
+
19
+ # @return [Integer] used inodes (typically count of files and directories)
20
+ attr_reader :nodes
21
+
22
+ #
23
+ # @param [Integer] max_space
24
+ # @param [Integer] max_nodes
25
+ # @param [Booleanr] strict
26
+ def initialize(max_space=nil,max_nodes=nil,strict=false)
27
+ @nodes = 0
28
+ @space = 0
29
+ @max_space = max_space
30
+ @max_nodes = max_nodes
31
+ @strict = strict
32
+ end
33
+
34
+ # Adjust accumlated statistics
35
+ # @param [Integer] delta_space change in {#space} usage
36
+ # @param [Integer] delta_nodes change in {#nodes} usage
37
+ #
38
+ # @return [void]
39
+ # @raise [Errno::ENOSPC] if {#strict} and adjusted {#space}/{#nodes} would exceed {#max_space} or {#max_nodes}
40
+ def adjust(delta_space,delta_nodes=0)
41
+ @nodes += delta_nodes
42
+ @space += delta_space
43
+ raise Errno::ENOSPC if @strict && ( @nodes > @max_nodes || @space > @max_space )
44
+ end
45
+
46
+ # @overload to_statistics()
47
+ # @return [Array<Integer>] in format expected by {FuseDir#statistics}
48
+ # @overload to_statistics(free_space,free_nodes)
49
+ # Calculate total space so that free space remains fixed
50
+ # @param [Integer] free_space available space in bytes
51
+ # @param [Integer] free_nodes available (virtual) inodes
52
+ # @return [Array<Integer>] in format expected by {FuseDir#statistics}
53
+ def to_statistics(free_space=nil,free_nodes=nil)
54
+ total_space = free_space ? space + free_space : max_space
55
+ total_nodes = free_nodes ? nodes + free_nodes : max_nodes
56
+ [ @space, @nodes, total_space, total_nodes ]
57
+ end
58
+ end
59
+
3
60
  # This class is equivalent to using Object.new() as the virtual directory
4
61
  # for target for {FuseFS.start}. It exists primarily to document the API
5
- # but can also be used as a superclass for your filesystem
6
- #
62
+ # but can also be used as a superclass for your filesystem providing sensible defaults
63
+ #
7
64
  # == Method call sequences
8
- #
65
+ #
9
66
  # === Stat (getattr)
10
67
  #
11
68
  # FUSE itself will generally stat referenced files and validate the results
@@ -38,10 +95,10 @@ module FuseFS
38
95
  # FUSE confirms path for the new file is a directory
39
96
  #
40
97
  # * {#can_write?} is checked at file open
41
- # * {#write_to} is called when the file is flushed or closed
98
+ # * {#write_to} is called when the file is synced, flushed or closed
99
+ #
100
+ # See also {#raw_open}, {#raw_truncate}, {#raw_write}, {#raw_sync}, {#raw_close}
42
101
  #
43
- # See also {#raw_open}, {#raw_truncate}, {#raw_write}, {#raw_close}
44
- #
45
102
  # === Deleting files
46
103
  #
47
104
  # FUSE confirms path is a file before we call {#can_delete?} then {#delete}
@@ -105,12 +162,12 @@ module FuseFS
105
162
 
106
163
  # File size
107
164
  # @abstract FuseFS api
108
- # @return [Fixnum] the size in byte of a file (lots of applications rely on this being accurate )
109
- def size(path);return 0;end
165
+ # @return [Integer] the size in byte of a file (lots of applications rely on this being accurate )
166
+ def size(path); read_file(path).length ;end
110
167
 
111
168
  # File time information. RFuseFS extension.
112
169
  # @abstract FuseFS api
113
- # @return [Array<Fixnum, Time>] a 3 element array [ atime, mtime. ctime ] (good for rsync etc)
170
+ # @return [Array<Integer, Time>] a 3 element array [ atime, mtime. ctime ] (good for rsync etc)
114
171
  def times(path);return INIT_TIMES;end
115
172
 
116
173
  # @abstract FuseFS api
@@ -176,21 +233,29 @@ module FuseFS
176
233
  def raw_open(path,mode,rfusefs = nil);end
177
234
 
178
235
  # RFuseFS extension.
236
+ # @abstract FuseFS api
179
237
  #
180
- # Truncate file at path (or filehandle raw) to offset bytes. Called immediately after a file is opened
181
- # for write without append.
238
+ # @overload raw_truncate(path,offset,raw)
239
+ # Truncate an open file to offset bytes
240
+ # @param [String] path
241
+ # @param [Integer] offset
242
+ # @param [Object] raw the filehandle returned from {#raw_open}
243
+ # @return [void]
182
244
  #
183
- # This method can also be invoked (without raw) outside of an open file context. See
184
- # FUSE documentation on truncate() vs ftruncate()
185
- # @abstract FuseFS api
186
- # @return [void]
187
- def raw_truncate(path,off,raw=nil);end
245
+ # @overload raw_truncate(path,offset)
246
+ # Optionally truncate a file to offset bytes directly
247
+ # @param [String] path
248
+ # @param [Integer] offset
249
+ # @return [Boolean]
250
+ # if truncate has been performed, otherwise the truncation will be performed with {#read_file} and {#write_to}
251
+ #
252
+ def raw_truncate(path,offset,raw=nil);end
188
253
 
189
254
  # Read _sz_ bytes from file at path (or filehandle raw) starting at offset off
190
- #
255
+ #
191
256
  # @param [String] path
192
- # @param [Fixnum] offset
193
- # @param [Fixnum] size
257
+ # @param [Integer] offset
258
+ # @param [Integer] size
194
259
  # @param [Object] raw the filehandle returned by {#raw_open}
195
260
  # @abstract FuseFS api
196
261
  # @return [void]
@@ -201,17 +266,36 @@ module FuseFS
201
266
  # @return [void]
202
267
  def raw_write(path,off,sz,buf,raw=nil);end
203
268
 
269
+
270
+ # Sync buffered data to your filesystem
271
+ # @param [String] path
272
+ # @param [Boolena] datasync only sync user data, not metadata
273
+ # @param [Object] raw the filehandle return by {#raw_open}
274
+ def raw_sync(path,datasync,raw=nil);end
275
+
204
276
  # Close the file previously opened at path (or filehandle raw)
205
277
  # @abstract FuseFS api
206
278
  # @return [void]
207
279
  def raw_close(path,raw=nil);end
208
280
 
209
281
  # RFuseFS extension.
210
- # Extended attributes. These will be set/retrieved/removed directly
282
+ # Extended attributes.
283
+ # @param [String] path
284
+ # @return [Hash] extended attributes for this path.
285
+ # The returned object will be manipulated directly using :[] :[]=,, :keys and :delete
286
+ # so the default (a new empty hash on every call) will not retain attributes that are set
287
+ # @abstract FuseFS api
288
+ def xattr(path); {} ; end
289
+
290
+ # RFuseFS extensions.
291
+ # File system statistics
211
292
  # @param [String] path
212
- # @return [Hash] extended attributes for this path
293
+ # @return [Array<Integer>] the statistics
294
+ # used_space (in bytes), used_files, max_space, max_files
295
+ # See {StatsHelper}
296
+ # @return [RFuse::StatVfs] or raw statistics
213
297
  # @abstract FuseFS api
214
- def xattr(path); return {}; end
298
+ def statistics(path); [0,0,0,0]; end
215
299
 
216
300
  # RFuseFS extension.
217
301
  # Called when the filesystem is mounted
@@ -16,15 +16,23 @@ module FuseFS
16
16
  @path = path
17
17
  @modified = false
18
18
  @contents = ""
19
+ @size = 0
19
20
  end
20
21
 
21
22
  def read(offset,size)
22
23
  contents[offset,size]
23
24
  end
24
25
 
26
+ def create
27
+ @contents = ""
28
+ @modified = true
29
+ end
30
+
25
31
  def write(offset,data)
32
+ # TODO: why append?
26
33
  if append? || offset >= contents.length
27
34
  #ignore offset
35
+ #TODO: should this zero fill?
28
36
  contents << data
29
37
  else
30
38
  contents[offset,data.length]=data
@@ -92,6 +100,10 @@ module FuseFS
92
100
  def initialize(root)
93
101
  @root = root
94
102
  @created_files = { }
103
+
104
+ # Keep track of changes to file counts and sizes made via Fuse - for #statfs
105
+ @adj_nodes = 0
106
+ @adj_size = 0
95
107
 
96
108
  #Define method missing for our filesystem
97
109
  #so we can just call all the API methods as required.
@@ -123,8 +135,8 @@ module FuseFS
123
135
 
124
136
  return wrap_context(ctx,__method__,path) if ctx
125
137
 
126
- uid = Process.gid
127
- gid = Process.uid
138
+ uid = Process.uid
139
+ gid = Process.gid
128
140
 
129
141
  if path == "/" || @root.directory?(path)
130
142
  #set "w" flag based on can_mkdir? || can_write? to path + "/._rfuse_check"
@@ -135,14 +147,13 @@ module FuseFS
135
147
  #nlink is set to 1 because apparently this makes find work.
136
148
  return RFuse::Stat.directory(mode,{ :uid => uid, :gid => gid, :nlink => 1, :atime => atime, :mtime => mtime, :ctime => ctime })
137
149
  elsif @created_files.has_key?(path)
138
- now = Time.now.to_i
139
- return RFuse::Stat.file(@created_files[path],{ :uid => uid, :gid => gid, :atime => now, :mtime => now, :ctime => now })
150
+ return @created_files[path]
140
151
  elsif @root.file?(path)
141
152
  #Set mode from can_write and executable
142
153
  mode = 0444
143
154
  mode |= 0222 if @root.can_write?(path)
144
155
  mode |= 0111 if @root.executable?(path)
145
- size = @root.size(path)
156
+ size = size(path)
146
157
  atime,mtime,ctime = @root.times(path)
147
158
  return RFuse::Stat.file(mode,{ :uid => uid, :gid => gid, :size => size, :atime => atime, :mtime => mtime, :ctime => ctime })
148
159
  else
@@ -160,6 +171,7 @@ module FuseFS
160
171
  end
161
172
 
162
173
  @root.mkdir(path)
174
+ @adj_nodes += 1
163
175
  end #mkdir
164
176
 
165
177
  def mknod(ctx,path,mode,major,minor)
@@ -170,10 +182,15 @@ module FuseFS
170
182
  raise Errno::EACCES.new(path)
171
183
  end
172
184
 
173
- @created_files[path] = mode
185
+ now = Time.now
186
+ stat = RFuse::Stat.file(mode,{ :uid => Process.uid, :gid => Process.gid, :atime => now, :mtime => now, :ctime => now })
187
+
188
+ @created_files[path] = stat
189
+ @adj_nodes += 1
174
190
  end #mknod
175
191
 
176
192
  #ftruncate - eg called after opening a file for write without append
193
+ #sizes are adjusted at file close
177
194
  def ftruncate(ctx,path,offset,ffi)
178
195
 
179
196
  return wrap_context(ctx,__method__,path,offset,ffi) if ctx
@@ -182,10 +199,11 @@ module FuseFS
182
199
 
183
200
  if fh.raw
184
201
  @root.raw_truncate(path,offset,fh.raw)
185
- elsif (offset <= 0)
186
- fh.contents = ""
187
- else
188
- fh.contents = fh.contents[0..offset]
202
+ if (offset <= 0)
203
+ fh.contents = ""
204
+ else
205
+ fh.contents = fh.contents[0..offset]
206
+ end
189
207
  end
190
208
  end
191
209
 
@@ -197,14 +215,16 @@ module FuseFS
197
215
  raise Errno::EACESS.new(path)
198
216
  end
199
217
 
218
+ current_size = size(path)
200
219
  unless @root.raw_truncate(path,offset)
201
220
  contents = @root.read_file(path)
202
221
  if (offset <= 0)
203
- @root.write_to(path,"")
222
+ @root.write_to(path,"")
204
223
  elsif offset < contents.length
205
224
  @root.write_to(path,contents[0..offset] )
206
225
  end
207
226
  end
227
+ @adj_size = @adj_size - current_size + (offset <= 0 ? 0 : offset)
208
228
  end #truncate
209
229
 
210
230
  # Open. Create a FileHandler and store in fuse file info
@@ -222,17 +242,15 @@ module FuseFS
222
242
  end
223
243
 
224
244
  unless fh.raw
225
-
226
245
  if fh.rdonly?
227
246
  fh.contents = @root.read_file(path)
228
- elsif fh.rdwr? || fh.wronly?
247
+ elsif fh.writing?
229
248
  unless @root.can_write?(path)
230
249
  raise Errno::EACCES.new(path)
231
250
  end
232
251
 
233
252
  if @created_files.has_key?(path)
234
- #we have an empty file
235
- fh.contents = "";
253
+ fh.create
236
254
  else
237
255
  if fh.rdwr? || fh.append?
238
256
  fh.contents = @root.read_file(path)
@@ -245,9 +263,9 @@ module FuseFS
245
263
  raise Errno::ENOPERM.new(path)
246
264
  end
247
265
  end
266
+
248
267
  #If we get this far, save our filehandle in the FUSE structure
249
268
  ffi.fh=fh
250
-
251
269
  end
252
270
 
253
271
  def read(ctx,path,size,offset,ffi)
@@ -285,7 +303,21 @@ module FuseFS
285
303
  else
286
304
  return fh.write(offset,buf)
287
305
  end
306
+ end
307
+
308
+ def fsync(ctx,path,datasync,ffi)
309
+ return wrap_context(ctx,__method__,path,datasync,ffi) if ctx
310
+ fh = ffi.fh
288
311
 
312
+ if fh && fh.raw
313
+ if FuseFS::RFUSEFS_COMPATIBILITY
314
+ @root.raw_sync(path,datasync != 0,fh.raw)
315
+ else
316
+ @root.raw_sync(path,datasync != 0)
317
+ end
318
+ else
319
+ flush(nil,path,ffi)
320
+ end
289
321
  end
290
322
 
291
323
  def flush(ctx,path,ffi)
@@ -298,13 +330,11 @@ module FuseFS
298
330
  #if it was created with mknod it now exists in the filesystem...
299
331
  @created_files.delete(path)
300
332
  end
301
-
302
333
  end
303
334
 
304
335
  def release(ctx,path,ffi)
305
336
  return wrap_context(ctx,__method__,path,ffi) if ctx
306
337
 
307
- flush(nil,path,ffi)
308
338
 
309
339
  fh = ffi.fh
310
340
  if fh && fh.raw
@@ -313,8 +343,10 @@ module FuseFS
313
343
  else
314
344
  @root.raw_close(path)
315
345
  end
346
+ else
347
+ # Probably just had flush called, but no harm calling it again
348
+ flush(nil,path,ffi)
316
349
  end
317
-
318
350
  end
319
351
 
320
352
  #def chmod(path,mode)
@@ -327,9 +359,7 @@ module FuseFS
327
359
  return wrap_context(ctx,__method__,path,actime,modtime) if ctx
328
360
 
329
361
  #Touch...
330
- if @root.respond_to?(:touch)
331
- @root.touch(path,modtime)
332
- end
362
+ @root.touch(path,modtime) if @root.respond_to?(:touch)
333
363
  end
334
364
 
335
365
  def unlink(ctx,path)
@@ -338,7 +368,10 @@ module FuseFS
338
368
  unless @root.can_delete?(path)
339
369
  raise Errno::EACCES.new(path)
340
370
  end
341
- @created_files.delete(path)
371
+
372
+ @adj_size = @adj_size - size(path)
373
+
374
+ @created_files.delete(path)
342
375
  @root.delete(path)
343
376
  end
344
377
 
@@ -371,8 +404,8 @@ module FuseFS
371
404
  #def link(path,as)
372
405
  #end
373
406
 
374
- def setxattr(ctx,path,name,value)
375
- return wrap_context(ctx,__method__,path,name,value) if ctx
407
+ def setxattr(ctx,path,name,value,flags)
408
+ return wrap_context(ctx,__method__,path,name,value,flags) if ctx
376
409
  @root.xattr(path)[name]=value
377
410
  end
378
411
 
@@ -399,12 +432,46 @@ module FuseFS
399
432
  #def releasedir(path,ffi)
400
433
  #end
401
434
 
435
+ #
402
436
  #def fsyncdir(path,meta,ffi)
403
437
  #end
404
438
 
405
439
  # Some random numbers to show with df command
406
- #def statfs(path)
407
- #end
440
+ # bsize preferred block size = 1K unless @root provides something different
441
+ # frsize = bsize (but apparently unused)
442
+ # blocks = total number of blocks
443
+ # bfree = number of free blocks
444
+ # bavail = bfree if mounted -o allow_other
445
+ # files = count of all files
446
+ # ffree - count of free file inode
447
+ #
448
+ def statfs(ctx,path)
449
+ return wrap_context(ctx,__method__,path) if ctx
450
+ block_size = 1024
451
+
452
+ stats = @root.statistics(path)
453
+ case stats
454
+ when Array
455
+ used_space, used_files, total_space, total_files = stats
456
+ used_files ||= 0
457
+ used_space ||= 0
458
+ total_files ||= used_files
459
+ total_space ||= used_space
460
+ result = RFuse::StatVfs.new(
461
+ "bsize" => block_size,
462
+ "frsize" => block_size,
463
+ "blocks" => total_space / block_size,
464
+ "bfree" => (total_space - used_space)/block_size,
465
+ "bavail" => (total_space - used_space)/block_size,
466
+ "files" => total_files,
467
+ "ffree" => (total_files - used_files)
468
+ )
469
+ return result
470
+ else
471
+ #expected to quack like rfuse:statvfs
472
+ return stats
473
+ end
474
+ end
408
475
 
409
476
  def mounted()
410
477
  @root.mounted()
@@ -431,5 +498,9 @@ module FuseFS
431
498
  self.class.context(ctx) { send(method,nil,*args) }
432
499
  end
433
500
 
501
+ def size(path)
502
+ @root.respond_to?(:size) ? @root.size(path) : @root.read_file(path).length
503
+ end
504
+
434
505
  end #class RFuseFS
435
506
  end #Module FuseFS
@@ -21,9 +21,15 @@ module FuseFS
21
21
 
22
22
  DEFAULT_FS = FuseDir.new()
23
23
 
24
- def initialize()
24
+ # @return [StatsHelper] helper for filesystem accounting (df etc)
25
+ attr_reader :stats
26
+
27
+ def initialize(stats = nil)
25
28
  @subdirs = Hash.new(nil)
26
29
  @files = Hash.new(nil)
30
+ @xattr = Hash.new() { |h,k| h[k] = Hash.new }
31
+ @stats = stats || StatsHelper.new()
32
+ @stats.adjust(0,1)
27
33
  end
28
34
 
29
35
  def split_path(path)
@@ -57,6 +63,13 @@ module FuseFS
57
63
  end
58
64
  end
59
65
 
66
+ # Extended attributes
67
+ def xattr(path)
68
+ pathmethod(:xattr,path) do | path |
69
+ @xattr[path]
70
+ end
71
+ end
72
+
60
73
  def read_file(path)
61
74
  pathmethod(:read_file,path) do |filename|
62
75
  @files[filename].to_s
@@ -78,6 +91,14 @@ module FuseFS
78
91
 
79
92
  def write_to(path,contents)
80
93
  pathmethod(:write_to,path,contents) do |filename, filecontents |
94
+ adj_size = filecontents.to_s.length
95
+ adj_nodes = 1
96
+ if @files.has_key?(filename)
97
+ adj_size = adj_size - @files[filename].to_s.length
98
+ adj_nodes = 0
99
+ end
100
+ @stats.adjust(adj_size,adj_nodes)
101
+
81
102
  @files[filename] = filecontents
82
103
  end
83
104
  end
@@ -91,7 +112,8 @@ module FuseFS
91
112
 
92
113
  def delete(path)
93
114
  pathmethod(:delete,path) do |filename|
94
- @files.delete(filename)
115
+ contents = @files.delete(filename)
116
+ @stats.adjust(-contents.to_s.length,-1)
95
117
  end
96
118
  end
97
119
 
@@ -104,7 +126,7 @@ module FuseFS
104
126
 
105
127
  def mkdir(path,dir=nil)
106
128
  pathmethod(:mkdir,path,dir) do | dirname,dirobj |
107
- dirobj ||= MetaDir.new
129
+ dirobj ||= MetaDir.new(@stats)
108
130
  @subdirs[dirname] = dirobj
109
131
  end
110
132
  end
@@ -119,6 +141,7 @@ module FuseFS
119
141
  def rmdir(path)
120
142
  pathmethod(:rmdir,path) do |dirname|
121
143
  @subdirs.delete(dirname)
144
+ @stats.adjust(0,-1)
122
145
  end
123
146
  end
124
147
 
@@ -135,6 +158,8 @@ module FuseFS
135
158
  if @files.has_key?(from_base)
136
159
  return false unless can_delete?(from_base) && to_fusefs.can_write?(to_path)
137
160
  to_fusefs.write_to(to_path,@files[from_base])
161
+ to_fusefs.xattr(to_path).merge!(@xattr[from_base])
162
+ @xattr.delete(from_base)
138
163
  @files.delete(from_base)
139
164
  elsif @subdirs.has_key?(from_base)
140
165
  # we don't check can_rmdir? because that would prevent us
@@ -142,7 +167,11 @@ module FuseFS
142
167
  return false unless mount_user? && to_fusefs.can_mkdir?(to_path)
143
168
  begin
144
169
  to_fusefs.mkdir(to_path,@subdirs[from_base])
170
+ to_fusefs.xattr(to_path).merge!(@xattr[from_base])
171
+ @xattr.delete(from_base)
145
172
  @subdirs.delete(from_base)
173
+ @stats.adjust(0,-1)
174
+ return true
146
175
  rescue ArgumentError
147
176
  # to_rest does not support mkdir with an arbitrary object
148
177
  return false
@@ -188,7 +217,20 @@ module FuseFS
188
217
  end
189
218
  end
190
219
 
191
- default_methods = FuseDir.public_instance_methods.select { |m|
220
+ # path is ignored? - recursively calculate for all subdirs - but cache and then rely on fuse to keep count
221
+ def statistics(path)
222
+ pathmethod(:statistics,path) do |stats_path|
223
+ if @subdirs.has_key?(stats_path)
224
+ #unlike all the other functions where this metadir applies
225
+ #the function to @subdirs - we need to pass it on
226
+ @subdirs[stats_path].statistics("/")
227
+ else
228
+ @stats.to_statistics
229
+ end
230
+ end
231
+ end
232
+
233
+ default_methods = FuseDir.public_instance_methods.select { |m|
192
234
  ![:mounted,:unmounted].include?(m) &&
193
235
  !self.public_method_defined?(m) && FuseDir.instance_method(m).owner == FuseDir
194
236
  }