rfusefs 1.1.0 → 1.1.1.rc20201114.37

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ module FuseFS
2
+
3
+ # A FuseFS over an existing directory
4
+ class DirLink < FuseDir
5
+
6
+ def initialize(dir)
7
+ File.directory?(dir) or raise ArgumentError, "DirLink.initialize expects a valid directory!"
8
+ @base = dir
9
+ end
10
+
11
+ def directory?(path)
12
+ File.directory?(File.join(@base,path))
13
+ end
14
+
15
+ def file?(path)
16
+ File.file?(File.join(@base,path))
17
+ end
18
+
19
+ def size(path)
20
+ File.size(File.join(@base,path))
21
+ end
22
+
23
+ def contents(path)
24
+ fn = File.join(@base,path)
25
+ Dir.entries(fn).map { |file|
26
+ file = file.sub(/^#{fn}\/?/,'')
27
+ if ['..','.'].include?(file)
28
+ nil
29
+ else
30
+ file
31
+ end
32
+ }.compact.sort
33
+ end
34
+
35
+ def read_file(path)
36
+ fn = File.join(@base,path)
37
+ if File.file?(fn)
38
+ IO.read(fn)
39
+ else
40
+ 'No such file'
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,287 @@
1
+ module FuseFS
2
+
3
+ # A full in-memory filesystem defined with hashes. It is writable to the
4
+ # user that mounted it
5
+ # may create and edit files within it, as well as the programmer
6
+ # === Usage
7
+ # root = Metadir.new()
8
+ # root.mkdir("/hello")
9
+ # root.write_to("/hello/world","Hello World!\n")
10
+ # root.write_to("/hello/everybody","Hello Everyone!\n")
11
+ #
12
+ # FuseFS.start(mntpath,root)
13
+ #
14
+ # Because Metadir is fully recursive, you can mount your own or other defined
15
+ # directory structures under it. For example, to mount a dictionary filesystem
16
+ # (see samples/dictfs.rb), use:
17
+ #
18
+ # root.mkdir("/dict",DictFS.new())
19
+ #
20
+ class MetaDir
21
+
22
+ DEFAULT_FS = FuseDir.new()
23
+
24
+ # @return [StatsHelper] helper for filesystem accounting (df etc)
25
+ attr_reader :stats
26
+
27
+ def initialize(stats = nil)
28
+ @subdirs = Hash.new(nil)
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)
33
+ end
34
+
35
+ def split_path(path)
36
+ DEFAULT_FS.split_path(path)
37
+ end
38
+
39
+ def scan_path
40
+ DEFAULT_FS.scan_path(path)
41
+ end
42
+
43
+ def directory?(path)
44
+ pathmethod(:directory?,path) do |filename|
45
+ !filename || filename == "/" || @subdirs.has_key?(filename)
46
+ end
47
+ end
48
+
49
+ def file?(path)
50
+ pathmethod(:file?,path) do |filename|
51
+ @files.has_key?(filename)
52
+ end
53
+ end
54
+
55
+ #List directory contents
56
+ def contents(path)
57
+ pathmethod(:contents,path) do | filename |
58
+ if !filename
59
+ (@files.keys + @subdirs.keys).sort.uniq
60
+ else
61
+ @subdirs[filename].contents("/")
62
+ end
63
+ end
64
+ end
65
+
66
+ # Extended attributes
67
+ def xattr(path)
68
+ pathmethod(:xattr,path) do | path |
69
+ @xattr[path]
70
+ end
71
+ end
72
+
73
+ def read_file(path)
74
+ pathmethod(:read_file,path) do |filename|
75
+ @files[filename].to_s
76
+ end
77
+ end
78
+
79
+ def size(path)
80
+ pathmethod(:size,path) do | filename |
81
+ return @files[filename].to_s.length
82
+ end
83
+ end
84
+
85
+ #can_write only applies to files... see can_mkdir for directories...
86
+ def can_write?(path)
87
+ pathmethod(:can_write?,path) do |filename|
88
+ return mount_user?
89
+ end
90
+ end
91
+
92
+ def write_to(path,contents)
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
+
102
+ @files[filename] = filecontents
103
+ end
104
+ end
105
+
106
+ # Delete a file
107
+ def can_delete?(path)
108
+ pathmethod(:can_delete?,path) do |filename|
109
+ return mount_user?
110
+ end
111
+ end
112
+
113
+ def delete(path)
114
+ pathmethod(:delete,path) do |filename|
115
+ contents = @files.delete(filename)
116
+ @stats.adjust(-contents.to_s.length,-1)
117
+ end
118
+ end
119
+
120
+ #mkdir - does not make intermediate dirs!
121
+ def can_mkdir?(path)
122
+ pathmethod(:can_mkdir?,path) do |dirname|
123
+ return mount_user?
124
+ end
125
+ end
126
+
127
+ def mkdir(path,dir=nil)
128
+ pathmethod(:mkdir,path,dir) do | dirname,dirobj |
129
+ dirobj ||= MetaDir.new(@stats)
130
+ @subdirs[dirname] = dirobj
131
+ end
132
+ end
133
+
134
+ # Delete an existing directory make sure it is not empty
135
+ def can_rmdir?(path)
136
+ pathmethod(:can_rmdir?,path) do |dirname|
137
+ return mount_user? && @subdirs.has_key?(dirname) && @subdirs[dirname].contents("/").empty?
138
+ end
139
+ end
140
+
141
+ def rmdir(path)
142
+ pathmethod(:rmdir,path) do |dirname|
143
+ @subdirs.delete(dirname)
144
+ @stats.adjust(0,-1)
145
+ end
146
+ end
147
+
148
+ def rename(from_path,to_path,to_fusefs = self)
149
+
150
+ from_base,from_rest = split_path(from_path)
151
+
152
+ case
153
+ when !from_base
154
+ # Shouldn't ever happen.
155
+ raise Errno::EACCES.new("Can't move root")
156
+ when !from_rest
157
+ # So now we have a file or directory to move
158
+ if @files.has_key?(from_base)
159
+ return false unless can_delete?(from_base) && to_fusefs.can_write?(to_path)
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)
163
+ @files.delete(from_base)
164
+ elsif @subdirs.has_key?(from_base)
165
+ # we don't check can_rmdir? because that would prevent us
166
+ # moving non empty directories
167
+ return false unless mount_user? && to_fusefs.can_mkdir?(to_path)
168
+ begin
169
+ to_fusefs.mkdir(to_path,@subdirs[from_base])
170
+ to_fusefs.xattr(to_path).merge!(@xattr[from_base])
171
+ @xattr.delete(from_base)
172
+ @subdirs.delete(from_base)
173
+ @stats.adjust(0,-1)
174
+ return true
175
+ rescue ArgumentError
176
+ # to_rest does not support mkdir with an arbitrary object
177
+ return false
178
+ end
179
+ else
180
+ #We shouldn't get this either
181
+ return false
182
+ end
183
+ when @subdirs.has_key?(from_base)
184
+ begin
185
+ if to_fusefs != self
186
+ #just keep recursing..
187
+ return @subdirs[from_base].rename(from_rest,to_path,to_fusefs)
188
+ else
189
+ to_base,to_rest = split_path(to_path)
190
+ if from_base == to_base
191
+ #mv within a subdir, just pass it on
192
+ return @subdirs[from_base].rename(from_rest,to_rest)
193
+ else
194
+ #OK, this is the tricky part, we want to move something further down
195
+ #our tree into something in another part of the tree.
196
+ #from this point on we keep a reference to the fusefs that owns
197
+ #to_path (ie us) and pass it down, but only if the eventual path
198
+ #is writable anyway!
199
+ if (file?(to_path))
200
+ return false unless can_write?(to_path)
201
+ else
202
+ return false unless can_mkdir?(to_path)
203
+ end
204
+
205
+ return @subdirs[from_base].rename(from_rest,to_path,self)
206
+ end
207
+ end
208
+ rescue NoMethodError
209
+ #sub dir doesn't support rename
210
+ return false
211
+ rescue ArgumentError
212
+ #sub dir doesn't support rename with additional to_fusefs argument
213
+ return false
214
+ end
215
+ else
216
+ return false
217
+ end
218
+ end
219
+
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|
234
+ ![:mounted,:unmounted].include?(m) &&
235
+ !self.public_method_defined?(m) && FuseDir.instance_method(m).owner == FuseDir
236
+ }
237
+
238
+ default_methods.each do |m|
239
+ define_method(m) do |*args|
240
+ pathmethod(m,*args) { |*args| DEFAULT_FS.send(m,*args) }
241
+ end
242
+ end
243
+
244
+ private
245
+ # is the accessing user the same as the user that mounted our FS?, used for
246
+ # all write activity
247
+ def mount_user?
248
+ return Process.uid == FuseFS.reader_uid
249
+ end
250
+
251
+ #All our FuseFS methods follow the same pattern...
252
+ def pathmethod(method, path,*args)
253
+ base,rest = split_path(path)
254
+
255
+ case
256
+ when ! base
257
+ #request for the root of our fs
258
+ yield(nil,*args)
259
+ when ! rest
260
+ #base is the filename, no more directories to traverse
261
+ yield(base,*args)
262
+ when @subdirs.has_key?(base)
263
+ #base is a subdirectory, pass it on if we can
264
+ begin
265
+ @subdirs[base].send(method,rest,*args)
266
+ rescue NoMethodError
267
+ #Oh well
268
+ return DEFAULT_FS.send(method,rest,*args)
269
+ rescue ArgumentError
270
+ #can_mkdir,mkdir
271
+ if args.pop.nil?
272
+ #possibly a default arg, try sending again with one fewer arg
273
+ @subdirs[base].send(method,rest,*args)
274
+ else
275
+ #definitely not a default arg, reraise
276
+ Kernel.raise
277
+ end
278
+ end
279
+ else
280
+ #return the default response
281
+ return DEFAULT_FS.send(method,path,*args)
282
+ end
283
+ end
284
+
285
+
286
+ end
287
+ end
@@ -0,0 +1,442 @@
1
+
2
+ module FuseFS
3
+ begin
4
+ require 'ffi-xattr'
5
+ HAS_FFI_XATTR = true
6
+ rescue LoadError
7
+ warn "ffi-xattr not available, extended attributes will not be mapped"
8
+ HAS_FFI_XATTR = false
9
+ end
10
+
11
+ # A FuseFS that maps files from their original location into a new path
12
+ # eg tagged audio files can be mapped by title etc...
13
+ #
14
+ class PathMapperFS < FuseDir
15
+
16
+ # Represents a mapped file or directory
17
+ class MNode
18
+
19
+ # Merge extended attributes with the ones from the underlying file
20
+ class XAttr
21
+
22
+ attr_reader :node, :file_xattr
23
+
24
+ def initialize(node)
25
+ @node = node
26
+ @file_xattr = ::Xattr.new(node.real_path.to_s) if node.file? && HAS_FFI_XATTR
27
+ end
28
+
29
+ def [](key)
30
+ additional[key] || (file_xattr && file_xattr[key])
31
+ end
32
+
33
+ def []=(key,value)
34
+ raise Errno::EACCES if additional.has_key?(key) || node.directory?
35
+ file_xattr[key] = value if file_xattr
36
+ end
37
+
38
+ def delete(key)
39
+ raise Errno::EACCES if additional.has_key?(key) || node.directory?
40
+ file_xattr.remove(key) if file_xattr
41
+ end
42
+
43
+ def keys
44
+ if file_xattr
45
+ additional.keys + file_xattr.list
46
+ else
47
+ additional.keys
48
+ end
49
+ end
50
+
51
+
52
+ def additional
53
+ @node[:xattr] || {}
54
+ end
55
+
56
+ end
57
+
58
+ # @return [Hash<String,MNode>] list of files in a directory, nil for file nodes
59
+ attr_reader :files
60
+
61
+ # Useful when mapping a file to store attributes against the
62
+ # parent directory
63
+ # @return [MNode] parent directory
64
+ attr_reader :parent
65
+
66
+ #
67
+ # @return [Hash] metadata for this node
68
+ attr_reader :options
69
+
70
+ #
71
+ # @return [String] path to backing file, or nil for directory nodes
72
+ attr_reader :real_path
73
+
74
+
75
+ # @!visibility private
76
+ def initialize(parent_dir,stats)
77
+ @parent = parent_dir
78
+ @files = {}
79
+ @options = {}
80
+ @stats = stats
81
+ @stats_size = 0
82
+ @stats.adjust(0,1)
83
+ end
84
+
85
+ # @!visibility private
86
+ def init_file(real_path,options)
87
+ @options.merge!(options)
88
+ @real_path = real_path
89
+ @files = nil
90
+ updated
91
+ self
92
+ end
93
+
94
+ def init_dir(options)
95
+ @options.merge!(options)
96
+ self
97
+ end
98
+
99
+ # @return [Boolean] true if node represents a file, otherwise false
100
+ def file?
101
+ real_path && true
102
+ end
103
+
104
+ # @return [Boolean] true if node represents a directory, otherwise false
105
+ def directory?
106
+ files && true
107
+ end
108
+
109
+ # @return [Boolean] true if node is the root directory
110
+ def root?
111
+ @parent.nil?
112
+ end
113
+
114
+ # Compatibility and convenience method
115
+ # @param [:pm_real_path,String,Symbol] key
116
+ # @return [String] {#real_path} if key == :pm_real_path
117
+ # @return [MNode] the node representing the file named key
118
+ # @return [Object] shortcut for {#options}[key]
119
+ def[](key)
120
+ case key
121
+ when :pm_real_path
122
+ real_path
123
+ when String
124
+ files[key]
125
+ else
126
+ options[key]
127
+ end
128
+ end
129
+
130
+ # Convenience method to set metadata into {#options}
131
+ def[]=(key,value)
132
+ options[key]=value
133
+ end
134
+
135
+ def xattr
136
+ @xattr ||= XAttr.new(self)
137
+ end
138
+
139
+ def deleted
140
+ @stats.adjust(-@stats_size,-1)
141
+ @stats_size = 0
142
+ end
143
+
144
+ def updated
145
+ new_size = File.size(real_path)
146
+ @stats.adjust(new_size - @stats_size)
147
+ @stats_size = new_size
148
+ end
149
+ end
150
+
151
+ # Convert FuseFS raw_mode strings back to IO open mode strings
152
+ def self.open_mode(raw_mode)
153
+ case raw_mode
154
+ when "r"
155
+ "r"
156
+ when "ra"
157
+ "r" #not really sensible..
158
+ when "rw"
159
+ "r+"
160
+ when "rwa"
161
+ "a+"
162
+ when "w"
163
+ "w"
164
+ when "wa"
165
+ "a"
166
+ end
167
+ end
168
+
169
+ # should raw file access should be used - useful for binary files
170
+ # @return [Boolean]
171
+ # default is false
172
+ attr_accessor :use_raw_file_access
173
+
174
+ # should filesystem support writing through to the real files
175
+ # @return [Boolean]
176
+ # default is false
177
+ attr_accessor :allow_write
178
+
179
+ #
180
+ # @return [StatsHelper] accumulated filesystem statistics
181
+ attr_reader :stats
182
+
183
+ # Creates a new Path Mapper filesystem over an existing directory
184
+ # @param [String] dir
185
+ # @param [Hash] options
186
+ # @yieldparam [String] file path to map
187
+ # @yieldreturn [String]
188
+ # @see #initialize
189
+ # @see #map_directory
190
+ def PathMapperFS.create(dir,options={ },&block)
191
+ pm_fs = self.new(options)
192
+ pm_fs.map_directory(dir,&block)
193
+ return pm_fs
194
+ end
195
+
196
+ # Create a new Path Mapper filesystem
197
+ # @param [Hash] options
198
+ # @option options [Boolean] :use_raw_file_access
199
+ # @option options [Boolean] :allow_write
200
+ # @option options [Integer] :max_space available space for writes (for df)
201
+ # @option options [Integer] :max_nodes available nodes for writes (for df)
202
+ def initialize(options = { })
203
+ @stats = StatsHelper.new()
204
+ @stats.max_space = options[:max_space]
205
+ @stats.max_nodes = options[:max_nodes]
206
+ @root = MNode.new(nil,@stats)
207
+ @use_raw_file_access = options[:use_raw_file_access]
208
+ @allow_write = options[:allow_write]
209
+ end
210
+
211
+ # Recursively find all files and map according to the given block
212
+ # @param [String...] dirs directories to list
213
+ # @yieldparam [String] file path to map
214
+ # @yieldreturn [String] the mapped path
215
+ # @yieldreturn nil to skip mapping this file
216
+ def map_directory(*dirs)
217
+ require 'find'
218
+ Find.find(*dirs) do |file|
219
+ new_path = yield file
220
+ map_file(file,new_path) if new_path
221
+ end
222
+ end
223
+ alias :mapDirectory :map_directory
224
+
225
+
226
+ # Add (or replace) a mapped file
227
+ #
228
+ # @param [String] real_path pointing at the real file location
229
+ # @param [String] new_path the mapped path
230
+ # @param [Hash<Symbol,Object>] options metadata for this path
231
+ # @option options [Hash<String,String>] :xattr hash to be used as extended attributes
232
+ # @return [MNode]
233
+ # a node representing the mapped path. See {#node}
234
+ def map_file(real_path,new_path,options = {})
235
+ make_node(new_path).init_file(real_path,options)
236
+ end
237
+ alias :mapFile :map_file
238
+
239
+ # Retrieve in memory node for a mapped path
240
+ #
241
+ # @param [String] path
242
+ # @return [MNode] in memory node at path
243
+ # @return nil if path does not exist in the filesystem
244
+ def node(path)
245
+ path_components = scan_path(path)
246
+
247
+ #not actually injecting anything here, we're just following the hash of hashes...
248
+ path_components.inject(@root) { |dir,file|
249
+ break unless dir.files[file]
250
+ dir.files[file]
251
+ }
252
+ end
253
+
254
+ # Takes a mapped file name and returns the original real_path
255
+ def unmap(path)
256
+ node = node(path)
257
+ (node && node.file?) ? node.real_path : nil
258
+ end
259
+
260
+ # Deletes files and directories.
261
+ # Yields each {#node} in the filesystem and deletes it if the block returns true
262
+ #
263
+ # Useful if your filesystem is periodically remapping the entire contents and you need
264
+ # to delete entries that have not been touched in the latest scan
265
+ #
266
+ # @yieldparam [Hash] filesystem node
267
+ # @yieldreturn [true,false] should this node be deleted
268
+ def cleanup(&block)
269
+ recursive_cleanup(@root,&block)
270
+ end
271
+
272
+
273
+ # @!visibility private
274
+ def directory?(path)
275
+ possible_dir = node(path)
276
+ possible_dir && possible_dir.directory?
277
+ end
278
+
279
+ # @!visibility private
280
+ def contents(path)
281
+ node(path).files.keys
282
+ end
283
+
284
+ # @!visibility private
285
+ def file?(path)
286
+ filename = unmap(path)
287
+ filename && File.file?(filename)
288
+ end
289
+
290
+ # @!visibility private
291
+ # only called if option :raw_reads is not set
292
+ def read_file(path)
293
+ IO.read(unmap(path))
294
+ end
295
+
296
+ # @!visibility private
297
+ # We can only write to existing files
298
+ # because otherwise we don't have anything to back it
299
+ def can_write?(path)
300
+ @allow_write && file?(path)
301
+ end
302
+
303
+ # Note we don't impleemnt can_mkdir? so this can
304
+ # only be called by code. Really only useful to
305
+ # create empty directories
306
+ def mkdir(path,options = {})
307
+ make_node(path).init_dir(options)
308
+ end
309
+
310
+ # @!visibility private
311
+ def write_to(path,contents)
312
+ node = node(path)
313
+ File.open(node.real_path,"w") { |f| f.print(contents) }
314
+ node.updated
315
+ end
316
+
317
+ # @!visibility private
318
+ def size(path)
319
+ File.size(unmap(path))
320
+ end
321
+
322
+ # @!visibility private
323
+ def times(path)
324
+ realpath = unmap(path)
325
+ if (realpath)
326
+ stat = File.stat(realpath)
327
+ return [ stat.atime, stat.mtime, stat.ctime ]
328
+ else
329
+ # We're a directory
330
+ return [0,0,0]
331
+ end
332
+ end
333
+
334
+ # @!visibility private
335
+ def xattr(path)
336
+ result = node(path).xattr
337
+ end
338
+
339
+ # @!visibility private
340
+ # Will create, store and return a File object for the underlying file
341
+ # for subsequent use with the raw_read/raw_close methods
342
+ # expects file? to return true before this method is called
343
+ def raw_open(path,mode,rfusefs = nil)
344
+
345
+ return false unless @use_raw_file_access
346
+
347
+ return false if mode.include?("w") && (!@allow_write)
348
+
349
+ @openfiles ||= Hash.new() unless rfusefs
350
+
351
+ real_path = unmap(path)
352
+
353
+ unless real_path
354
+ if rfusefs
355
+ raise Errno::ENOENT.new(path)
356
+ else
357
+ #fusefs will go on to call file?
358
+ return false
359
+ end
360
+ end
361
+
362
+ file = File.new(real_path,PathMapperFS.open_mode(mode))
363
+
364
+ @openfiles[path] = file unless rfusefs
365
+
366
+ return file
367
+ end
368
+
369
+ # @!visibility private
370
+ def raw_read(path,off,sz,file=nil)
371
+ file = @openfiles[path] unless file
372
+ file.sysseek(off)
373
+ file.sysread(sz)
374
+ end
375
+
376
+ # @!visibility private
377
+ def raw_write(path,offset,sz,buf,file=nil)
378
+ file = @openfiles[path] unless file
379
+ file.sysseek(offset)
380
+ file.syswrite(buf[0,sz])
381
+ end
382
+
383
+ # @!visibility private
384
+ def raw_sync(path,datasync,file=nil)
385
+ file = @openfiles[path] unless file
386
+ if datasync
387
+ file.fdatasync
388
+ else
389
+ file.sync
390
+ end
391
+ end
392
+
393
+ # @!visibility private
394
+ def raw_close(path,file=nil)
395
+ file = @openfiles.delete(path) unless file
396
+
397
+ if file && !file.closed?
398
+ begin
399
+ flags = file.fcntl(Fcntl::F_GETFL) & Fcntl::O_ACCMODE
400
+ if flags == Fcntl::O_WRONLY || flags == Fcntl::O_RDWR
401
+ #update stats
402
+ node = node(path)
403
+ node.updated if node
404
+ end
405
+ ensure
406
+ file.close
407
+ end
408
+ end
409
+
410
+ end
411
+
412
+ # @!visibility private
413
+ def statistics(path)
414
+ @stats.to_statistics
415
+ end
416
+
417
+ private
418
+
419
+ def make_node(path)
420
+ #split path into components
421
+ components = path.to_s.scan(/[^\/]+/)
422
+ components.inject(@root) { |parent_dir, file|
423
+ parent_dir.files[file] ||= MNode.new(parent_dir,@stats)
424
+ }
425
+ end
426
+
427
+ def recursive_cleanup(dir_node,&block)
428
+ dir_node.files.delete_if do |path,child|
429
+ del = if child.file?
430
+ yield child
431
+ else
432
+ recursive_cleanup(child,&block)
433
+ child.files.size == 0
434
+ end
435
+ child.deleted if del
436
+ del
437
+ end
438
+ end
439
+ end
440
+
441
+ end
442
+