rfusefs 1.0.0 → 1.0.1.RC0

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