Class: HomeFS

Inherits:
Object
  • Object
show all
Defined in:
lib/homefs/homefs.rb

Overview

An instance of HomeFS; this implements all FuseFS methods, and ultimately handles reading, writing, and inspection of files.

We rely on the context provided to us by FUSE to determine the calling user’s UID and GID. If you want to access an instance of this class directly, you may set the UID and GID of the current thread like so

Thread.current[:fusefs_reader_uid] = uid
Thread.current[:fusefs_reader_gid] = gid

where ‘uid’ and ‘gid’ are the desired UID and GID you wish to emulate.

Author:

  • Dylan Frese

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ HomeFS

Creates a new instance of HomeFS. This instance should probably be passed to FuseFS, as it does nothing on its own. The path is relative to the calling user’s home directory, that is, the user interacting with the filesystem through open(2), read(2), write(2), etc. the file system.

Parameters:

  • path (String)

    the path, relative to $HOME, to use as the root of



21
22
23
24
25
# File 'lib/homefs/homefs.rb', line 21

def initialize(path)
    @relpath = path
    read_passwd
    read_group
end

Instance Method Details

#can_delete?(path) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Test if we can delete the file given by path.

Parameters:

  • path (String)

    the path to the file to test



243
244
245
# File 'lib/homefs/homefs.rb', line 243

def can_delete?(path)
    writable?(File.dirname(homepath(path)))
end

#can_mkdir?(path) ⇒ Boolean

Note:

This method should usually only be called by FUSE, and not called directly

Test if we can make a directory at the given path.

Parameters:

  • path (String)

    the path to test

Returns:

  • (Boolean)

    true if we can make a directory at path.



225
226
227
# File 'lib/homefs/homefs.rb', line 225

def can_mkdir?(path)
    writable?(homepath(path))
end

#can_rmdir?(path) ⇒ Boolean

Note:

This method should usually only be called by FUSE, and not called directly

Test whether we can remove the directory given by path.

Parameters:

  • path (String)

    the path to the directory to test

Returns:

  • (Boolean)

    true if the directory given can be removed.



206
207
208
209
# File 'lib/homefs/homefs.rb', line 206

def can_rmdir?(path)
    File.writable?(homepath(path)) &&
        Dir.new(path).to_a.size == 2
end

#can_write?(path) ⇒ Boolean

Note:

This method should usually only be called by FUSE, and not called directly

Test whether we can write out to the given path.

Parameters:

  • path (String)

    the path to test

Returns:

  • (Boolean)

    whether we can write to the given path



262
263
264
# File 'lib/homefs/homefs.rb', line 262

def can_write?(path)
    writable?(homepath(path))
end

#contents(path) ⇒ Array<String>

Note:

This method should usually only be called by FUSE, and not called directly

List the contents of a directory. This includes ‘.’ and ‘..’ (the current directory and its parent).

Parameters:

  • path (String)

    the path to the directory to list

Returns:

  • (Array<String>)

    a list of file names of files in the directory



305
306
307
# File 'lib/homefs/homefs.rb', line 305

def contents(path)
    Dir.new(homepath(path)).to_a
end

#delete(path) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Delete the file given by path. This is similar to rm(1).

Parameters:

  • path (String)

    the path to the file to delete



234
235
236
# File 'lib/homefs/homefs.rb', line 234

def delete(path)
    FileUtils.rm(homepath(path))
end

#directory?(path) ⇒ Boolean

Note:

This method should usually only be called by FUSE, and not called directly

Test whether path is the name of a directory

Parameters:

  • path (String)

    the path to test

Returns:

  • (Boolean)

    whether path represents a directory



332
333
334
# File 'lib/homefs/homefs.rb', line 332

def directory?(path)
    File.directory?(homepath(path))
end

#executable?(path) ⇒ Boolean

Note:

This method should usually only be called by FUSE, and not called directly

Test whether the file given by path is executable.

Parameters:

  • path (String)

    the path to the file to test

Returns:

  • (Boolean)

    whether the file at the given path is executable.



284
285
286
# File 'lib/homefs/homefs.rb', line 284

def executable?(path)
    mode_mask(homepath(path), 0111)
end

#file?(path) ⇒ Boolean

Note:

This method should usually only be called by FUSE, and not called directly

Test whether path is the name of a file (i.e., not a directory)

Parameters:

  • path (String)

    the path to test

Returns:

  • (Boolean)

    whether path represents a file



323
324
325
# File 'lib/homefs/homefs.rb', line 323

def file?(path)
    File.file?(homepath(path))
end

#homepath(path = nil) ⇒ String

Return the path to the root of HomeFS relative to the underlying filesystem. If path is specified, it is taken to be relative to the root of HomeFS.

Returns:

  • (String)

    path to the root of the HomeFS relative to the underlying filesystem, or if path is specified, the path to that resource relative to the underlying filesystem.

Raises:

  • (Errno::ENOENT)


87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/homefs/homefs.rb', line 87

def homepath(path = nil)
    uid = FuseFS.reader_uid
    basepath = @homedirs[uid]
    # basepath shouldn't ever be nil, but fail gracefully just
    # in case.
    raise Errno::ENOENT if basepath == nil
    if path
        File.join(basepath, @relpath, path)
    else
        File.join(basepath, @relpath)
    end
end

#mkdir(path) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Make a new directory at path. This is similar to mkdir(1).

Parameters:

  • path (String)

    the path to the directory to make



216
217
218
# File 'lib/homefs/homefs.rb', line 216

def mkdir(path)
    FileUtils.mkdir(homepath(path))
end

#mode_mask(file, mask, check_ids = true) ⇒ Boolean

Check whether the given file has a mode that matches the given mask. If check_ids is true, the third-least significant digit of the mask is set to zero if the calling user is not the owner of the file, and the second-least significant digit of the mask is zeroed if the calling user’s group is not the group of the file. This has the effect of only checking the owner’s permissions if we’re the owner, and only checking the group permissions if we’re in that group.

This method checks if any of the set bits in the mask are set in the file’s mode. It does not check if the mask matches the file’s mode, nor does it check if all set bits in the mask are set in the file’s mode.

The first three least significant digits of the mode, in octal, (that is, the three rightmost digits) control the read (4), write (2), and execute (1) permissions of the file. The next digit controls setuid (4), setgid (2), and the sticky bit (1) of the file.

It is recommended to see

man chmod

to get a more detailed description about file modes.

Examples:

Test if we can write to a file

mode_mask("/example", 0222, true)
mode_mask("/example", 0222)

Test if a file is executable by anyone

mode_mask("/binary", 0111, false)

Test if a directory has the sticky bit set

mode_mask("/tmp", 01000, false)

Parameters:

  • file (String)

    the path to the file to check

  • mask (Integer)

    the mask against which to check the file’s mode. It is recommended you write this in octal (with a leading 0).

  • check_ids (Boolean) (defaults to: true)

    if true, only check user and group permissions if the caller is, respectively, the owner of the file/in the file’s group.

Returns:

  • (Boolean)

    whether the mode of file matches the given mask



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/homefs/homefs.rb', line 136

def mode_mask(file, mask, check_ids = true)
    stat = File.stat(file)
    fmode = stat.mode
    if check_ids
        fuid, fgid = stat.uid, stat.gid
        uid = FuseFS.reader_uid
        # Zero out the third digit (in octal).
        # We could use a constant here, but this works
        # for a mask of any length
        if uid != fuid
            mask &= ~(mask & 0700)
        end
        if !@groups[fgid].include?(uid)
            mask &= ~(mask & 0070)
        end
    end
    fmode & mask != 0
end

#raw_close(path, raw = nil) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Close the file handle given by raw.

Parameters:

  • path

    ignored

  • raw (File) (defaults to: nil)

    the file handle to close



394
395
396
397
# File 'lib/homefs/homefs.rb', line 394

def raw_close(path, raw = nil)
    return if raw.nil? # ???
    raw.close
end

#raw_open(path, mode, rfusefs = nil) ⇒ File

Note:

This method should usually only be called by FUSE, and not called directly

Open a file handle to the file given by path, with the mode given by mode. Right now, the supported modes are “rw”, “w”, and “r”, for read/write, write, and read. If the mode is write or read/write and no file exists at path, on will be created.

Parameters:

  • path (String)

    the path to the file to open

  • mode (String)

    the mode to open the file with

  • rfusefs (defaults to: nil)

    ignored

Returns:

  • (File)

    a file handle of the file given by path



346
347
348
349
350
351
352
353
# File 'lib/homefs/homefs.rb', line 346

def raw_open(path, mode, rfusefs = nil)
    mode = case mode
           when "rw" then File::RDWR | File::CREAT | File::BINARY
           when "r"  then File::RDONLY | File::BINARY
           when "w"  then File::WRONLY | File::CREAT | File::BINARY
           end
    File.open(homepath(path), mode)
end

#raw_read(path, offset, size, raw = nil) ⇒ String

Note:

This method should usually only be called by FUSE, and not called directly

Read size bytes, starting at offset, from the file handle given by raw (which is returned by a call to raw_open). If raw is nil and a path is given, a file will be opened at path, and closed after the data is read. Otherwise, path is ignored.

Parameters:

  • path (String)

    the path to the file

  • offset (Integer)

    the offset, in bytes, from the start of the file to start reading from.

  • size (Integer)

    the amount of bytes to read

  • raw (File) (defaults to: nil)

    the file handle to read from

Returns:

  • (String)

    a binary string of the requested data



367
368
369
370
371
372
373
# File 'lib/homefs/homefs.rb', line 367

def raw_read(path, offset, size, raw = nil)
    file = raw || raw_open(path, "r")
    file.seek(offset, :SET)
    file.read(size)
ensure
    file.close if raw.nil?
end

#raw_sync(path, datasync, raw = nil) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Sync writes to the underlying filesystem. If raw is nil, no operation is performed.

Parameters:

  • path

    ignored

  • datasync

    ignored

  • raw (File) (defaults to: nil)

    the file handle to sync



383
384
385
386
# File 'lib/homefs/homefs.rb', line 383

def raw_sync(path, datasync, raw = nil)
    return if raw.nil? # Should we sync anyway?
    raw.fdatasync
end

#raw_write(path, off, sz, buf, raw = nil) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Write sz bytes from buf to the file handle raw, starting at off. If no file handle is given, one will be opened at the given path, and closed after the write is complete.

Parameters:

  • path (String)

    the path of the file

  • off (Integer)

    the offset, in bytes, to starting writing to the file at

  • sz (Integer)

    the amount of bytes to read from buf

  • buf (String)

    a binary string containing the data to be written

  • raw (File) (defaults to: nil)

    the file handle to write to



411
412
413
414
415
416
417
# File 'lib/homefs/homefs.rb', line 411

def raw_write(path, off, sz, buf, raw = nil)
    file = raw || File.open(path, "w")
    file.seek(off, :SET)
    file.write(buf[0...sz])
ensure
    file.close if raw.nil?
end

#read_file(path) ⇒ String

Note:

This method should usually only be called by FUSE, and not called directly

Read the contents of the file given by path.

Parameters:

  • path (String)

    the path of the file to read

Returns:

  • (String)

    a binary string of the contents of the file



293
294
295
296
297
# File 'lib/homefs/homefs.rb', line 293

def read_file(path)
    File.open(homepath(path), "rb") do |file|
        file.read
    end
end

#read_groupvoid

This method returns an undefined value.

Read or re-read /etc/group to update the internal table of group group membership.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/homefs/homefs.rb', line 60

def read_group
    # Try to read from getent
    group = `getent group`
    if $? != 0
        group = File.read('/etc/group')
    end

    @groups = Hash.new

    group.each_line.map { |line|
        next if line.strip.empty?
        _name, _, gid, members = line.split(':')
        members = members.strip.split(',').map {|member| p member; @usernames[member]}
        [Integer(gid), members]
    }.reject { |line|
        line.nil?
    }.each { |gid, members|
        @groups[gid] = members
    }
end

#read_passwdvoid

This method returns an undefined value.

Read or re-read /etc/passwd to update the internal table of home directories for each UID. This method does not try to guess as to which user IDs are owned by actual users of the system, and which are utility accounts such as ‘lp’ or ‘pulse’.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/homefs/homefs.rb', line 32

def read_passwd
    # Try to read from getent
    passwd = `getent passwd`
    if $? != 0
        passwd = File.read('/etc/passwd')
    end

    @homedirs = Hash.new
    @usernames = Hash.new

    # Here we map each line in passwd to an array,
    # which Hash interprets as a key, value pair.
    passwd.each_line.map { |line|
        next if line.strip.empty?
        values = line.split(':')
        # Username, UID, home directory
        [values[0], Integer(values[2]), values[5]]
    }.reject { |line|
        line.nil?
    }.each { |username, uid, home|
        @homedirs[uid] = home
        @usernames[username] = uid
    }
end

#rename(from_path, to_path) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Rename the file or directory at from_path to to_path. This is mostly equivalent to /bin/mv.

Parameters:

  • from_path (String)

    the path of the file to be moved

  • to_path (String)

    the destination of the file



176
177
178
# File 'lib/homefs/homefs.rb', line 176

def rename(from_path, to_path)
    FileUtils.mv(homepath(from_path), homepath(to_path))
end

#rmdir(path) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Remove the directory at the given path. The directory must be empty. This is mostly equivalent to rmdir(1).

Parameters:

  • path (String)

    the path to the directory to remove



197
198
199
# File 'lib/homefs/homefs.rb', line 197

def rmdir(path)
    FileUtils.rmdir(homepath(path))
end

#size(path) ⇒ Integer

Note:

This method should usually only be called by FUSE, and not called directly

Get the size of the file at path

Parameters:

  • path (String)

    the path to the file or directory to get the size of

Returns:

  • (Integer)

    the size, in bytes, of the file



314
315
316
# File 'lib/homefs/homefs.rb', line 314

def size(path)
    File.size(homepath(path))
end

#times(path) ⇒ Array<Time>

Note:

This method should usually only be called by FUSE, and not called directly

Get the times (atime, mtime, ctime) for the file at the given path.

Parameters:

  • path (String)

    the path to the file to get the times for

Returns:

  • (Array<Time>)

    an array of size three of the last-accessed time, the last-modified time, and the creation time for the file



272
273
274
275
276
277
# File 'lib/homefs/homefs.rb', line 272

def times(path)
    atime = File.atime(homepath(path))
    mtime = File.mtime(homepath(path))
    ctime = File.ctime(homepath(path))
    [atime, mtime, ctime]
end

#touch(path, modtime) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Update the mtime and atime (last-modified and last-accessed time) of the file at path to the time given by modtime.

Parameters:

  • path (String)

    the path to the file to touch

  • modtime (Time)

    the time to update the file’s times to



187
188
189
# File 'lib/homefs/homefs.rb', line 187

def touch(path, modtime)
    File.utime(modtime, modtime, homepath(path))
end

#writable?(path) ⇒ Boolean

Test whether we can write to the given path. If no file exists at path, we test if the parent directory is either writable or has the sticky bit set.

Parameters:

  • path (String)

    the path to test

Returns:

  • (Boolean)

    whether the given path is writable



160
161
162
163
164
165
166
167
# File 'lib/homefs/homefs.rb', line 160

def writable?(path)
    return true if mode_mask(path, 0222)
    if !File.exist?(path)
        mode_mask(File.dirname(path), 01222)
    else
        false
    end
end

#write_to(path, str) ⇒ void

Note:

This method should usually only be called by FUSE, and not called directly

This method returns an undefined value.

Write the given data to the given path.

Parameters:

  • path (String)

    the path to write to

  • str (String)

    a binary string of data to write



253
254
255
# File 'lib/homefs/homefs.rb', line 253

def write_to(path, str)
    File.open(homepath(path), "wb") {|file| file.write(str) }
end