homefs 0.1.1 → 0.2.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/lib/homefs/homefs.rb +283 -37
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e74057ef8ec81a7efa25cb238fa1d960ec7fa727
|
4
|
+
data.tar.gz: 6a9bff9e3760f6d4f35f5a368849e17d7caecb07
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 60a6183228c6436b89a30581a85e47211b9bcf1a56f6fe4e0c50d63b30929c1aa456266be3e2dfde148cec82dbbe062131f57be173d2875c149125e6f83f42cf
|
7
|
+
data.tar.gz: 0a58275485f48cafdc452161d78cdd030b57e9965b22c15608c443db60842386cbdfa4b5d164e0751387cb5f0d0752689af2a634db8e12bff09c34d94603b061
|
data/lib/homefs/homefs.rb
CHANGED
@@ -1,25 +1,89 @@
|
|
1
1
|
require 'rfusefs'
|
2
2
|
|
3
|
+
# An instance of HomeFS; this implements all FuseFS methods, and ultimately
|
4
|
+
# handles reading, writing, and inspection of files.
|
5
|
+
#
|
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.
|
12
|
+
# @author Dylan Frese
|
3
13
|
class HomeFS
|
14
|
+
|
15
|
+
# 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
|
+
# calling user's home directory, that is, the user interacting with the
|
18
|
+
# filesystem through open(2), read(2), write(2), etc.
|
19
|
+
# @param [String] path the path, relative to $HOME, to use as the root of
|
20
|
+
# the file system.
|
4
21
|
def initialize(path)
|
5
22
|
@relpath = path
|
6
|
-
@dirlinks = Hash.new
|
7
23
|
read_passwd
|
24
|
+
read_group
|
8
25
|
end
|
9
26
|
|
27
|
+
# Read or re-read /etc/passwd to update the internal table of home
|
28
|
+
# directories for each UID. This method does not try to guess as to which
|
29
|
+
# user IDs are owned by actual users of the system, and which are utility
|
30
|
+
# accounts such as 'lp' or 'pulse'.
|
31
|
+
# @return [void]
|
10
32
|
def read_passwd
|
11
|
-
#
|
33
|
+
# Try to read from getent
|
34
|
+
passwd = `getent passwd`
|
35
|
+
if $? != 0
|
36
|
+
passwd = File.read('/etc/passwd')
|
37
|
+
end
|
38
|
+
|
39
|
+
@homedirs = Hash.new
|
40
|
+
@usernames = Hash.new
|
41
|
+
|
42
|
+
# Here we map each line in passwd to an array,
|
12
43
|
# which Hash interprets as a key, value pair.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
44
|
+
passwd.each_line.map { |line|
|
45
|
+
next if line.strip.empty?
|
46
|
+
values = line.split(':')
|
47
|
+
# Username, UID, home directory
|
48
|
+
[values[0], Integer(values[2]), values[5]]
|
49
|
+
}.reject { |line|
|
50
|
+
line.nil?
|
51
|
+
}.each { |username, uid, home|
|
52
|
+
@homedirs[uid] = home
|
53
|
+
@usernames[username] = uid
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Read or re-read /etc/group to update the internal table of group
|
58
|
+
# group membership.
|
59
|
+
# @return [void]
|
60
|
+
def read_group
|
61
|
+
# Try to read from getent
|
62
|
+
group = `getent group`
|
63
|
+
if $? != 0
|
64
|
+
group = File.read('/etc/group')
|
65
|
+
end
|
66
|
+
|
67
|
+
@groups = Hash.new
|
68
|
+
|
69
|
+
group.each_line.map { |line|
|
70
|
+
next if line.strip.empty?
|
71
|
+
_name, _, gid, members = line.split(':')
|
72
|
+
members = members.strip.split(',').map {|member| p member; @usernames[member]}
|
73
|
+
[Integer(gid), members]
|
74
|
+
}.reject { |line|
|
75
|
+
line.nil?
|
76
|
+
}.each { |gid, members|
|
77
|
+
@groups[gid] = members
|
78
|
+
}
|
21
79
|
end
|
22
80
|
|
81
|
+
# Return the path to the root of HomeFS relative to the underlying
|
82
|
+
# filesystem. If _path_ is specified, it is taken to be relative to the
|
83
|
+
# root of HomeFS.
|
84
|
+
# @return [String] path to the root of the HomeFS relative to the underlying
|
85
|
+
# filesystem, or if _path_ is specified, the path to that resource
|
86
|
+
# relative to the underlying filesystem.
|
23
87
|
def homepath(path = nil)
|
24
88
|
uid = FuseFS.reader_uid
|
25
89
|
basepath = @homedirs[uid]
|
@@ -27,85 +91,184 @@ class HomeFS
|
|
27
91
|
# in case.
|
28
92
|
raise Errno::ENOENT if basepath == nil
|
29
93
|
if path
|
30
|
-
|
94
|
+
File.join(basepath, @relpath, path)
|
31
95
|
else
|
32
|
-
|
96
|
+
File.join(basepath, @relpath)
|
33
97
|
end
|
34
98
|
end
|
35
99
|
|
100
|
+
# Check whether the given file has a mode that matches the given mask. If
|
101
|
+
# check_ids is true, the third-least significant digit of the mask is set to
|
102
|
+
# zero if the calling user is not the owner of the file, and the
|
103
|
+
# second-least significant digit of the mask is zeroed if the calling user's
|
104
|
+
# group is not the group of the file. This has the effect of only checking
|
105
|
+
# the owner's permissions if we're the owner, and only checking the group
|
106
|
+
# permissions if we're in that group.
|
107
|
+
#
|
108
|
+
# This method checks if _any_ of the set bits in the mask are set in the
|
109
|
+
# file's mode. It does not check if the mask matches the file's mode, nor
|
110
|
+
# does it check if _all_ set bits in the mask are set in the file's mode.
|
111
|
+
#
|
112
|
+
# The first three least significant digits of the mode, in octal, (that is,
|
113
|
+
# the three rightmost digits) control the read (4), write (2), and execute
|
114
|
+
# (1) permissions of the file. The next digit controls setuid (4), setgid
|
115
|
+
# (2), and the sticky bit (1) of the file.
|
116
|
+
#
|
117
|
+
# It is recommended to see
|
118
|
+
# man chmod
|
119
|
+
# to get a more detailed description about file modes.
|
120
|
+
#
|
121
|
+
# @example Test if we can write to a file
|
122
|
+
# mode_mask("/example", 0222, true)
|
123
|
+
# mode_mask("/example", 0222)
|
124
|
+
# @example Test if a file is executable by anyone
|
125
|
+
# mode_mask("/binary", 0111, false)
|
126
|
+
# @example Test if a directory has the sticky bit set
|
127
|
+
# mode_mask("/tmp", 01000, false)
|
128
|
+
#
|
129
|
+
# @param [String] file the path to the file to check
|
130
|
+
# @param [Integer] mask the mask against which to check the file's mode.
|
131
|
+
# 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.
|
135
|
+
# @return [Boolean] whether the mode of _file_ matches the given mask
|
36
136
|
def mode_mask(file, mask, check_ids = true)
|
37
137
|
stat = File.stat(file)
|
38
|
-
# First digit: setuid (4), setgid (2), sticky bit (1)
|
39
|
-
# Second, third, and fourth digits: user, group, and
|
40
|
-
# world permissions for read (4), write (2), execute (1)
|
41
|
-
# (see `man chmod` for more)
|
42
138
|
fmode = stat.mode
|
43
139
|
if check_ids
|
44
140
|
fuid, fgid = stat.uid, stat.gid
|
45
|
-
uid
|
141
|
+
uid = FuseFS.reader_uid
|
46
142
|
# Zero out the third digit (in octal).
|
47
143
|
# We could use a constant here, but this works
|
48
144
|
# for a mask of any length
|
49
145
|
if uid != fuid
|
50
146
|
mask &= ~(mask & 0700)
|
51
147
|
end
|
52
|
-
if
|
148
|
+
if !@groups[fgid].include?(uid)
|
53
149
|
mask &= ~(mask & 0070)
|
54
150
|
end
|
55
151
|
end
|
56
152
|
fmode & mask != 0
|
57
153
|
end
|
58
154
|
|
155
|
+
# Test whether we can write to the given path. If no file exists at _path_,
|
156
|
+
# we test if the parent directory is either writable or has the sticky bit
|
157
|
+
# set.
|
158
|
+
# @param [String] path the path to test
|
159
|
+
# @return [Boolean] whether the given path is writable
|
160
|
+
def writable?(path)
|
161
|
+
return true if mode_mask(path, 0222)
|
162
|
+
if !File.exist?(path)
|
163
|
+
mode_mask(File.dirname(path), 01222)
|
164
|
+
else
|
165
|
+
false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
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
|
173
|
+
# @return [void]
|
174
|
+
# @note This method should usually only be called by FUSE, and not called
|
175
|
+
# directly
|
59
176
|
def rename(from_path, to_path)
|
60
177
|
FileUtils.mv(homepath(from_path), homepath(to_path))
|
61
178
|
end
|
62
179
|
|
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
|
184
|
+
# @return [void]
|
185
|
+
# @note This method should usually only be called by FUSE, and not called
|
186
|
+
# directly
|
63
187
|
def touch(path, modtime)
|
64
188
|
File.utime(modtime, modtime, homepath(path))
|
65
189
|
end
|
66
190
|
|
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
|
194
|
+
# @return [void]
|
195
|
+
# @note This method should usually only be called by FUSE, and not called
|
196
|
+
# directly
|
67
197
|
def rmdir(path)
|
68
198
|
FileUtils.rmdir(homepath(path))
|
69
199
|
end
|
70
200
|
|
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
|
71
206
|
def can_rmdir?(path)
|
72
|
-
File.writable?(homepath(path))
|
207
|
+
File.writable?(homepath(path)) &&
|
208
|
+
Dir.new(path).to_a.size == 2
|
73
209
|
end
|
74
210
|
|
211
|
+
# Make a new directory at _path_. This is similar to mkdir(1).
|
212
|
+
# @param [String] path the path to the directory to make
|
213
|
+
# @return [void]
|
214
|
+
# @note This method should usually only be called by FUSE, and not called
|
215
|
+
# directly
|
75
216
|
def mkdir(path)
|
76
217
|
FileUtils.mkdir(homepath(path))
|
77
218
|
end
|
78
219
|
|
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
|
79
225
|
def can_mkdir?(path)
|
80
|
-
writable?(
|
226
|
+
writable?(homepath(path))
|
81
227
|
end
|
82
228
|
|
229
|
+
# Delete the file given by _path_. This is similar to rm(1).
|
230
|
+
# @param [String] path the path to the file to delete
|
231
|
+
# @return [void]
|
232
|
+
# @note This method should usually only be called by FUSE, and not called
|
233
|
+
# directly
|
83
234
|
def delete(path)
|
84
235
|
FileUtils.rm(homepath(path))
|
85
236
|
end
|
86
237
|
|
238
|
+
# Test if we can delete the file given by _path_.
|
239
|
+
# @param [String] path the path to the file to test
|
240
|
+
# @return [void]
|
241
|
+
# @note This method should usually only be called by FUSE, and not called
|
242
|
+
# directly
|
87
243
|
def can_delete?(path)
|
88
244
|
writable?(File.dirname(homepath(path)))
|
89
245
|
end
|
90
246
|
|
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
|
250
|
+
# @return [void]
|
251
|
+
# @note This method should usually only be called by FUSE, and not called
|
252
|
+
# directly
|
91
253
|
def write_to(path, str)
|
92
254
|
File.open(homepath(path), "wb") {|file| file.write(str) }
|
93
255
|
end
|
94
256
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
99
262
|
def can_write?(path)
|
100
|
-
|
101
|
-
if File.exist?(hpath)
|
102
|
-
writable?(hpath)
|
103
|
-
else
|
104
|
-
# FIXME: We don't support sticky bits
|
105
|
-
writable?(File.dirname(hpath))
|
106
|
-
end
|
263
|
+
writable?(homepath(path))
|
107
264
|
end
|
108
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
|
109
272
|
def times(path)
|
110
273
|
atime = File.atime(homepath(path))
|
111
274
|
mtime = File.mtime(homepath(path))
|
@@ -113,32 +276,73 @@ class HomeFS
|
|
113
276
|
[atime, mtime, ctime]
|
114
277
|
end
|
115
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
|
116
284
|
def executable?(path)
|
117
285
|
mode_mask(homepath(path), 0111)
|
118
286
|
end
|
119
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
|
120
293
|
def read_file(path)
|
121
294
|
File.open(homepath(path), "rb") do |file|
|
122
295
|
file.read
|
123
296
|
end
|
124
297
|
end
|
125
298
|
|
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
|
126
305
|
def contents(path)
|
127
306
|
Dir.new(homepath(path)).to_a
|
128
307
|
end
|
129
308
|
|
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
|
130
314
|
def size(path)
|
131
315
|
File.size(homepath(path))
|
132
316
|
end
|
133
317
|
|
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
|
134
323
|
def file?(path)
|
135
324
|
File.file?(homepath(path))
|
136
325
|
end
|
137
326
|
|
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
|
138
332
|
def directory?(path)
|
139
333
|
File.directory?(homepath(path))
|
140
334
|
end
|
141
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
|
142
346
|
def raw_open(path, mode, rfusefs = nil)
|
143
347
|
mode = case mode
|
144
348
|
when "rw" then File::RDWR | File::CREAT | File::BINARY
|
@@ -148,25 +352,67 @@ class HomeFS
|
|
148
352
|
File.open(homepath(path), mode)
|
149
353
|
end
|
150
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
|
151
367
|
def raw_read(path, offset, size, raw = nil)
|
152
|
-
raw
|
153
|
-
|
154
|
-
|
368
|
+
file = raw || raw_open(path, "r")
|
369
|
+
file.seek(offset, :SET)
|
370
|
+
file.read(size)
|
371
|
+
ensure
|
372
|
+
file.close if raw.nil?
|
155
373
|
end
|
156
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
|
380
|
+
# @return [void]
|
381
|
+
# @note This method should usually only be called by FUSE, and not called
|
382
|
+
# directly
|
157
383
|
def raw_sync(path, datasync, raw = nil)
|
158
384
|
return if raw.nil? # Should we sync anyway?
|
159
385
|
raw.fdatasync
|
160
386
|
end
|
161
387
|
|
388
|
+
# Close the file handle given by _raw_.
|
389
|
+
# @param path ignored
|
390
|
+
# @param [File] raw the file handle to close
|
391
|
+
# @return [void]
|
392
|
+
# @note This method should usually only be called by FUSE, and not called
|
393
|
+
# directly
|
162
394
|
def raw_close(path, raw = nil)
|
163
395
|
return if raw.nil? # ???
|
164
396
|
raw.close
|
165
397
|
end
|
166
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
|
408
|
+
# @return [void]
|
409
|
+
# @note This method should usually only be called by FUSE, and not called
|
410
|
+
# directly
|
167
411
|
def raw_write(path, off, sz, buf, raw = nil)
|
168
|
-
raw
|
169
|
-
|
170
|
-
|
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?
|
171
417
|
end
|
172
418
|
end
|