Class: FFI::Libfuse::Filesystem::VirtualDir

Inherits:
VirtualNode
  • Object
show all
Includes:
Utils
Defined in:
lib/ffi/libfuse/filesystem/virtual_dir.rb

Overview

A Filesystem of Filesystems

Implements a simple Hash based directory of sub filesystems.

FUSE Callbacks

If path is root ('/') then the operation applies to this directory itself

If the path is a simple basename (with leading slash and no others) then the operation applies to an entry in this directory. The operation is handled by the directory and then passed on to the entry itself (with path = '/')

Otherwise it is passed on to the next entry via #path_method

Constraints

Instance Attribute Summary collapse

Attributes included from Ruby::VirtualNode

#accounting, #virtual_stat, #virtual_xattr

FUSE Callbacks collapse

Instance Method Summary collapse

Methods included from Utils

#directory?, #empty_dir?, #empty_file?, #exists?, #file?, #mkdir_p, #stat

Methods included from Ruby::VirtualNode

#chmod, #chown, #getxattr, #init_node, #listxattr, #statfs, #utimens

Constructor Details

#initialize(accounting: Accounting.new) ⇒ VirtualDir

Returns a new instance of VirtualDir.



42
43
44
45
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 42

def initialize(accounting: Accounting.new)
  @entries = {}
  super
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object (private)



395
396
397
398
399
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 395

def method_missing(method, *args, &block)
  return super unless FuseOperations.path_callbacks.include?(method)

  path_method(method, *args, block: block)
end

Instance Attribute Details

#entriesHash<String,FuseOperations> (readonly)

Returns our directory entries.

Returns:



40
41
42
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 40

def entries
  @entries
end

Instance Method Details

#create(path, mode = FuseContext.get.mask(0o644), ffi = nil) {|String| ... } ⇒ Object

For our entries, creates a new file

Yields:

  • (String)

    filename the name of the file in this directory

Yield Returns:

  • (:getattr)

    something that quacks with the FUSE Callbacks of a regular file

    :create or :mknod + :open will be attempted with path = '/' on this file

Returns:

Raises:

  • (Errno::EISDIR)

    if the entry exists and responds_to?(:readdir)

  • (Errno::EEXIST)

    if the entry exists



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 149

def create(path, mode = FuseContext.get.mask(0o644), ffi = nil, &file)
  raise Errno::EISDIR if root?(path)

  # fuselib will fallback to mknod on ENOSYS on a case by case basis
  path_method(__method__, path, mode, ffi, notsup: Errno::ENOSYS, block: file) do |name, existing|
    raise Errno::EISDIR if dir_entry?(existing)
    raise Errno::EEXIST if existing

    # TODO: Strictly should understand setgid and sticky bits of this dir's mode when creating new files
    new_file = file ? file.call(name) : new_file(name)
    if entry_fuse_respond_to?(new_file, :create)
      new_file.create('/', mode, ffi)
    else
      # TODO: generate a sensible device number
      entry_send(new_file, :mknod, '/', mode, 0)
      entry_send(new_file, :open, '/', ffi)
    end
    entries[name] = new_file
  end
end

#getattr(path, stat_buf = nil, ffi = nil) ⇒ Object

For the root path provides this directory's stat information, otherwise passes on to the next filesystem



52
53
54
55
56
57
58
59
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 52

def getattr(path, stat_buf = nil, ffi = nil)
  if root?(path)
    stat_buf&.directory(nlink: entries.size + 2, **virtual_stat)
    return self
  end

  path_method(__method__, path, stat_buf, ffi, notsup: Errno::ENOSYS)
end

Create a new hard link in this filesystem

Parameters:

  • from_path (String, nil)
  • to_path (String)

Yields:

  • (existing)

    Used to retrieve the filesystem object at from_path to be linked at to_path

    If not supplied, a proc wrapping ##new_link is created and used or passed on to sub-filesystems

Yield Parameters:

Yield Returns:

  • (FuseOperations)

    an object representing an inode to be linked at to_path

Raises:

  • (Errno::EISDIR)

    if this object is trying to be added as a link (since you can't hard link directories)

See Also:



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 215

def link(from_path, to_path, &linker)
  # Can't link to a directory
  raise Errno::EISDIR if root?(to_path)
  raise Errno::ENOSYS unless from_path || linker

  same_filesystem_method(__method__, from_path, to_path) do
    linker ||= proc { |replacing| new_link(from_path, replacing) }
    path_method(__method__, from_path, to_path, block: linker) do |link_name, existing|
      linked_entry = linker.call(existing)
      entries[link_name] = linked_entry
    end
  end
end

#mkdir(path, mode = FuseContext.get.mask(0o777)) {|String| ... } ⇒ Object

Creates a new directory entry in this directory

Parameters:

  • path (String)
  • mode (Integer) (defaults to: FuseContext.get.mask(0o777))

Yields:

  • (String)

    name the name of the directory in this filesystem

Yield Returns:

  • (Object)

    something that quacks with the FUSE Callbacks representing a directory

Returns:

Raises:

  • (Errno::EEXIST)

    if the entry already exists at path



184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 184

def mkdir(path, mode = FuseContext.get.mask(0o777), &dir)
  return init_node(mode) if root?(path)

  path_method(__method__, path, mode, block: dir) do |dir_name, existing|
    raise Errno::EEXIST if existing

    new_dir = dir ? dir.call(dir_name) : new_dir(dir_name)
    entry_send(new_dir, :mkdir, '/', mode)
    entries[dir_name] = new_dir
  end
end

#new_dir(_name) ⇒ FuseOperations

Method for creating a new directory, called from mkdir

Parameters:

  • _name (String)

Returns:



199
200
201
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 199

def new_dir(_name)
  VirtualDir.new(accounting: accounting)
end

#new_file(_name) ⇒ FuseOperations

Method for creating a new file

Parameters:

  • _name (String)

Returns:



173
174
175
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 173

def new_file(_name)
  VirtualFile.new(accounting: accounting)
end

Called from within ##link Uses ##getattr(from_path) to find the filesystem object at from_path. Calls ##link(nil, '/') on this object to signal that a new link has been created to it. Filesystem objects that do not support linking should raise Errno::EPERM if the object should not be hard linked (eg directories)

Returns:

Raises:

  • Errno::EXIST if there is an existing object to replace

  • Errno::EPERM if the object at from_path is not a filesystem (does not itself respond to #getattr)



237
238
239
240
241
242
243
244
245
246
247
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 237

def new_link(from_path, replacing)
  raise Errno::EEXIST if replacing

  linked_entry = getattr(from_path)

  # the linked entry itself must represent a filesystem inode
  raise Errno::EPERM unless entry_fuse_respond_to?(linked_entry, :getattr)

  entry_send(linked_entry, :link, nil, '/')
  linked_entry
end


352
353
354
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 352

def new_symlink(_name)
  VirtualLink.new(accounting: accounting)
end

#open(path, *args) ⇒ Object?

Safely passes on file open to next filesystem

Returns:

  • (Object)

    the result of #path_method for the sub filesystem

  • (nil)

    for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS

Raises:

  • (Errno::EISDIR)

    for the root path since we are a directory rather than a file



66
67
68
69
70
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 66

def open(path, *args)
  raise Errno::EISDIR if root?(path)

  path_method(__method__, path, *args, notsup: nil)
end

#opendir(path, ffi) ⇒ self, ...

Safely handles directory open to next filesystem

Returns:

  • (self)

    for the root path, which helps shortcut future operations. See #readdir

  • (Object)

    the result of #path_method for all other paths

  • (nil)

    for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS



87
88
89
90
91
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 87

def opendir(path, ffi)
  return (ffi.fh = self) if root?(path)

  path_method(__method__, path, ffi, notsup: nil)
end

#path_method(callback, *args, notsup: Errno::ENOTSUP, block: nil) {|entry_key, entry| ... } ⇒ Object

Finds the path argument of the callback and splits it into an entry in this directory and a remaining path

If a block is given and there is no remaining path (ie our entry) the block is called and its value returned

If the path is not our entry, the callback is passed on to the sub filesystem entry with the remaining path

If the path is our entry, but not block is provided, the callback is passed to our entry with a path of '/'

@yield(entry_key, entry)

Parameters:

  • callback (Symbol)

    a FUSE Callback

  • args (Array)

    callback arguments (first argument is typically 'path')

  • notsup (Errno) (defaults to: Errno::ENOTSUP)

    an error to raise if this callback is not supported by our entry

  • block (Proc) (defaults to: nil)

    optional block to keep passing down. See #mkdir, #create, #link

Yield Parameters:

  • entry_key (String, nil)

    the name of the entry in this directory or nil, if path is '/'

  • entry (FuseOperations, nil)

    the filesystem object currently stored at entry_key

Raises:

  • (Errno:ENOENT)

    if the next entry does not exist

  • (Errno::ENOTDIR)

    if the next entry must be a directory, but does not respond to :raaddir

  • (SystemCallError)

    error from notsup if the next entry does not respond to ths callback



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 376

def path_method(callback, *args, notsup: Errno::ENOTSUP, block: nil)
  # Inside path_method
  _read_arg_method, path_arg_method, next_arg_method = FuseOperations.path_arg_methods(callback)
  path = args.send(path_arg_method)

  entry_key, next_path = entry_path(path)
  our_entry = root?(next_path)

  return yield entry_key, entries[entry_key] if block_given? && our_entry

  # Pass to our entry
  args.send(next_arg_method, next_path)

  notdir = Errno::ENOTDIR unless our_entry
  entry_send(entries[entry_key], callback, *args, notsup: notsup, notdir: notdir, &block)
end

#readdir(path, buf, filler, offset, ffi, *flag) ⇒ Object

If path is root fills the directory from the keys in #entries

If ffi.fh is itself a filesystem then try to call its :readdir directly

Otherwise passes to the next filesystem in path



109
110
111
112
113
114
115
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 109

def readdir(path, buf, filler, offset, ffi, *flag)
  return %w[. ..].concat(entries.keys).each(&Adapter::Ruby::ReaddirFiller.new(buf, filler)) if root?(path)

  return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if dir_entry?(ffi.fh)

  path_method(:readdir, path, buf, filler, offset, ffi, *flag, notsup: Errno::ENOTDIR)
end

#release(path, *args) ⇒ Object

Safely handle file release

Passes on to next filesystem, rescuing ENOTSUP or ENOSYS

Raises:

  • (Errno::EISDIR)

    for the root path since we are a directory rather than a file



76
77
78
79
80
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 76

def release(path, *args)
  raise Errno::EISDIR if root?(path)

  path_method(__method__, path, *args, notsup: nil)
end

#releasedir(path, *args) ⇒ Object

Safely handles directory release

Does nothing for the root path

Otherwise safely passes on to next filesystem, rescuing ENOTSUP or ENOSYS



98
99
100
101
102
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 98

def releasedir(path, *args)
  return if root?(path)

  path_method(__method__, path, *args, notsup: nil)
end

#rename(from_path, to_path) ⇒ Object

Note:

As per POSIX raname(2) silently succeeds if from_path and to_path are hard links to the

Rename is handled via ##link and ##unlink using their respective block arguments to handle validation and retrieve the object at from_path. Intermediate directory filesystems are only required to pass on the block, while the final directory target of from_path and to_path must call these blocks as this class does.

If to_path is being replaced the existing entry will be signaled via ##unlink('/'), or ##rmdir('/') same filesystem object (ie without unlinking from_path)

Raises:

  • Errno::EINVAL if trying to rename the root object OR from_path is a directory prefix of to_path

  • Errno::ENOENT if the filesystem at from_path does not exist

  • Errno::ENOSYS if the filesystem at from_path or directory of to_path does not support rename

  • Errno::EEXIST if the filesystem at to_path already exists and is not a symlink

See Also:

  • rename(2)


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 288

def rename(from_path, to_path)
  return if from_path == to_path
  raise Errno::EINVAL if root?(from_path)

  same_filesystem_method(__method__, from_path, to_path, rescue_notsup: true) do
    # Can't rename into a subdirectory of itself
    raise Errno::EINVAL if to_path.start_with?("#{from_path}/")

    # POSIX rename(2) requires to silently abandon, without unlinking from_path,
    # if the inodes at from_path and to_path are the same object (ie hard linked to each other))
    catch :same_hard_link do
      link(nil, to_path) do |replacing|
        check_rename_unlink(from_path)
        unlink(from_path) do |source|
          raise Errno::ENOENT unless source

          throw :same_hard_link if source.equal?(replacing)
          rename_cleanup_overwritten(replacing)
        end
      end
    end
  end
end

#rmdir(path) ⇒ Object

For root path validates we are empty and removes a node link from Ruby::VirtualNode#accounting For our entries, passes on the call to the entry (with path='/') and then removes the entry.

Raises:

  • (Errno::ENOTEMPTY)

    if path is root and our entries list is not empty

  • (Errno::ENOENT)

    if the entry does not exist

  • (Errno::ENOTDIR)

    if the entry does not respond to :readdir (ie: is not a directory)



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 122

def rmdir(path)
  if root?(path)
    raise Errno::ENOTEMPTY unless entries.empty?

    accounting.adjust(0, -1)
    return
  end

  path_method(__method__, path) do |entry_key, dir|
    raise Errno::ENOENT unless dir
    raise Errno::ENOTDIR unless dir_entry?(dir)

    entry_send(dir, :rmdir, '/')

    entries.delete(entry_key)
    dir
  end
end

#same_filesystem_method(callback, from_path, to_path, rescue_notsup: false) ⇒ Object

Common between #link and #rename are callbacks that might have different semantics if called within the same sub-filesystem. While from_path and to_path have a common top level directory, we pass the callback on to the entry at that directory



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 316

def same_filesystem_method(callback, from_path, to_path, rescue_notsup: false)
  return yield unless from_path # no from_path to traverse

  to_dir, next_to_path = entry_path(to_path)
  return yield if root?(next_to_path) # target is our entry, no more directories to traverse

  from_dir, next_from_path = entry_path(from_path)
  return yield if from_dir != to_dir # from and to in different directories, we need to handle it ourself

  # try traverse into sub-fs, which must itself be a directory
  begin
    entry_send(
      entries[to_dir], callback,
      next_from_path, next_to_path,
      notsup: Errno::ENOSYS, notdir: Errno::ENOTDIR, rescue_notsup: rescue_notsup
    )
  rescue Errno::ENOSYS, Errno::ENOTSUP
    raise unless rescue_notsup

    yield
  end
end

Creates a new symbolic link in this directory

Parameters:

  • target (String)
    • an absolute path for the operating system or relative to path
  • path (String)
    • the path to create the link at


342
343
344
345
346
347
348
349
350
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 342

def symlink(target, path)
  path_method(__method__, target, path) do |link_name, existing|
    raise Errno::EEXIST if existing

    new_link = new_symlink(link_name)
    entry_send(new_link, :symlink, target, '/')
    entries[link_name] = new_link
  end
end

For our entries validates the entry exists and calls unlink('/') on it to do any cleanup before removing the entry from our entries list.

If a block is supplied (eg ##rename) it will be called before the entry is deleted

@yield(file_name, entry)

Yield Parameters:

  • entry (FuseOperations)

    a filesystem like object representing the file being unlinked

Yield Returns:

  • (void)

Returns:

  • the unlinked filesystem object

Raises:

  • (Errno:EISDIR)

    if we are unlinking ourself (use rmdir instead)

  • (Errno::ENOENT)

    if the entry does not exist at path (and no block is provided)



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/ffi/libfuse/filesystem/virtual_dir.rb', line 260

def unlink(path, &rename)
  raise Errno::EISDIR if root?(path)

  path_method(__method__, path, block: rename) do |entry_key, entry|
    if rename
      rename.call(entry)
    elsif entry
      entry_send(entry, :unlink, '/')
    else
      raise Errno::ENOENT
    end

    entries.delete(entry_key)
  end
end