homefs 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8da5aed5c790cde44852ed7d17e9878f5ee8a786
4
- data.tar.gz: bd0a7440218649a6901b6f7530c41cc66502ab9f
3
+ metadata.gz: f11d42ee83c4b4ebf69e367d4ae58cc1a4927fac
4
+ data.tar.gz: c8bba0a2cad6c689a3865c910c7d172c14bb9933
5
5
  SHA512:
6
- metadata.gz: 2897cf7ee8057733b4c896303d38629feb86ef92486c4c27cb1aebd68465bef5fd339aa0cf985cf753d2560330df9b7d515f6b957b72efbe563d8aeaf994f435
7
- data.tar.gz: 2989acfca06ed15ba9a10796d8613e2e43fc8e7e2015f776ec3a903fedf22a34172ece57d0266efafcb94cfb66a3e76801054344f2c46ed35af5bbe871135cda
6
+ metadata.gz: d31980d7d5be1f62af06b45e14ae95bfc1001202eb674edb479921730c5fea3eb7c5908158b613fe7ff721edf418554cf1d29a8a7678d269c76ed6e30ab08156
7
+ data.tar.gz: 714090d6643662fe47fdc65987fd867a8da0368ef5cfa9534211ea314120ec5eadd53914ece6196e7d8e58f17c004bd13dbfb2426746fd1e5e98685a428426ae
data/bin/homefs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'homefs'
4
- require 'rb-inotify'
5
- require 'fileutils'
4
+
5
+ DEBUG = false
6
6
 
7
7
  if ARGV.size < 2
8
8
  warn "Usage:\n\t#{File.basename($0)} relative_directory mountpoint [options...]"
@@ -10,8 +10,10 @@ if ARGV.size < 2
10
10
  end
11
11
 
12
12
  # Return immediately
13
- if fork
14
- exit 0
13
+ unless DEBUG
14
+ if fork
15
+ exit 0
16
+ end
15
17
  end
16
18
 
17
19
  reldir = ARGV.shift
@@ -19,35 +21,47 @@ reldir = ARGV.shift
19
21
  fs = HomeFS.new(reldir)
20
22
 
21
23
  # Shut up
22
- $stdout = File.open("/dev/null", "w")
23
- $stderr = File.open("/dev/null", "w")
24
-
25
- pid = fork
26
- if pid
27
- # The child process will be the one actually handling the FuseFS,
28
- # so we want to exit as soon as it dies
29
- trap 'CLD' do
30
- exit 0
31
- end
24
+ unless DEBUG
25
+ $stdout = File.open("/dev/null", "w")
26
+ $stderr = File.open("/dev/null", "w")
27
+ end
32
28
 
33
- # Set a watch for /etc/passwd, and update our hash of UIDs => homedirs
34
- # when it changes
35
- notifier = INotify::Notifier.new
36
- notifier.watch('/etc/passwd', :modify) do
37
- # Send a signal to the child process; it will catch USR1 and update
38
- # the homedirs hash
39
- Process::kill('USR1', pid)
40
- end
41
- notifier.run
42
- else
43
- # Here we trap the signal to be sent to us by our parent
44
- # when /etc/passwd changes and update our hash accordingly
45
- trap 'USR1' do
46
- fs.read_passwd
29
+ if DEBUG
30
+ fs = HomeFS::Wrapper.new(fs)
31
+ end
32
+
33
+ begin
34
+ require 'rb-inotify'
35
+ pid = fork
36
+ if pid
37
+ # The child process will be the one actually handling the FuseFS,
38
+ # so we want to exit as soon as it dies
39
+ trap 'CLD' do
40
+ exit 0
41
+ end
42
+
43
+ # Set a watch for /etc/passwd, and update our hash of UIDs => homedirs
44
+ # when it changes
45
+ notifier = INotify::Notifier.new
46
+ notifier.watch('/etc/passwd', :modify) do
47
+ # Send a signal to the child process; it will catch USR1 and update
48
+ # the homedirs hash
49
+ Process::kill('USR1', pid)
50
+ end
51
+ notifier.run
52
+ else
53
+ # Here we trap the signal to be sent to us by our parent
54
+ # when /etc/passwd changes and update our hash accordingly
55
+ trap 'USR1' do
56
+ fs.read_passwd
57
+ end
47
58
  end
59
+ rescue LoadError
60
+ # We don't have rb-inotify, and so don't watch for changes to
61
+ # /etc/passwd
48
62
  end
49
63
 
50
64
  # We set allow_other, because this filesystem doesn't really make sense
51
65
  # without it (why direct different users to different places if only one
52
66
  # user can access the filesystem?).
53
- FuseFS.main(ARGV + ["-o", "allow_other"]) { fs }
67
+ RFuse.main(ARGV + ["-o", "allow_other"]) { fs }
@@ -0,0 +1,21 @@
1
+ class HomeFS
2
+ class Wrapper
3
+ def initialize(object)
4
+ methods = (object.class.instance_methods -
5
+ Object.instance_methods)
6
+ methods.each do |method|
7
+ self.define_singleton_method(method) do |*args|
8
+ warn "#{method} called with args #{args.inspect}"
9
+ begin
10
+ ret = object.send(method, *args)
11
+ warn "\tMethod returned #{ret.inspect}"
12
+ return ret
13
+ rescue Exception => e
14
+ warn e
15
+ warn e.backtrace
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/homefs/homefs.rb CHANGED
@@ -1,24 +1,25 @@
1
- require 'rfusefs'
1
+ require 'rfuse'
2
2
 
3
3
  # An instance of HomeFS; this implements all FuseFS methods, and ultimately
4
4
  # handles reading, writing, and inspection of files.
5
5
  #
6
6
  # We rely on the context provided to us by FUSE to determine the calling user's
7
- # UID and GID. If you want to access an instance of this class directly, you may
8
- # set the UID and GID of the current thread like so
9
- # Thread.current[:fusefs_reader_uid] = uid
10
- # Thread.current[:fusefs_reader_gid] = gid
11
- # where 'uid' and 'gid' are the desired UID and GID you wish to emulate.
7
+ # UID and GID.
8
+ #
9
+ # Currently, HomeFS does not support POSIX locking (which has little effect
10
+ # in practice), and special files (those created by mknod/FIFOs). We do
11
+ # supported extended filesystem attributes, but only if you have the
12
+ # 'ffi-xattr' gem.
12
13
  # @author Dylan Frese
13
14
  class HomeFS
14
15
 
15
16
  # Creates a new instance of HomeFS. This instance should probably be passed
16
- # to FuseFS, as it does nothing on its own. The path is relative to the
17
+ # to RFuse, as it does nothing on its own. The path is relative to the
17
18
  # calling user's home directory, that is, the user interacting with the
18
19
  # filesystem through open(2), read(2), write(2), etc.
19
20
  # @param [String] path the path, relative to $HOME, to use as the root of
20
21
  # the file system.
21
- def initialize(path)
22
+ def initialize(path, options = Hash.new)
22
23
  @relpath = path
23
24
  read_passwd
24
25
  read_group
@@ -54,7 +55,7 @@ class HomeFS
54
55
  }
55
56
  end
56
57
 
57
- # Read or re-read /etc/group to update the internal table of group
58
+ # Read or re-read /etc/group to update the internal table of
58
59
  # group membership.
59
60
  # @return [void]
60
61
  def read_group
@@ -69,7 +70,7 @@ class HomeFS
69
70
  group.each_line.map { |line|
70
71
  next if line.strip.empty?
71
72
  _name, _, gid, members = line.split(':')
72
- members = members.strip.split(',').map {|member| p member; @usernames[member]}
73
+ members = members.strip.split(',').map {|member| @usernames[member]}
73
74
  [Integer(gid), members]
74
75
  }.reject { |line|
75
76
  line.nil?
@@ -81,11 +82,12 @@ class HomeFS
81
82
  # Return the path to the root of HomeFS relative to the underlying
82
83
  # filesystem. If _path_ is specified, it is taken to be relative to the
83
84
  # root of HomeFS.
85
+ # @param [Integer] uid the UID whose home directory to use as a base
86
+ # @param [String] path a relative path to a resource
84
87
  # @return [String] path to the root of the HomeFS relative to the underlying
85
88
  # filesystem, or if _path_ is specified, the path to that resource
86
89
  # relative to the underlying filesystem.
87
- def homepath(path = nil)
88
- uid = FuseFS.reader_uid
90
+ def homepath(uid, path = nil)
89
91
  basepath = @homedirs[uid]
90
92
  # basepath shouldn't ever be nil, but fail gracefully just
91
93
  # in case.
@@ -129,16 +131,15 @@ class HomeFS
129
131
  # @param [String] file the path to the file to check
130
132
  # @param [Integer] mask the mask against which to check the file's mode.
131
133
  # It is recommended you write this in octal (with a leading 0).
132
- # @param [Boolean] check_ids if true, only check user and group permissions
133
- # if the caller is, respectively, the owner of the file/in the file's
134
- # group.
134
+ # @param [Integer] uid if not nil, only check user and group permissions
135
+ # if the user it represents is, respectively, the owner of the
136
+ # file/in the file's group.
135
137
  # @return [Boolean] whether the mode of _file_ matches the given mask
136
- def mode_mask(file, mask, check_ids = true)
138
+ def mode_mask(file, mask, uid = nil)
137
139
  stat = File.stat(file)
138
140
  fmode = stat.mode
139
- if check_ids
141
+ if uid
140
142
  fuid, fgid = stat.uid, stat.gid
141
- uid = FuseFS.reader_uid
142
143
  # Zero out the third digit (in octal).
143
144
  # We could use a constant here, but this works
144
145
  # for a mask of any length
@@ -156,263 +157,640 @@ class HomeFS
156
157
  # we test if the parent directory is either writable or has the sticky bit
157
158
  # set.
158
159
  # @param [String] path the path to test
160
+ # @param [Integer] uid the UID to check the privilage of. If nil, this
161
+ # method returns whether the file is writable by anyone.
159
162
  # @return [Boolean] whether the given path is writable
160
- def writable?(path)
161
- return true if mode_mask(path, 0222)
163
+ def writable?(path, uid = nil)
162
164
  if !File.exist?(path)
163
- mode_mask(File.dirname(path), 01222)
165
+ mode_mask(File.dirname(path), 01222, uid)
164
166
  else
165
- false
167
+ mode_mask(path, 0222, uid)
166
168
  end
167
169
  end
168
170
 
169
- # Rename the file or directory at from_path to to_path. This is mostly
170
- # equivalent to /bin/mv.
171
- # @param [String] from_path the path of the file to be moved
172
- # @param [String] to_path the destination of the file
171
+ # Test whether the user described by _uid_ can read from the given
172
+ # path. If no file exists at the given path, an Errno::ENOENT is
173
+ # raised.
174
+ # @param [String] path the path to test
175
+ # @param [Integer] uid the UID to check the privilage of. If nil, this
176
+ # method returns whether the file is readable by anyone.
177
+ # @return [Boolean] whether the given path is readable
178
+ # @raise [Errno::ENOENT] if no file exists at _path_
179
+ def readable?(path, uid = nil)
180
+ mode_mask(path, 0444, uid)
181
+ end
182
+
183
+ # Test whether the user described by _uid_ can execute the given
184
+ # path. If no file exists at the given path, an Errno::ENOENT is
185
+ # raised.
186
+ # @param [String] path the path to test
187
+ # @param [Integer] uid the UID to check the privilage of. If nil, this
188
+ # method returns whether the file is executable by anyone.
189
+ # @return [Boolean] whether the given path is executable
190
+ # @raise [Errno::ENOENT] if no file exists at _path_
191
+ def executable?(path, uid = nil)
192
+ mode_mask(path, 0111, uid)
193
+ end
194
+
195
+ # This method raises an Errno::EACCES if the user given by _uid_ cannot
196
+ # write to the given path. The check is the same as in {#writable?}.
197
+ # @param [String] relpath the path to test, relative to the root of
198
+ # HomeFS (not the actual filesystem)
199
+ # @param [Integer] uid the UID to check the privilage of
173
200
  # @return [void]
174
- # @note This method should usually only be called by FUSE, and not called
175
- # directly
176
- def rename(from_path, to_path)
177
- FileUtils.mv(homepath(from_path), homepath(to_path))
201
+ # @raise [Errno::EACCES] if the path is not writable by the given user
202
+ def check_writable(uid, relpath)
203
+ hpath = homepath(uid, relpath)
204
+ unless writable?(hpath, uid)
205
+ fail Errno::EACCES, relpath
206
+ end
178
207
  end
179
208
 
180
- # Update the mtime and atime (last-modified and last-accessed time) of the
181
- # file at _path_ to the time given by modtime.
182
- # @param [String] path the path to the file to touch
183
- # @param [Time] modtime the time to update the file's times to
209
+ # This method raises an Errno::EACCES if the user given by _uid_ cannot
210
+ # read from the given path.
211
+ # @param [String] relpath the path to test, relative to the root of
212
+ # HomeFS (not the actual filesystem)
213
+ # @param [Integer] uid the UID to check the privilage of
184
214
  # @return [void]
185
- # @note This method should usually only be called by FUSE, and not called
186
- # directly
187
- def touch(path, modtime)
188
- File.utime(modtime, modtime, homepath(path))
215
+ # @raise [Errno::EACCES] if the path is not readable by the given user
216
+ # @raise [Errno::ENOENT] if no file or directory exists at the given
217
+ # path
218
+ def check_readable(uid, relpath)
219
+ hpath = homepath(uid, relpath)
220
+ unless readable?(hpath, uid)
221
+ fail Errno::EACCES, relpath
222
+ end
189
223
  end
190
224
 
191
- # Remove the directory at the given path. The directory must be empty. This
192
- # is mostly equivalent to rmdir(1).
193
- # @param [String] path the path to the directory to remove
225
+ # This method raises an Errno::EACCES if the user given by _uid_ cannot
226
+ # list the given directory, i.e., if the user does not have the execute
227
+ # and read permissions on the given directory. If the path given is not
228
+ # a directory, the dirname of the given path is tested.
229
+ # @param [String] relpath the path to test, relative to the root of
230
+ # HomeFS (not the actual filesystem)
231
+ # @param [Integer] uid the UID to check the privilage of
194
232
  # @return [void]
195
- # @note This method should usually only be called by FUSE, and not called
196
- # directly
197
- def rmdir(path)
198
- FileUtils.rmdir(homepath(path))
233
+ # @raise [Errno::EACCES] if the path is not readable by the given user
234
+ def check_listable(uid, relpath)
235
+ hpath = homepath(uid, relpath)
236
+ unless File.directory?(hpath)
237
+ hpath = File.dirname(hpath)
238
+ end
239
+ unless readable?(hpath) && executable?(hpath)
240
+ fail Errno::EACCES, relpath
241
+ end
199
242
  end
200
243
 
201
- # Test whether we can remove the directory given by _path_.
202
- # @param [String] path the path to the directory to test
203
- # @return [Boolean] true if the directory given can be removed.
204
- # @note This method should usually only be called by FUSE, and not called
205
- # directly
206
- def can_rmdir?(path)
207
- File.writable?(homepath(path)) &&
208
- Dir.new(path).to_a.size == 2
244
+ # This method raises an Errno::EACCES if the user given by _uid_ is not
245
+ # the owner of the file described by _relpath_
246
+ # @param [String] relpath the path to test, relative to the root of
247
+ # HomeFS (not the actual filesystem)
248
+ # @param [Integer] uid the UID to check ownership of
249
+ # @return [void]
250
+ # @raise [Errno::EACCES] if the path is not owned by the given user
251
+ # @raise [Errno::ENOENT] if no file or directory exists at the given
252
+ # path
253
+ def check_owner(uid, relpath)
254
+ hpath = File.dirname(homepath(uid, relpath))
255
+ unless File.stat(hpath).uid == uid
256
+ fail Errno::EACCES, relpath
257
+ end
209
258
  end
210
259
 
211
- # Make a new directory at _path_. This is similar to mkdir(1).
212
- # @param [String] path the path to the directory to make
260
+ # Check access permissions
261
+ # @overload access(context,path,mode)
262
+ # @abstract
263
+ # @param [Context] context
264
+ # @param [String] path
265
+ # @param [Integer] mode the permissions to check
213
266
  # @return [void]
214
- # @note This method should usually only be called by FUSE, and not called
215
- # directly
216
- def mkdir(path)
217
- FileUtils.mkdir(homepath(path))
267
+ # @raise [Errno::EACCESS] if the requested permission isn't available
268
+ # @note This method should usually only be called by FUSE, and not
269
+ # called directly
270
+ def access(context, path, mode)
271
+ uid = context.uid
272
+ unless mode_mask(homepath(uid, path), mode * 0111, uid)
273
+ raise Errno::EACCES
274
+ end
218
275
  end
219
276
 
220
- # Test if we can make a directory at the given path.
221
- # @param [String] path the path to test
222
- # @return [Boolean] true if we can make a directory at _path_.
223
- # @note This method should usually only be called by FUSE, and not called
224
- # directly
225
- def can_mkdir?(path)
226
- writable?(homepath(path))
277
+ # Change file permissions
278
+ # @overload chmod(context,path,mode)
279
+ # @abstract
280
+ # @param [Context] context
281
+ # @param [String] path
282
+ # @param [Integer] mode
283
+ # @return [void]
284
+ # @raise [Errno]
285
+ # @note This method should usually only be called by FUSE, and not
286
+ # called directly
287
+ def chmod(context, path, mode)
288
+ hpath = homepath(context.uid, path)
289
+ check_owner(context.uid, path)
290
+ File.chmod(mode, hpath)
227
291
  end
228
292
 
229
- # Delete the file given by _path_. This is similar to rm(1).
230
- # @param [String] path the path to the file to delete
293
+ # Change file ownership
294
+ # @overload chown(context,path,uid,gid)
295
+ # @abstract
296
+ # @param [Context] context
297
+ # @param [String] path
298
+ # @param [Integer] uid new user id
299
+ # @param [Integer] gid new group id
231
300
  # @return [void]
232
- # @note This method should usually only be called by FUSE, and not called
233
- # directly
234
- def delete(path)
235
- FileUtils.rm(homepath(path))
301
+ # @raise [Errno]
302
+ # @note This method should usually only be called by FUSE, and not
303
+ # called directly
304
+ def chown(context, path, uid, gid)
305
+ raise Errno::EACCES, path if context.uid != 0
306
+ hpath = homepath(context.uid, path)
307
+ File.chown(uid, gid, hpath)
236
308
  end
237
309
 
238
- # Test if we can delete the file given by _path_.
239
- # @param [String] path the path to the file to test
310
+ # Create and open a file
311
+ # @abstract
312
+ # @param [Context] context
313
+ # @param [String] path
314
+ # @param [Integer] mode the file permissions to create
315
+ # @param [Fileinfo] ffi - use the FileInfo#fh attribute to store a
316
+ # filehandle
240
317
  # @return [void]
241
- # @note This method should usually only be called by FUSE, and not called
242
- # directly
243
- def can_delete?(path)
244
- writable?(File.dirname(homepath(path)))
318
+ # @raise [Errno]
319
+ # If the file does not exist, first create it with the specified mode,
320
+ # and then open it.
321
+ # @note This method should usually only be called by FUSE, and not
322
+ # called directly
323
+ def create(context, path, mode, ffi)
324
+ check_writable(context.uid, path)
325
+ hpath = homepath(context.uid, path)
326
+
327
+ # It is important that we create the file and chown it before we
328
+ # set its mode to prevent a possible privilage escalation
329
+ # race-condition. To be more specific, if this filesystem is
330
+ # running as root (which it usually is), the file will be created
331
+ # owned by root. If the mode were set when the file is created,
332
+ # then an attacker could create a setuid file writable by anyone,
333
+ # and then, in the 'real' filesystem (not HomeFS), quickly write a
334
+ # small program that just executes /bin/bash. If the write
335
+ # completed before handle.chown was called, the attacker could have
336
+ # a setuid shell.
337
+ handle = File.new(hpath, File::CREAT | File::WRONLY, 0600)
338
+ handle.chown(context.uid, context.gid)
339
+ handle.chmod(mode)
340
+ ffi.fh = handle
245
341
  end
246
342
 
247
- # Write the given data to the given path.
248
- # @param [String] path the path to write to
249
- # @param [String] str a binary string of data to write
343
+ # Get attributes of an open file
344
+ # @overload fgetattr(context,path,ffi)
345
+ # @abstract
346
+ # @param [Context] context
347
+ # @param [String] path
348
+ # @param [Fileinfo] ffi
349
+ # @return [Stat] file attributes
350
+ # @raise [Errno]
351
+ # @note This method should usually only be called by FUSE, and not
352
+ # called directly
353
+ def fgetattr(context, path, ffi)
354
+ ffi.fh.lstat
355
+ end
356
+
357
+ # Possibly flush cached data
358
+ # @overload flush(context,path,ffi)
359
+ # @abstract
360
+ # @param [Context] context
361
+ # @param [String] path
362
+ # @param [FileInfo] ffi
250
363
  # @return [void]
251
- # @note This method should usually only be called by FUSE, and not called
252
- # directly
253
- def write_to(path, str)
254
- File.open(homepath(path), "wb") {|file| file.write(str) }
364
+ # @raise [Errno]
365
+ # BIG NOTE: This is not equivalent to fsync(). It's not a request to sync dirty data.
366
+ # Flush is called on each close() of a file descriptor. So if a
367
+ # filesystem wants to return write errors in close() and the file has
368
+ # cached dirty data, this is a good place to write back data and return
369
+ # any errors. Since many applications ignore close() errors this is not
370
+ # always useful.
371
+ #
372
+ # NOTE: The flush() method may be called more than once for each
373
+ # open(). This happens if more than one file descriptor refers to an
374
+ # opened file due to dup(), dup2() or fork() calls. It is not possible
375
+ # to determine if a flush is final, so each flush should be treated
376
+ # equally. Multiple write-flush sequences are relatively rare, so this
377
+ # shouldn't be a problem.
378
+ #
379
+ # Filesystems shouldn't assume that flush will always be called after
380
+ # some writes, or that if will be called at all.
381
+ # @note This method should usually only be called by FUSE, and not
382
+ # called directly
383
+ def flush(context, path, ffi) # We don't do any caching
255
384
  end
256
385
 
257
- # Test whether we can write out to the given path.
258
- # @param [String] path the path to test
259
- # @return [Boolean] whether we can write to the given path
260
- # @note This method should usually only be called by FUSE, and not called
261
- # directly
262
- def can_write?(path)
263
- writable?(homepath(path))
264
- end
265
-
266
- # Get the times (atime, mtime, ctime) for the file at the given path.
267
- # @param [String] path the path to the file to get the times for
268
- # @return [Array<Time>] an array of size three of the last-accessed time,
269
- # the last-modified time, and the creation time for the file
270
- # @note This method should usually only be called by FUSE, and not called
271
- # directly
272
- def times(path)
273
- atime = File.atime(homepath(path))
274
- mtime = File.mtime(homepath(path))
275
- ctime = File.ctime(homepath(path))
276
- [atime, mtime, ctime]
277
- end
278
-
279
- # Test whether the file given by _path_ is executable.
280
- # @param [String] path the path to the file to test
281
- # @return [Boolean] whether the file at the given path is executable.
282
- # @note This method should usually only be called by FUSE, and not called
283
- # directly
284
- def executable?(path)
285
- mode_mask(homepath(path), 0111)
286
- end
287
-
288
- # Read the contents of the file given by _path_.
289
- # @param [String] path the path of the file to read
290
- # @return [String] a binary string of the contents of the file
291
- # @note This method should usually only be called by FUSE, and not called
292
- # directly
293
- def read_file(path)
294
- File.open(homepath(path), "rb") do |file|
295
- file.read
386
+ # Synchronize file contents
387
+ # @overload fsync(context,path,datasync,ffi)
388
+ # @abstract
389
+ # @param [Context] context
390
+ # @param [String] path
391
+ # @param [Integer] datasync if non-zero, then only user data should be
392
+ # flushed, not the metadata
393
+ # @param [FileInfo] ffi
394
+ # @return [void]
395
+ # @raise [Errno]
396
+ # @note This method should usually only be called by FUSE, and not
397
+ # called directly
398
+ def fsync(context, path, datasync, ffi)
399
+ if datasync
400
+ ffi.fh.fdatasync
401
+ else
402
+ ffi.fh.fsync
296
403
  end
297
404
  end
298
405
 
299
- # List the contents of a directory. This includes '.' and '..' (the current
300
- # directory and its parent).
301
- # @param [String] path the path to the directory to list
302
- # @return [Array<String>] a list of file names of files in the directory
303
- # @note This method should usually only be called by FUSE, and not called
304
- # directly
305
- def contents(path)
306
- Dir.new(homepath(path)).to_a
406
+ # Change the size of an open file
407
+ # @overload ftruncate(context,path,size,ffi)
408
+ # @abstract
409
+ # @param [Context] context
410
+ # @param [String] path
411
+ # @param [Integer] size
412
+ # @param [Fileinfo] ffi
413
+ # @return [void]
414
+ # @raise [Errno]
415
+ # @note This method should usually only be called by FUSE, and not
416
+ # called directly
417
+ def ftruncate(context, path, size, ffi)
418
+ ffi.fh.truncate(size)
307
419
  end
308
420
 
309
- # Get the size of the file at _path_
310
- # @param [String] path the path to the file or directory to get the size of
311
- # @return [Integer] the size, in bytes, of the file
312
- # @note This method should usually only be called by FUSE, and not called
313
- # directly
314
- def size(path)
315
- File.size(homepath(path))
421
+ # Get file attributes.
422
+ # @overload getattr(context,path)
423
+ # @abstract
424
+ # @param [Context] context
425
+ # @param [String] path
426
+ # @return [Stat] or something that quacks like a stat, or nil if the path does not exist
427
+ # @raise [Errno]
428
+ # Similar to stat(). The 'st_dev' and 'st_blksize' fields are ignored.
429
+ # The 'st_ino' field is ignored except if the 'use_ino' mount option is
430
+ # given.
431
+ # @note This method should usually only be called by FUSE, and not
432
+ # called directly
433
+ def getattr(context, path)
434
+ check_listable(context.uid, path)
435
+ File.lstat(homepath(context.uid, path))
316
436
  end
317
437
 
318
- # Test whether _path_ is the name of a file (i.e., not a directory)
319
- # @param [String] path the path to test
320
- # @return [Boolean] whether _path_ represents a file
321
- # @note This method should usually only be called by FUSE, and not called
322
- # directly
323
- def file?(path)
324
- File.file?(homepath(path))
438
+ # Called when filesystem is initialised
439
+ # @overload init(info)
440
+ # @abstract
441
+ # @param [Context] context
442
+ # @param [Struct] info connection information
443
+ # @return [void]
444
+ # @note This method should usually only be called by FUSE, and not
445
+ # called directly
446
+ def init(context, info)
325
447
  end
326
448
 
327
- # Test whether _path_ is the name of a directory
328
- # @param [String] path the path to test
329
- # @return [Boolean] whether _path_ represents a directory
330
- # @note This method should usually only be called by FUSE, and not called
331
- # directly
332
- def directory?(path)
333
- File.directory?(homepath(path))
334
- end
335
-
336
- # Open a file handle to the file given by _path_, with the mode given by
337
- # _mode_. Right now, the supported modes are "rw", "w", and "r", for
338
- # read/write, write, and read. If the mode is write or read/write and no
339
- # file exists at _path_, on will be created.
340
- # @param [String] path the path to the file to open
341
- # @param [String] mode the mode to open the file with
342
- # @param rfusefs ignored
343
- # @return [File] a file handle of the file given by _path_
344
- # @note This method should usually only be called by FUSE, and not called
345
- # directly
346
- def raw_open(path, mode, rfusefs = nil)
347
- mode = case mode
348
- when "rw" then File::RDWR | File::CREAT | File::BINARY
349
- when "r" then File::RDONLY | File::BINARY
350
- when "w" then File::WRONLY | File::CREAT | File::BINARY
351
- end
352
- File.open(homepath(path), mode)
353
- end
354
-
355
- # Read _size_ bytes, starting at _offset_, from the file handle given by _raw_
356
- # (which is returned by a call to _raw_open_). If raw is nil and a path is
357
- # given, a file will be opened at _path_, and closed after the data is read.
358
- # Otherwise, _path_ is ignored.
359
- # @param [String] path the path to the file
360
- # @param [Integer] offset the offset, in bytes, from the start of the file
361
- # to start reading from.
362
- # @param [Integer] size the amount of bytes to read
363
- # @param [File] raw the file handle to read from
364
- # @return [String] a binary string of the requested data
365
- # @note This method should usually only be called by FUSE, and not called
366
- # directly
367
- def raw_read(path, offset, size, raw = nil)
368
- file = raw || raw_open(path, "r")
369
- file.seek(offset, :SET)
370
- file.read(size)
371
- ensure
372
- file.close if raw.nil?
373
- end
374
-
375
- # Sync writes to the underlying filesystem. If _raw_ is nil, no operation is
376
- # performed.
377
- # @param path ignored
378
- # @param datasync ignored
379
- # @param [File] raw the file handle to sync
449
+ # Create a hard link to file
450
+ # @overload link(context,from,to)
451
+ # @abstract
452
+ # @param [Context] context
453
+ # @param [String] from
454
+ # @param [String] to
380
455
  # @return [void]
381
- # @note This method should usually only be called by FUSE, and not called
382
- # directly
383
- def raw_sync(path, datasync, raw = nil)
384
- return if raw.nil? # Should we sync anyway?
385
- raw.fdatasync
456
+ # @raise [Errno]
457
+ # @note This method should usually only be called by FUSE, and not
458
+ # called directly
459
+ def link(context, from, to)
460
+ hfrom = homepath(context.uid, from)
461
+ check_writable(context.uid, from)
462
+ File.link(hfrom, to)
463
+ File.chown(context.uid, hfrom) if context.uid == 0
386
464
  end
387
465
 
388
- # Close the file handle given by _raw_.
389
- # @param path ignored
390
- # @param [File] raw the file handle to close
466
+ # (see RFuse::Fuse#mkdir)
467
+ # @note This method should usually only be called by FUSE, and not
468
+ # called directly
469
+ def mkdir(context, path, mode)
470
+ check_writable(context.uid, path)
471
+ hpath = homepath(context.uid, path)
472
+ Dir.mkdir(hpath, mode)
473
+ File.chown(context.uid, context.gid, hpath) if context.uid == 0
474
+ end
475
+
476
+ # File open operation
477
+ # @overload open(context,path,ffi)
478
+ # @abstract
479
+ # @param [Context] context
480
+ # @param [String] path
481
+ # @param [FileInfo] ffi
482
+ # file open flags etc.
483
+ # The fh attribute may be used to store an arbitrary filehandle object
484
+ # which will be passed to all subsequent operations on this file
485
+ # @raise [Errno::ENOPERM] if user is not permitted to open the file
486
+ # @raise [Errno] for other errors
391
487
  # @return [void]
392
- # @note This method should usually only be called by FUSE, and not called
393
- # directly
394
- def raw_close(path, raw = nil)
395
- return if raw.nil? # ???
396
- raw.close
397
- end
398
-
399
- # Write _sz_ bytes from _buf_ to the file handle _raw_, starting at _off_.
400
- # If no file handle is given, one will be opened at the given path, and
401
- # closed after the write is complete.
402
- # @param [String] path the path of the file
403
- # @param [Integer] off the offset, in bytes, to starting writing to the
404
- # file at
405
- # @param [Integer] sz the amount of bytes to read from buf
406
- # @param [String] buf a binary string containing the data to be written
407
- # @param [File] raw the file handle to write to
488
+ # @note This method should usually only be called by FUSE, and not
489
+ # called directly
490
+ def open(context, path, ffi)
491
+ if ffi.flags & File::RDWR != 0
492
+ mask = 0666
493
+ elsif ffi.flags & File::WRONLY != 0
494
+ mask = 0222
495
+ else
496
+ mask = 0444
497
+ end
498
+ hpath = homepath(context.uid, path)
499
+ unless mode_mask(hpath, mask, context.uid)
500
+ fail Errno::EACCES, path
501
+ end
502
+
503
+ # We pass the flags straight into File.open because the constants
504
+ # in RFuse::Fcntl and File::Constants have the same values, because
505
+ # they both have the same values as the open syscall. If this were
506
+ # not the case, we'd have the map the values of RFuse::Fcntl to
507
+ # their equivalents in File::Constants
508
+ ffi.fh = File.open(hpath, ffi.flags)
509
+ end
510
+
511
+ # Open directory
512
+ # @overload opendir(context,path,name)
513
+ # @abstract
514
+ # @param [Context] context
515
+ # @param [String] path
516
+ # @param [FileInfo] ffi
408
517
  # @return [void]
409
- # @note This method should usually only be called by FUSE, and not called
410
- # directly
411
- def raw_write(path, off, sz, buf, raw = nil)
412
- file = raw || File.open(path, "w")
413
- file.seek(off, :SET)
414
- file.write(buf[0...sz])
415
- ensure
416
- file.close if raw.nil?
518
+ # @raise [Errno]
519
+ # Unless the 'default_permissions' mount option is given, this method
520
+ # should check if opendir is permitted for this directory. Optionally
521
+ # opendir may also return an arbitrary filehandle in the fuse_file_info
522
+ # structure, which will be available to {#readdir}, {#fsyncdir},
523
+ # {#releasedir}.
524
+ # @note This method should usually only be called by FUSE, and not
525
+ # called directly
526
+ def opendir(context, path, ffi)
527
+ check_listable(context.uid, path)
528
+ ffi.fh = Dir.new(homepath(context.uid, path))
529
+ end
530
+
531
+ # Read data from an open file
532
+ # @overload read(context,path,size,offset,ffi)
533
+ # @abstract
534
+ # @param [Context] context
535
+ # @param [String] path
536
+ # @param [Integer] size
537
+ # @param [Integer] offset
538
+ # @param [FileInfo] ffi
539
+ # @return [String] should be exactly the number of bytes requested, or
540
+ # empty string on EOF
541
+ # @raise [Errno]
542
+ # @note This method should usually only be called by FUSE, and not
543
+ # called directly
544
+ def read(context, path, size, offset, ffi)
545
+ if offset < 0
546
+ ffi.fh.seek(offset, :END)
547
+ else
548
+ ffi.fh.seek(offset, :SET)
549
+ end
550
+ ffi.fh.read(size)
551
+ end
552
+
553
+ # (see RFuse::Fuse#readdir)
554
+ # @note This method should usually only be called by FUSE, and not
555
+ # called directly
556
+ def readdir(context, path, filler, offset, ffi)
557
+ filler.push(".", nil, 0)
558
+ filler.push("..", nil, 0)
559
+ ffi.fh.pos = offset
560
+ ffi.fh.each do |filename|
561
+ next if filename == '.' || filename == '..'
562
+ filler.push(filename, nil, 0)
563
+ end
417
564
  end
565
+
566
+ # Resolve target of symbolic link
567
+ # @overload readlink(context,path,size)
568
+ # @abstract
569
+ # @param [Context] context
570
+ # @param [String] path
571
+ # @param [Integer] size if the resolved path is greater than this size
572
+ # it should be truncated
573
+ # @return [String] the resolved link path
574
+ # @raise [Errno]
575
+ # @note This method should usually only be called by FUSE, and not
576
+ # called directly
577
+ def readlink(context, path, size)
578
+ # Is it okay that we return the 'real' path here?
579
+ File.readlink(homepath(context.uid, path))[0 ... size]
580
+ end
581
+
582
+ # Release an open file
583
+ # @overload release(context,path,ffi)
584
+ # @abstract
585
+ # @param [Context] context
586
+ # @param [String] path
587
+ # @param [FileInfo] ffi
588
+ # @return [void]
589
+ # Release is called when there are no more references to an open file:
590
+ # all file descriptors are closed and all memory mappings are unmapped.
591
+ #
592
+ # For every {#open} call there will be exactly one {#release} call with
593
+ # the same flags and file descriptor. It is possible to have a file
594
+ # opened more than once, in which case only the last release will mean,
595
+ # that no more reads/writes will happen on the file.
596
+ # @note This method should usually only be called by FUSE, and not
597
+ # called directly
598
+ def release(context, path, ffi)
599
+ ffi.fh.close
600
+ end
601
+
602
+ # (see RFuse::Fuse#rename)
603
+ # @note This method should usually only be called by FUSE, and not
604
+ # called directly
605
+ def rename(context, from, to)
606
+ hfrom = homepath(context.uid, from)
607
+ hto = homepath(context.uid, to)
608
+ check_readable(context.uid, from)
609
+ check_writable(File.dirname(from), context.uid)
610
+ check_writable(File.dirname(to), context.uid)
611
+ FileUtils.mv(hfrom, hto, :force => true)
612
+ end
613
+
614
+ # Create a symbolic link
615
+ # @overload symlink(context,to,from)
616
+ # @abstract
617
+ # @param [Context] context
618
+ # @param [String] to
619
+ # @param [String] from
620
+ # @return [void]
621
+ # @raise [Errno]
622
+ # Create a symbolic link named "from" which, when evaluated, will lead
623
+ # to "to".
624
+ # @note This method should usually only be called by FUSE, and not
625
+ # called directly
626
+ def symlink(context, to, from)
627
+ check_writable(context.uid, from)
628
+ hfrom = homepath(context.uid, from)
629
+ File.symlink(to, hfrom)
630
+ begin
631
+ File.lchown(context.uid, context.gid, hfrom)
632
+ rescue
633
+ end
634
+ end
635
+
636
+ # Change the size of a file
637
+ # @overload truncate(context,path,offset)
638
+ # @abstract
639
+ # @param [Context] context
640
+ # @param [String] path
641
+ # @param [Integer] offset
642
+ # @return [void]
643
+ # @raise [Errno]
644
+ # @note This method should usually only be called by FUSE, and not
645
+ # called directly
646
+ def truncate(context, path, offset)
647
+ check_writable(context.uid, path)
648
+ File.truncate(homepath(context.uid, path), offset)
649
+ end
650
+
651
+ # Remove a file
652
+ # @overload unlink(context,path)
653
+ # @abstract
654
+ # @param [Context] context
655
+ # @param [String] path
656
+ # @return [void]
657
+ # @raise [Errno]
658
+ # @note This method should usually only be called by FUSE, and not
659
+ # called directly
660
+ def unlink(context, path)
661
+ check_writable(context.uid, File.dirname(path))
662
+ File.unlink(homepath(context.uid, path))
663
+ end
664
+
665
+ # Change access/modification times of a file
666
+ # @overload utimens(context,path,actime,modtime)
667
+ # @abstract
668
+ # @param [Context] context
669
+ # @param [String] path
670
+ # @param [Integer] actime access time in nanoseconds
671
+ # @param [Integer] modtime modification time in nanoseconds
672
+ # @return [void]
673
+ # @raise [Errno]
674
+ # @note This method should usually only be called by FUSE, and not
675
+ # called directly
676
+ def utimens(context, path, actime, modtime)
677
+ check_writable(context.uid, path)
678
+ File.utime(actime, modtime, homepath(context.uid, path))
679
+ end
680
+
681
+ # Write data to an open file
682
+ # @overload write(context,path,data,offset,ffi)
683
+ # @abstract
684
+ # @param [Context] context
685
+ # @param [String] path
686
+ # @param [String] data
687
+ # @param [Integer] offset
688
+ # @param [FileInfo] ffi
689
+ # @return [Integer] exactly the number of bytes requested except on
690
+ # error
691
+ # @raise [Errno]
692
+ # @note This method should usually only be called by FUSE, and not
693
+ # called directly
694
+ def write(context, path, data, offset, ffi)
695
+ if offset < 0
696
+ ffi.fh.seek(offset, :END)
697
+ else
698
+ ffi.fh.seek(offset, :SET)
699
+ end
700
+ ffi.fh.write(data)
701
+ end
702
+
703
+ begin
704
+
705
+ require 'ffi-xattr'
706
+
707
+ # Get extended attribute
708
+ # @overload getxattr(context,path,name)
709
+ # @abstract
710
+ # @param [Context] context
711
+ # @param [String] path
712
+ # @param [String] name
713
+ # @return [String] attribute value
714
+ # @raise [Errno] Errno::ENOATTR if attribute does not exist
715
+ # @note This method should usually only be called by FUSE, and not
716
+ # called directly
717
+ # @note This method is only defined if the gem 'ffi-xattr' is
718
+ # available
719
+ def getxattr(context, path, name)
720
+ check_readable(context.uid, path)
721
+ hpath = homepath(context.uid, path)
722
+ check_nodata Xattr::Lib.get(hpath, false, name)
723
+ end
724
+
725
+ # Set extended attributes
726
+ # @overload setxattr(context,path,name,data,flags)
727
+ # @abstract
728
+ # @param [Context] context
729
+ # @param [String] path
730
+ # @param [String] name
731
+ # @param [String] data
732
+ # @param [Integer] flags
733
+ # @return [void]
734
+ # @raise [Errno]
735
+ # @note This method should usually only be called by FUSE, and not
736
+ # called directly
737
+ # @note This method is only defined if the gem 'ffi-xattr' is
738
+ # available
739
+ def setxattr(context, path, name, data, flags)
740
+ check_writable(context.uid, path)
741
+ hpath = homepath(context.uid, path)
742
+ Xattr::Lib.set(hpath, false, name, data)
743
+ end
744
+
745
+ # List extended attributes
746
+ # @overload listxattr(context,path)
747
+ # @abstract
748
+ # @param [Context] context
749
+ # @param [String] path
750
+ # @return [Array<String>] list of attribute names
751
+ # @raise [Errno]
752
+ # @note This method should usually only be called by FUSE, and not
753
+ # called directly
754
+ # @note This method is only defined if the gem 'ffi-xattr' is
755
+ # available
756
+ def listxattr(context, path)
757
+ check_readable(context.uid, path)
758
+ hpath = homepath(context.uid, path)
759
+ check_nodata Xattr::Lib.list(hpath, false)
760
+ end
761
+
762
+ # Remove extended attribute
763
+ # @overload removexattr(context,path,name)
764
+ # @abstract
765
+ # @param [Context] context
766
+ # @param [String] path
767
+ # @param [String] name attribute to remove
768
+ # @return [void]
769
+ # @raise [Errno]
770
+ # @note This method should usually only be called by FUSE, and not
771
+ # called directly
772
+ # @note This method is only defined if the gem 'ffi-xattr' is
773
+ # available
774
+ def removexattr(context, path, name)
775
+ check_writable(context.uid, path)
776
+ hpath = homepath(context.uid, path)
777
+ Xattr::Lib.remove(hpath, false, name)
778
+ end
779
+
780
+ # Raise Errno::ENODATA if _value_ is nil. Otherwise, return _value_.
781
+ # @param [Object] value the value to check
782
+ # @return [Object] the given value
783
+ # @raise [Errno::ENODATA] if value is nil
784
+ def check_nodata(value)
785
+ if value.nil?
786
+ fail Errno::ENODATA
787
+ end
788
+ value
789
+ end
790
+ private :check_nodata
791
+
792
+ rescue LoadError # require 'ffi-xattr'
793
+ # We don't have the library, so we don't define those methods
794
+ end
795
+
418
796
  end
data/lib/homefs.rb CHANGED
@@ -1 +1,2 @@
1
1
  require 'homefs/homefs'
2
+ require 'homefs/debug'
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homefs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dylan Frese
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-26 00:00:00.000000000 Z
11
+ date: 2015-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rfusefs
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rb-inotify
14
+ name: rfuse
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
@@ -39,9 +25,9 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
27
  description: |-
42
- FuseFS currently written in Ruby that directs filesystem
43
- calls to a directory relative to the calling user's home
44
- directory.
28
+ FUSE filesystem currently written in Ruby that directs
29
+ filesystem calls to a directory relative to the calling
30
+ user's home directory.
45
31
  email: dmfrese@gmail.com
46
32
  executables:
47
33
  - homefs
@@ -50,6 +36,7 @@ extra_rdoc_files: []
50
36
  files:
51
37
  - bin/homefs
52
38
  - lib/homefs.rb
39
+ - lib/homefs/debug.rb
53
40
  - lib/homefs/homefs.rb
54
41
  homepage:
55
42
  licenses: