rfusefs 1.0.2.RC0 → 1.0.2.RC1

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