rfusefs 1.0.0 → 1.0.1.RC0

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/.gitignore CHANGED
@@ -1,8 +1,8 @@
1
1
  #No hidden files (except for .gitignore)
2
2
  .*
3
3
  !.gitignore
4
+ !.travis.yml
4
5
  #Files generated by rake
5
- rfusefs.gemspec
6
6
  doc
7
7
  pkg
8
8
  #Jedit stupid default backup settings
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.8.7"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ before_install:
7
+ - sudo apt-get update -qq
8
+ - sudo apt-get install -qq libfuse-dev
data/README.rdoc CHANGED
@@ -16,22 +16,7 @@ RFuseFS is api compatible with FuseFS (0.7.0)
16
16
  FuseFS provides a layer of abstraction to a programmer who wants to create a
17
17
  virtual filesystem via FUSE.
18
18
 
19
- FuseFS programs consist of two parts:
20
-
21
- 1. FuseFS, which is defined in 'rfusefs.rb'
22
- 2. An object that defines a virtual directory by subclassing {FuseFS::FuseDir}
23
-
24
- To write a FuseFS program, you must:
25
-
26
- * Define and create a Directory object that responds to the methods required
27
- by FuseFS for its desired use.
28
-
29
- * Call
30
-
31
- FuseFS.start <virtualdir> <mountpoint> [mount_options]
32
-
33
- where <virtualdir> with an object defining your virtual directory.
34
- and <mountpoint> is a real directory on your filesystem
19
+ First define a virtual directory by subclassing {FuseFS::FuseDir}
35
20
 
36
21
  See samples under /samples and also the following starter classes
37
22
 
@@ -39,13 +24,19 @@ See samples under /samples and also the following starter classes
39
24
  * {FuseFS::MetaDir}
40
25
  * {FuseFS::DirLink}
41
26
  * {FuseFS::PathMapperFS}
27
+ * {FuseFS::SqliteMapperFS}
42
28
 
43
- To use the filesystem open up your favourite file browser/terminal and
29
+ Then start your filesystem with
30
+
31
+ * {FuseFS.start}
32
+ * {FuseFS.main}
33
+
34
+ Finally to use the filesystem open up your favourite file browser/terminal and
44
35
  explore the contents under <mountpoint>
45
36
 
46
37
  Happy Filesystem Hacking!
47
38
 
48
- === the hello world filesystem in 14 LOC
39
+ === the hello world filesystem in 16 LOC
49
40
 
50
41
  require 'rfusefs'
51
42
 
@@ -59,12 +50,13 @@ Happy Filesystem Hacking!
59
50
  def read_file(path)
60
51
  "Hello, World!\n"
61
52
  end
53
+ def size(path)
54
+ read_file(path).size
55
+ end
62
56
  end
63
57
 
64
- hellodir = HelloDir.new
65
-
66
58
  # Usage: #{$0} mountpoint [mount_options]
67
- FuseFS.start(hellodir,*ARGV)
59
+ FuseFS.main() { |options| HelloDir.new }
68
60
 
69
61
  == REQUIREMENTS:
70
62
 
data/Rakefile CHANGED
@@ -15,4 +15,5 @@ RSpec::Core::RakeTask.new("spec:fusefs") do |t|
15
15
  t.pattern = 'spec-fusefs/**/*_spec.rb'
16
16
  end
17
17
 
18
+ task :default => ["spec","spec:fusefs"]
18
19
  # vim: syntax=ruby
data/lib/fuse/fusedir.rb CHANGED
@@ -196,6 +196,21 @@ module FuseFS
196
196
  # @abstract FuseFS api
197
197
  def raw_close(path,raw=nil);end
198
198
 
199
+ # RFuseFS extension.
200
+ # Extended attributes. These will be set/retrieved/removed directly
201
+ # @param [String] path
202
+ # @return [Hash] extended attributes for this path
203
+ # @abstract FuseFS api
204
+ def xattr(path); return {}; end
205
+
206
+ # RFuseFS extension.
207
+ # Called when the filesystem is mounted
208
+ def mounted();end
209
+
210
+ # RFuseFS extension.
211
+ # Called when the filesystem is unmounted
212
+ def unmounted();end
213
+
199
214
  end
200
215
 
201
216
  DEFAULT_FS = FuseDir.new()
@@ -371,17 +371,27 @@ module FuseFS
371
371
  #def link(path,as)
372
372
  #end
373
373
 
374
- # def setxattr(path,name,value,size,flags)
375
- # end
374
+ def setxattr(ctx,path,name,value)
375
+ return wrap_context(ctx,__method__,path,name,value) if ctx
376
+ @root.xattr(path)[name]=value
377
+ end
376
378
 
377
- # def getxattr(path,name,size)
378
- # end
379
+ def getxattr(ctx,path,name)
380
+ return wrap_context(ctx,__method__,path,name) if ctx
381
+ result = @root.xattr(path)[name]
382
+ raise Errno::ENODATA.new("No attribute #{name}") unless result
383
+ result
384
+ end
379
385
 
380
- # def listxattr(path,size)
381
- # end
386
+ def listxattr(ctx,path)
387
+ return wrap_context(ctx,__method__,path) if ctx
388
+ @root.xattr(path).keys
389
+ end
382
390
 
383
- # def removexattr(path,name)
384
- # end
391
+ def removexattr(ctx,path,name)
392
+ return wrap_context(ctx,__method__,path,name) if ctx
393
+ @root.xattr(path).delete(name)
394
+ end
385
395
 
386
396
  #def opendir(path,ffi)
387
397
  #end
@@ -396,6 +406,14 @@ module FuseFS
396
406
  #def statfs(path)
397
407
  #end
398
408
 
409
+ def mounted()
410
+ @root.mounted()
411
+ end
412
+
413
+ def unmounted()
414
+ @root.unmounted()
415
+ end
416
+
399
417
  def self.context(ctx,&block)
400
418
  begin
401
419
  Thread.current[:fusefs_reader_uid] = ctx.uid
@@ -189,7 +189,8 @@ module FuseFS
189
189
  end
190
190
 
191
191
  default_methods = FuseDir.public_instance_methods.select { |m|
192
- !self.public_method_defined?(m) && FuseDir.instance_method(m).owner == FuseDir
192
+ ![:mounted,:unmounted].include?(m) &&
193
+ !self.public_method_defined?(m) && FuseDir.instance_method(m).owner == FuseDir
193
194
  }
194
195
 
195
196
  default_methods.each do |m|
@@ -1,10 +1,78 @@
1
1
 
2
2
  module FuseFS
3
3
 
4
- # A FuseFS that maps files from files from their original location into a new path
4
+ # A FuseFS that maps files from their original location into a new path
5
5
  # eg tagged audio files can be mapped by title etc...
6
+ #
6
7
  class PathMapperFS < FuseDir
7
- # Convert raw_mode strings to IO open mode strings
8
+
9
+ # Represents a mappted file or directory
10
+ class MNode
11
+
12
+ # @return [Hash<String,MNode>] list of files in a directory, nil for file nodes
13
+ attr_reader :files
14
+
15
+ # @return [MNode] parent directory
16
+ attr_reader :parent
17
+
18
+ # @return [Hash] metadata for this node
19
+ attr_reader :options
20
+
21
+ # @return [String] path to backing file, or nil for directory nodes
22
+ attr_reader :real_path
23
+
24
+ # @!visibility private
25
+ def initialize(parent_dir)
26
+ @parent = parent_dir
27
+ @files = {}
28
+ @options = {}
29
+ end
30
+
31
+ # @!visibility private
32
+ def init_file(real_path,options)
33
+ @options = options
34
+ @real_path = real_path
35
+ @files = nil
36
+ end
37
+
38
+ # @return [Boolean] true if node represents a file, otherwise false
39
+ def file?
40
+ real_path && true
41
+ end
42
+
43
+ # @return [Boolean] true if node represents a directory, otherwise false
44
+ def directory?
45
+ files && true
46
+ end
47
+
48
+ # @return [Boolean] true if node is the root directory
49
+ def root?
50
+ @parent.nil?
51
+ end
52
+
53
+ # Compatibility and convenience method
54
+ # @param [:pm_real_path,String,Symbol] key
55
+ # @return [String] {#real_path} if key == :pm_real_path
56
+ # @return [MNode] the node representing the file named key
57
+ # @return [Object] shortcut for {#options}[key]
58
+ def[](key)
59
+ case key
60
+ when :pm_real_path
61
+ real_path
62
+ when String
63
+ files[key]
64
+ else
65
+ options[key]
66
+ end
67
+ end
68
+
69
+ # Convenience method to set metadata into {#options}
70
+ def[]=(key,value)
71
+ options[key]=value
72
+ end
73
+ end
74
+
75
+ # Convert FuseFS raw_mode strings to IO open mode strings
8
76
  def self.open_mode(raw_mode)
9
77
  case raw_mode
10
78
  when "r"
@@ -12,107 +80,163 @@ module FuseFS
12
80
  when "ra"
13
81
  "r" #not really sensible..
14
82
  when "rw"
15
- "w+"
83
+ "r+"
16
84
  when "rwa"
17
85
  "a+"
18
- when
86
+ when "w"
19
87
  "w"
20
88
  when "wa"
21
89
  "a"
22
90
  end
23
91
  end
24
- attr_accessor :use_raw_file_access, :allow_write
25
- #Creates a PathMapperFS
26
- #See #mapDirectory
92
+
93
+ # should raw file access should be used - useful for binary files
94
+ # @return [Boolean]
95
+ # default is false
96
+ attr_accessor :use_raw_file_access
97
+
98
+ # should filesystem support writing through to the real files
99
+ # @return [Boolean]
100
+ # default is false
101
+ attr_accessor :allow_write
102
+
103
+ # Creates a new Path Mapper filesystem over an existing directory
104
+ # @param [String] dir
105
+ # @param [Hash] options
106
+ # @yieldparam [String] file path to map
107
+ # @yieldreturn [String]
108
+ # @see #initialize
109
+ # @see #map_directory
27
110
  def PathMapperFS.create(dir,options={ },&block)
28
111
  pm_fs = PathMapperFS.new(options)
29
- pm_fs.mapDirectory(dir) do |file|
30
- block.call(file)
31
- end
112
+ pm_fs.map_directory(dir,&block)
32
113
  return pm_fs
33
114
  end
34
115
 
116
+ # Create a new Path Mapper filesystem
117
+ # @param [Hash] options
118
+ # @option options [Boolean] :use_raw_file_access
119
+ # @option options [Boolean] :allow_write
35
120
  def initialize(options = { })
36
- @root = { }
121
+ @root = MNode.new(nil)
37
122
  @use_raw_file_access = options[:use_raw_file_access]
38
123
  @allow_write = options[:allow_write]
39
124
  end
125
+
126
+ # Recursively find all files and map according to the given block
127
+ # @param [String...] dirs directories to list
128
+ # @yieldparam [String] file path to map
129
+ # @yieldreturn [String] the mapped path
130
+ # @yieldreturn nil to skip mapping this file
131
+ def map_directory(*dirs)
132
+ require 'find'
133
+ Find.find(*dirs) do |file|
134
+ new_path = yield file
135
+ map_file(file,new_path) if new_path
136
+ end
137
+ end
138
+ alias :mapDirectory :map_directory
40
139
 
41
- # Adds new_path to our list of mapped files
140
+
141
+ # Add (or replace) a mapped file
42
142
  #
43
- # Returns a hash entry which stores the real_path under the :pm_real_path key.
44
- def mapFile(real_path,new_path)
143
+ # @param [String] real_path pointing at the real file location
144
+ # @param [String] new_path the mapped path
145
+ # @param [Hash<Symbol,Object>] options metadata for this path
146
+ # @option options [Hash<String,String>] :xattr hash to be used as extended attributes
147
+ # @return [MNode]
148
+ # a node representing the mapped path. See {#node}
149
+ def map_file(real_path,new_path,options = {})
45
150
  #split path into components
46
- components = new_path.scan(/[^\/]+/)
151
+ components = new_path.to_s.scan(/[^\/]+/)
47
152
 
48
153
  #create a hash of hashes to represent our directory structure
49
- new_file = components.inject(@root) { |directory, file|
50
- directory[file] ||= Hash.new()
154
+ new_file = components.inject(@root) { |parent_dir, file|
155
+ parent_dir.files[file] ||= MNode.new(parent_dir)
51
156
  }
52
- new_file[:pm_real_path] = real_path
157
+ new_file.init_file(real_path,options)
158
+
53
159
  return new_file
54
160
  end
161
+ alias :mapFile :map_file
162
+
163
+ # Retrieve in memory node for a mapped path
164
+ #
165
+ # @param [String] path
166
+ # @return [MNode] in memory node at path
167
+ # @return nil if path does not exist in the filesystem
168
+ def node(path)
169
+ path_components = scan_path(path)
55
170
 
56
- # Convenience method to recursively map all files according to the given block
57
- def mapDirectory(*dirs)
58
- require 'find'
59
- Find.find(*dirs) do |file|
60
- new_path = yield file
61
- mapFile(file,new_path) if new_path
62
- end
171
+ #not actually injecting anything here, we're just following the hash of hashes...
172
+ path_components.inject(@root) { |dir,file|
173
+ break unless dir.files[file]
174
+ dir.files[file]
175
+ }
63
176
  end
64
177
 
65
178
  # Takes a mapped file name and returns the original real_path
66
179
  def unmap(path)
67
- possible_file = node(path)
68
- return possible_file ? possible_file[:pm_real_path] : nil
180
+ node = node(path)
181
+ (node && node.file?) ? node.real_path : nil
69
182
  end
183
+
184
+ # Deletes files and directories.
185
+ # Yields each {#node} in the filesystem and deletes it if the block returns true
186
+ #
187
+ # Useful if your filesystem is periodically remapping the entire contents and you need
188
+ # to delete entries that have not been touched in the latest scan
189
+ #
190
+ # @yieldparam [Hash] filesystem node
191
+ # @yieldreturn [true,false] should this node be deleted
192
+ def cleanup(&block)
193
+ recursive_cleanup(@root,&block)
194
+ end
195
+
70
196
 
71
- # Returns true for any directory referenced by a mapped file
72
- # See FuseFS API.txt
197
+ # @!visibility private
73
198
  def directory?(path)
74
199
  possible_dir = node(path)
75
- possible_dir && !possible_dir[:pm_real_path]
200
+ possible_dir && possible_dir.directory?
76
201
  end
77
202
 
78
- # See FuseFS API.txt
79
- # expects to be called only if directory? returns true
203
+ # @!visibility private
80
204
  def contents(path)
81
- node(path).keys
205
+ node(path).files.keys
82
206
  end
83
207
 
84
- # See FuseFS API.txt
208
+ # @!visibility private
85
209
  def file?(path)
86
210
  filename = unmap(path)
87
211
  filename && File.file?(filename)
88
212
  end
89
213
 
90
- # See FuseFS API.txt
214
+ # @!visibility private
91
215
  # only called if option :raw_reads is not set
92
216
  def read_file(path)
93
217
  IO.read(unmap(path))
94
218
  end
95
219
 
220
+ # @!visibility private
96
221
  # We can only write to existing files
97
222
  # because otherwise we don't have anything to back it
98
223
  def can_write?(path)
99
224
  @allow_write && file?(path)
100
225
  end
101
226
 
102
- # TODO: This can't possibly work- the path is not unmapped
103
- # and we don't open the file for writing
227
+ # @!visibility private
104
228
  def write_to(path,contents)
105
- File.open(path) do |f|
229
+ File.open(unmap(path),"w") do |f|
106
230
  f.print(contents)
107
231
  end
108
232
  end
109
233
 
110
- # See FuseFS API.txt
234
+ # @!visibility private
111
235
  def size(path)
112
236
  File.size(unmap(path))
113
237
  end
114
238
 
115
- # See RFuseFS API.txt
239
+ # @!visibility private
116
240
  def times(path)
117
241
  realpath = unmap(path)
118
242
  if (realpath)
@@ -124,7 +248,12 @@ module FuseFS
124
248
  end
125
249
  end
126
250
 
127
- # See FuseFS API.txt
251
+ # @!visibility private
252
+ def xattr(path)
253
+ result = node(path).options[:xattr] || {}
254
+ end
255
+
256
+ # @!visibility private
128
257
  # Will create, store and return a File object for the underlying file
129
258
  # for subsequent use with the raw_read/raw_close methods
130
259
  # expects file? to return true before this method is called
@@ -132,7 +261,7 @@ module FuseFS
132
261
 
133
262
  return false unless @use_raw_file_access
134
263
 
135
- return false if mode.include?("w") && (!@allow_writes)
264
+ return false if mode.include?("w") && (!@allow_write)
136
265
 
137
266
  @openfiles ||= Hash.new() unless rfusefs
138
267
 
@@ -154,21 +283,21 @@ module FuseFS
154
283
  return file
155
284
  end
156
285
 
157
- # See (R)FuseFS API.txt
286
+ # @!visibility private
158
287
  def raw_read(path,off,sz,file=nil)
159
288
  file = @openfiles[path] unless file
160
289
  file.sysseek(off)
161
290
  file.sysread(sz)
162
291
  end
163
292
 
164
- # See (R)FuseFS API.txt
293
+ # @!visibility private
165
294
  def raw_write(path,offset,sz,buf,file=nil)
166
295
  file = @openfiles[path] unless file
167
- file.sysseek(off)
296
+ file.sysseek(offset)
168
297
  file.syswrite(buf[0,sz])
169
298
  end
170
299
 
171
- # See (R)FuseFS API.txt
300
+ # @!visibility private
172
301
  def raw_close(path,file=nil)
173
302
  unless file
174
303
  file = @openfiles.delete(path)
@@ -177,16 +306,16 @@ module FuseFS
177
306
  end
178
307
 
179
308
  private
180
- # returns a hash representing a given node, if we have a mapped entry for it, nil otherwise
181
- # this entry is a file if it has_key?(:pm_real_path), otherwise it is a directory.
182
- def node(path)
183
- path_components = scan_path(path)
184
-
185
- #not actually injecting anything here, we're just following the hash of hashes...
186
- path_components.inject(@root) { |dir,file|
187
- break unless dir[file]
188
- dir[file]
189
- }
309
+
310
+ def recursive_cleanup(dir_node,&block)
311
+ dir_node.files.delete_if do |path,child|
312
+ if child.file?
313
+ yield child
314
+ else
315
+ recursive_cleanup(child,&block)
316
+ child.files.size == 0
317
+ end
318
+ end
190
319
  end
191
320
  end
192
321
 
@@ -0,0 +1,125 @@
1
+ require 'fusefs/pathmapper'
2
+ require 'sqlite3'
3
+ require 'rb-inotify'
4
+ require 'thread'
5
+
6
+ module FuseFS
7
+
8
+ class SqliteMapperFS < PathMapperFS
9
+
10
+ # The database connection
11
+ attr_reader :db
12
+
13
+ # Maintains a count of the number of times through the scan loop
14
+ attr_reader :scan_id
15
+
16
+ #
17
+ #
18
+ # @param [String] db_path Path to Sqlite database
19
+ # @param [String] sql query
20
+ # @param [Hash] options see {PathMapperFS#initialize}
21
+ # @yieldparam [Row] row to map
22
+ # @yieldreturn [String,String,Hash<Symbol,Object>] newpath, realpath, options
23
+ # * newpath - the mapped path
24
+ # * realpath - path to the real file
25
+ # * options - additional information to store with this path
26
+ def initialize(db_path,sql,options = { },&row_mapper)
27
+ @db_path = db_path.to_s
28
+ @sql = sql.to_s
29
+ define_singleton_method(:map_row,row_mapper) if block_given?
30
+ super(options)
31
+ end
32
+
33
+ # Maps a row into a new filepath
34
+ #
35
+ # @param [Hash] row sqlite result hash for a row
36
+ # @return [String,String,Hash<Symbol,Object>] newpath, realpath, options
37
+ # * newpath - the mapped path
38
+ # * realpath - path to the real file
39
+ # * options - additional information to store with this path
40
+ # @abstract
41
+ def map_row(row)
42
+ raise NotImplementedError, "abstract method #{__method__} not implemented"
43
+ end
44
+
45
+ # FuseFS callback when the filesystem is mounted
46
+ # performs the initial scan and starts watching the database for changes
47
+ # @api FuseFS
48
+ def mounted()
49
+ @mutex = Mutex.new
50
+ @cv = ConditionVariable.new
51
+ @mounted = true
52
+
53
+ notifier = start_notifier
54
+
55
+
56
+ @scan_thread = Thread.new() do
57
+ scan_loop(notifier)
58
+ end
59
+ end
60
+
61
+ # FuseFS callback when filesystem is unmounted
62
+ #
63
+ # Stops the database watching threads
64
+ # @api FuseFS
65
+ def unmounted()
66
+ @mounted = false
67
+ @mutex.synchronize { @cv.signal }
68
+ @scan_thread.join
69
+ end
70
+
71
+ # Executes the sql query and passes each row to map_row (or the block passed in {#initialize})
72
+ #
73
+ # Subclasses can override this method for pre/post scan processing, calling super as required
74
+ def scan()
75
+ db.execute(@sql) do |row|
76
+ new_path, real_path, options = map_row(row)
77
+ options ||= {}
78
+ options[:sqlite_scan_id] = @scan_id
79
+ map_file(new_path, real_path, options)
80
+ end
81
+ cleanup() { |file_node| file_node.options[:sqlite_scan_id] != @scan_id }
82
+ end
83
+
84
+ private
85
+
86
+ def start_notifier
87
+ notifier = INotify::Notifier.new()
88
+ modified = false
89
+ notifier.watch(@db_path,:modify, :close_write) do |event|
90
+ modified = true if event.flags.include?(:modify)
91
+ if event.flags.include?(:close_write) && modified
92
+ @mutex.synchronize {@cv.signal}
93
+ modified = false
94
+ end
95
+ end
96
+
97
+ Thread.new { notifier.run }
98
+
99
+ notifier
100
+ end
101
+
102
+ def scan_loop(notifier)
103
+ @mutex.synchronize() do
104
+ @scan_id = 0
105
+ while @mounted
106
+ begin
107
+ @db = SQLite3::Database.new(@db_path,:readonly => true)
108
+ @db.results_as_hash = true
109
+ @db.busy_timeout(10000)
110
+ @scan_id = @scan_id + 1
111
+ scan()
112
+ rescue StandardError => e
113
+ puts e
114
+ puts e.backtrace.join("\n")
115
+ ensure
116
+ @db.close unless @db.closed?
117
+ @db = nil
118
+ end
119
+ @cv.wait(@mutex)
120
+ end
121
+ notifier.stop
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,3 +1,3 @@
1
1
  module RFuseFS
2
- VERSION="1.0.0"
2
+ VERSION="1.0.1.RC0"
3
3
  end