homefs 0.2.1 → 0.3.0
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.
- checksums.yaml +4 -4
- data/bin/homefs +43 -29
- data/lib/homefs/debug.rb +21 -0
- data/lib/homefs/homefs.rb +609 -231
- data/lib/homefs.rb +1 -0
- metadata +7 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f11d42ee83c4b4ebf69e367d4ae58cc1a4927fac
|
4
|
+
data.tar.gz: c8bba0a2cad6c689a3865c910c7d172c14bb9933
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
5
|
-
|
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
|
-
|
14
|
-
|
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
|
-
|
23
|
-
$
|
24
|
-
|
25
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
67
|
+
RFuse.main(ARGV + ["-o", "allow_other"]) { fs }
|
data/lib/homefs/debug.rb
ADDED
@@ -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 '
|
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.
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
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
|
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|
|
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 [
|
133
|
-
# if the
|
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,
|
138
|
+
def mode_mask(file, mask, uid = nil)
|
137
139
|
stat = File.stat(file)
|
138
140
|
fmode = stat.mode
|
139
|
-
if
|
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
|
-
|
167
|
+
mode_mask(path, 0222, uid)
|
166
168
|
end
|
167
169
|
end
|
168
170
|
|
169
|
-
#
|
170
|
-
#
|
171
|
-
#
|
172
|
-
# @param [String]
|
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
|
-
# @
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
#
|
181
|
-
#
|
182
|
-
# @param [String]
|
183
|
-
#
|
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
|
-
# @
|
186
|
-
#
|
187
|
-
|
188
|
-
|
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
|
-
#
|
192
|
-
#
|
193
|
-
#
|
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
|
-
# @
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
#
|
202
|
-
#
|
203
|
-
# @
|
204
|
-
#
|
205
|
-
#
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
#
|
212
|
-
# @
|
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
|
-
# @
|
215
|
-
#
|
216
|
-
|
217
|
-
|
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
|
-
#
|
221
|
-
# @
|
222
|
-
# @
|
223
|
-
# @
|
224
|
-
#
|
225
|
-
|
226
|
-
|
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
|
-
#
|
230
|
-
# @
|
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
|
-
# @
|
233
|
-
#
|
234
|
-
|
235
|
-
|
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
|
-
#
|
239
|
-
# @
|
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
|
-
# @
|
242
|
-
#
|
243
|
-
|
244
|
-
|
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
|
-
#
|
248
|
-
# @
|
249
|
-
# @
|
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
|
-
# @
|
252
|
-
#
|
253
|
-
|
254
|
-
|
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
|
-
#
|
258
|
-
# @
|
259
|
-
# @
|
260
|
-
# @
|
261
|
-
#
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
#
|
267
|
-
# @
|
268
|
-
#
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
#
|
300
|
-
#
|
301
|
-
# @
|
302
|
-
# @
|
303
|
-
# @
|
304
|
-
#
|
305
|
-
|
306
|
-
|
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
|
310
|
-
# @
|
311
|
-
# @
|
312
|
-
# @
|
313
|
-
#
|
314
|
-
|
315
|
-
|
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
|
-
#
|
319
|
-
# @
|
320
|
-
# @
|
321
|
-
# @
|
322
|
-
#
|
323
|
-
|
324
|
-
|
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
|
-
#
|
328
|
-
# @
|
329
|
-
# @
|
330
|
-
# @
|
331
|
-
#
|
332
|
-
|
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
|
-
# @
|
382
|
-
#
|
383
|
-
|
384
|
-
|
385
|
-
|
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
|
-
#
|
389
|
-
# @
|
390
|
-
#
|
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
|
393
|
-
# directly
|
394
|
-
def
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
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
|
-
# @
|
410
|
-
#
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
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
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.
|
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-
|
11
|
+
date: 2015-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
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
|
-
|
43
|
-
calls to a directory relative to the calling
|
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:
|