ffi-libfuse 0.0.1.rctest12 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +3 -1
- data/CHANGELOG.md +60 -0
- data/LICENSE +21 -0
- data/README.md +127 -44
- data/lib/ffi/accessors.rb +6 -6
- data/lib/ffi/boolean_int.rb +27 -0
- data/lib/ffi/devt.rb +23 -0
- data/lib/ffi/encoding.rb +38 -0
- data/lib/ffi/flock.rb +7 -5
- data/lib/ffi/gnu_extensions.rb +1 -1
- data/lib/ffi/libfuse/ackbar.rb +3 -3
- data/lib/ffi/libfuse/adapter/context.rb +12 -10
- data/lib/ffi/libfuse/adapter/fuse2_compat.rb +52 -51
- data/lib/ffi/libfuse/adapter/fuse3_support.rb +7 -4
- data/lib/ffi/libfuse/adapter/interrupt.rb +1 -1
- data/lib/ffi/libfuse/adapter/ruby.rb +499 -148
- data/lib/ffi/libfuse/adapter/safe.rb +12 -11
- data/lib/ffi/libfuse/adapter.rb +1 -2
- data/lib/ffi/libfuse/callbacks.rb +1 -1
- data/lib/ffi/libfuse/filesystem/accounting.rb +116 -0
- data/lib/ffi/libfuse/filesystem/mapped_dir.rb +74 -0
- data/lib/ffi/libfuse/filesystem/mapped_files.rb +141 -0
- data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +55 -0
- data/lib/ffi/libfuse/filesystem/pass_through_file.rb +45 -0
- data/lib/ffi/libfuse/filesystem/utils.rb +102 -0
- data/lib/ffi/libfuse/filesystem/virtual_dir.rb +306 -0
- data/lib/ffi/libfuse/filesystem/virtual_file.rb +94 -0
- data/lib/ffi/libfuse/filesystem/virtual_fs.rb +196 -0
- data/lib/ffi/libfuse/filesystem/virtual_node.rb +101 -0
- data/lib/ffi/libfuse/filesystem.rb +25 -0
- data/lib/ffi/libfuse/fuse2.rb +32 -24
- data/lib/ffi/libfuse/fuse3.rb +28 -18
- data/lib/ffi/libfuse/fuse_args.rb +71 -34
- data/lib/ffi/libfuse/fuse_buffer.rb +128 -26
- data/lib/ffi/libfuse/fuse_callbacks.rb +1 -5
- data/lib/ffi/libfuse/fuse_common.rb +60 -61
- data/lib/ffi/libfuse/fuse_config.rb +134 -143
- data/lib/ffi/libfuse/fuse_conn_info.rb +310 -134
- data/lib/ffi/libfuse/fuse_context.rb +45 -3
- data/lib/ffi/libfuse/fuse_operations.rb +57 -21
- data/lib/ffi/libfuse/fuse_opt.rb +1 -1
- data/lib/ffi/libfuse/fuse_version.rb +10 -6
- data/lib/ffi/libfuse/gem_version.rb +54 -0
- data/lib/ffi/libfuse/main.rb +96 -48
- data/lib/ffi/libfuse/test_helper.rb +145 -0
- data/lib/ffi/libfuse/version.rb +1 -1
- data/lib/ffi/libfuse.rb +13 -4
- data/lib/ffi/ruby_object.rb +4 -1
- data/lib/ffi/stat/constants.rb +9 -0
- data/lib/ffi/stat/native.rb +36 -6
- data/lib/ffi/stat/time_spec.rb +26 -10
- data/lib/ffi/stat.rb +111 -22
- data/lib/ffi/stat_vfs.rb +59 -1
- data/lib/ffi/struct_wrapper.rb +22 -1
- data/sample/hello_fs.rb +54 -0
- data/sample/memory_fs.rb +5 -181
- data/sample/no_fs.rb +20 -21
- data/sample/pass_through_fs.rb +30 -0
- metadata +83 -10
- data/lib/ffi/libfuse/adapter/thread_local_context.rb +0 -36
- data/lib/ffi/libfuse/test/operations.rb +0 -56
- data/lib/ffi/libfuse/test.rb +0 -3
@@ -10,7 +10,7 @@ module FFI
|
|
10
10
|
# @!visibility private
|
11
11
|
def fuse_wrappers(*wrappers)
|
12
12
|
wrappers << {
|
13
|
-
wrapper: proc { |fm, *args, **_, &b| Safe.safe_callback(fm, *args, &b) },
|
13
|
+
wrapper: proc { |fm, *args, **_, &b| Safe.safe_callback(fm, *args, default_errno: default_errno, &b) },
|
14
14
|
excludes: %i[init destroy]
|
15
15
|
}
|
16
16
|
return wrappers unless defined?(super)
|
@@ -18,8 +18,10 @@ module FFI
|
|
18
18
|
super(*wrappers)
|
19
19
|
end
|
20
20
|
|
21
|
-
#
|
22
|
-
|
21
|
+
# @return [Integer] the default errno. ENOTRECOVERABLE unless overridden
|
22
|
+
def default_errno
|
23
|
+
defined?(super) ? super : Errno::ENOTRECOVERABLE::Errno
|
24
|
+
end
|
23
25
|
|
24
26
|
module_function
|
25
27
|
|
@@ -27,30 +29,29 @@ module FFI
|
|
27
29
|
#
|
28
30
|
# @yieldreturn [SystemCallError] expected callback errors rescued to return equivalent -ve errno value
|
29
31
|
# @yieldreturn [StandardError,ScriptError] unexpected callback errors are rescued
|
30
|
-
# to return -
|
32
|
+
# to return -ve {default_errno} after emitting backtrace to #warn
|
31
33
|
#
|
32
34
|
# @yieldreturn [Integer]
|
33
35
|
#
|
34
36
|
# * -ve values returned directly
|
35
|
-
# * +ve values returned directly for fuse_methods in {MEANINGFUL_RETURN} list
|
37
|
+
# * +ve values returned directly for fuse_methods in {FuseOperations.MEANINGFUL_RETURN} list
|
36
38
|
# * otherwise returns 0
|
37
39
|
#
|
38
40
|
# @yieldreturn [Object] always returns 0 if no exception is raised
|
39
41
|
#
|
40
|
-
def safe_callback(fuse_method, *args)
|
42
|
+
def safe_callback(fuse_method, *args, default_errno: Errno::ENOTRECOVERABLE::Errno)
|
41
43
|
result = yield(*args)
|
42
44
|
|
43
|
-
return
|
44
|
-
return 0 unless result.negative? || MEANINGFUL_RETURN.include?(fuse_method)
|
45
|
+
return result.to_i if FuseOperations.meaningful_return?(fuse_method)
|
45
46
|
|
46
|
-
|
47
|
+
0
|
47
48
|
rescue SystemCallError => e
|
48
49
|
-e.errno
|
49
50
|
rescue StandardError, ScriptError => e
|
50
51
|
# rubocop:disable Layout/LineLength
|
51
|
-
warn "FFI::
|
52
|
+
warn ["FFI::Libfuse error in #{fuse_method}", *e.backtrace.reverse, "#{e.class.name}:#{e.message}"].join("\n\t")
|
52
53
|
# rubocop:enable Layout/LineLength
|
53
|
-
-
|
54
|
+
-default_errno.abs
|
54
55
|
end
|
55
56
|
end
|
56
57
|
end
|
data/lib/ffi/libfuse/adapter.rb
CHANGED
@@ -11,7 +11,7 @@ module FFI
|
|
11
11
|
#
|
12
12
|
# These will implement {FuseOperations#fuse_wrappers} to add the proc which can then...
|
13
13
|
#
|
14
|
-
# *
|
14
|
+
# * populate thread local information - eg. ({Context})
|
15
15
|
# * wrap common arguments - eg. ({Pathname})
|
16
16
|
# * handle return values/exceptions - eg. ({Safe})
|
17
17
|
# * or just wrap the underlying block - eg. ({Debug})
|
@@ -70,7 +70,6 @@ module FFI
|
|
70
70
|
end
|
71
71
|
|
72
72
|
require_relative 'adapter/context'
|
73
|
-
require_relative 'adapter/thread_local_context'
|
74
73
|
require_relative 'adapter/debug'
|
75
74
|
require_relative 'adapter/ruby'
|
76
75
|
require_relative 'adapter/interrupt'
|
@@ -37,7 +37,7 @@ module FFI
|
|
37
37
|
|
38
38
|
def initialize_callbacks(callbacks, delegate:, wrappers: [])
|
39
39
|
callbacks.select { |m| respond_to_callback?(m, delegate) }.each do |m|
|
40
|
-
register(m, wrappers) { |*f_args| delegate.
|
40
|
+
register(m, wrappers) { |*f_args| delegate.public_send(m, *f_args) }
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../stat_vfs'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
module Filesystem
|
8
|
+
# Helper for filesystem accounting
|
9
|
+
class Accounting
|
10
|
+
OPTIONS = { 'max_space=' => :max_space, 'max_nodes=' => :max_nodes }.freeze
|
11
|
+
|
12
|
+
HELP =
|
13
|
+
<<~END_HELP
|
14
|
+
#{name} options:
|
15
|
+
-o max_space=<int> maximum space consumed by files, --ve will always show free space
|
16
|
+
-o max_nodes=<int> maximum number of files and directories, -ve will always show free nodes
|
17
|
+
|
18
|
+
END_HELP
|
19
|
+
|
20
|
+
def fuse_opt_proc(key:, value:, **)
|
21
|
+
return :keep unless OPTIONS.values.include?(key)
|
22
|
+
|
23
|
+
public_send("#{key}=", value.to_i)
|
24
|
+
:handled
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Integer] maximum allowed space in bytes
|
28
|
+
#
|
29
|
+
# Positive values will limit values in {adjust} to stay below this value
|
30
|
+
#
|
31
|
+
# Negative or zero will simply report this amount of space as 'free' in {to_statvfs}
|
32
|
+
attr_accessor :max_space
|
33
|
+
|
34
|
+
# @return [Integer] maximum number of (virtual) inodes
|
35
|
+
#
|
36
|
+
# Positive values will limit {adjust} to stay below this value
|
37
|
+
#
|
38
|
+
# Negative or zero will simply report this number of inodes as 'free' in {to_statvfs}
|
39
|
+
attr_accessor :max_nodes
|
40
|
+
|
41
|
+
# @return [Integer] accumulated space in bytes
|
42
|
+
attr_reader :space
|
43
|
+
|
44
|
+
# @return [Integer] accumulated inodes (typically count of files and directories)
|
45
|
+
attr_reader :nodes
|
46
|
+
|
47
|
+
# @return [Integer] block size for statvfs
|
48
|
+
attr_accessor :block_size
|
49
|
+
|
50
|
+
def initialize(max_space: 0, max_nodes: 0, block_size: 1024)
|
51
|
+
@nodes = 0
|
52
|
+
@space = 0
|
53
|
+
@max_space = max_space
|
54
|
+
@max_nodes = max_nodes
|
55
|
+
@block_size = block_size
|
56
|
+
end
|
57
|
+
|
58
|
+
# Adjust accumlated statistics
|
59
|
+
# @param [Integer] delta_space change in {#space} usage
|
60
|
+
# @param [Integer] delta_nodes change in {#nodes} usage
|
61
|
+
# @return [self]
|
62
|
+
# @raise [Errno::ENOSPC] if adjustment {#space}/{#nodes} would exceed {#max_space} or {#max_nodes}
|
63
|
+
def adjust(delta_space, delta_nodes = 0, strict: true)
|
64
|
+
strict_space = strict && delta_space.positive? && max_space.positive?
|
65
|
+
raise Errno::ENOSPC if strict_space && space + delta_space > max_space
|
66
|
+
|
67
|
+
strict_nodes = strict && delta_nodes.positive? && max_nodes.positive?
|
68
|
+
raise Errno::ENOSPC if strict_nodes && nodes + delta_nodes > max_nodes
|
69
|
+
|
70
|
+
@nodes += delta_nodes
|
71
|
+
@space += delta_space
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Adjust for incremental write
|
76
|
+
# @param [Integer] current_size
|
77
|
+
# @param [Integer] data_size size of new data
|
78
|
+
# @param [Integer] offset offset of new data
|
79
|
+
# @return [self]
|
80
|
+
def write(current_size, data_size, offset, strict: true)
|
81
|
+
adjust(offset + data_size - current_size, strict: strict) if current_size < offset + data_size
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
# Adjust for truncate
|
86
|
+
# @param [Integer] current_size
|
87
|
+
# @param [Integer] new_size the size being truncated to
|
88
|
+
# @return [self]
|
89
|
+
def truncate(current_size, new_size)
|
90
|
+
adjust(new_size - current_size, strict: false) if new_size < current_size
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
# rubocop:disable Metrics/AbcSize
|
95
|
+
|
96
|
+
# @param [FFI::StatVfs] statvfs an existing statvfs buffer to fill
|
97
|
+
# @param [Integer] block_size
|
98
|
+
# @return [FFI::StatVfs] the filesystem statistics
|
99
|
+
def to_statvfs(statvfs = FFI::StatVfs.new, block_size: self.block_size || 1024)
|
100
|
+
used_blocks, max_blocks = [space, max_space].map { |s| s / block_size }
|
101
|
+
max_blocks = used_blocks - max_blocks unless max_blocks.positive?
|
102
|
+
max_files = max_nodes.positive? ? max_nodes : nodes - max_nodes
|
103
|
+
statvfs.bsize = block_size # block size (in Kb)
|
104
|
+
statvfs.frsize = block_size # fragment size pretty much always bsize
|
105
|
+
statvfs.blocks = max_blocks
|
106
|
+
statvfs.bfree = max_blocks - used_blocks
|
107
|
+
statvfs.bavail = max_blocks - used_blocks
|
108
|
+
statvfs.files = max_files
|
109
|
+
statvfs.ffree = max_files - nodes
|
110
|
+
statvfs
|
111
|
+
end
|
112
|
+
# rubocop:enable Metrics/AbcSize
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'mapped_files'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
module Filesystem
|
8
|
+
# A read-only directory of {MappedFiles}
|
9
|
+
#
|
10
|
+
# Subclasses must implement {#entries} and {#map_path}
|
11
|
+
class MappedDir
|
12
|
+
include MappedFiles
|
13
|
+
include Utils
|
14
|
+
attr_accessor :stat
|
15
|
+
|
16
|
+
# @!method entries
|
17
|
+
# @abstract
|
18
|
+
# @return [Enumerable] set of entries in this directory (excluding '.' and '..')
|
19
|
+
|
20
|
+
def initialize(accounting: nil)
|
21
|
+
@accounting = accounting
|
22
|
+
@root = VirtualNode.new(accounting: accounting)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!group Fuse Callbacks
|
26
|
+
|
27
|
+
# For the root path provides this directory's stat information, otherwise passes on to the next filesystem
|
28
|
+
def getattr(path, stat = nil, _ffi = nil)
|
29
|
+
return super unless root?(path)
|
30
|
+
|
31
|
+
stat&.directory(@root.virtual_stat.merge({ nlink: entries.size + 2 }))
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# For root path enumerates {#entries}
|
37
|
+
# @raise [Errno::ENOTDIR] unless root path
|
38
|
+
def readdir(path, *_args, &block)
|
39
|
+
raise Errno::ENOTDIR unless root?(path)
|
40
|
+
|
41
|
+
%w[. ..].concat(entries).each(&block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def mkdir(path, mode, *_args)
|
45
|
+
raise Errno::EROFS unless root?(path)
|
46
|
+
|
47
|
+
@root.init_node(mode)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @!endgroup
|
51
|
+
|
52
|
+
# Passes FUSE Callbacks on to the {#root} filesystem
|
53
|
+
def method_missing(method, path = nil, *args, &block)
|
54
|
+
return @root.public_send(method, path, *args, &block) if @root.respond_to?(method) && root?(path)
|
55
|
+
|
56
|
+
raise Errno::ENOTSUP if FuseOperations.path_callbacks.include?(method)
|
57
|
+
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond_to_missing?(method, private = false)
|
62
|
+
(FuseOperations.fuse_callbacks.include?(method) && @root.respond_to?(method, false)) || super
|
63
|
+
end
|
64
|
+
|
65
|
+
# subclass only call super for root path
|
66
|
+
def map_path(path)
|
67
|
+
raise ArgumentError, "map_path received non root path #{path}" unless root?(path)
|
68
|
+
|
69
|
+
[path, @root]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../adapter'
|
4
|
+
require_relative 'accounting'
|
5
|
+
|
6
|
+
module FFI
|
7
|
+
module Libfuse
|
8
|
+
module Filesystem
|
9
|
+
# An abstract filesystem mapping paths to either real files or an alternate filesystem based on outcome of
|
10
|
+
# {#map_path} as implemented by including class
|
11
|
+
#
|
12
|
+
# Real files permissions are made read-only by default. Including classes can override {#stat_mask} to
|
13
|
+
# change this behaviour
|
14
|
+
#
|
15
|
+
# Implements callbacks satisfying {Adapter::Ruby} which is automatically included.
|
16
|
+
module MappedFiles
|
17
|
+
# Do we have ffi-xattr to handle extended attributes in real files
|
18
|
+
HAS_XATTR =
|
19
|
+
begin
|
20
|
+
require 'ffi-xattr'
|
21
|
+
true
|
22
|
+
rescue LoadError
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Accounting|nil]
|
27
|
+
# if set the accounting object will be used to provide {#statfs} for the root path
|
28
|
+
# @note the real LIBC statvfs is always used for non-root paths
|
29
|
+
attr_accessor :accounting
|
30
|
+
|
31
|
+
# @!method map_path(path)
|
32
|
+
# @abstract
|
33
|
+
# @param [String] path the path in the fuse filesystem
|
34
|
+
# @return [String] mapped_path in an underlying filesystem
|
35
|
+
#
|
36
|
+
# Fuse callbacks are fulfilled using Ruby's native File methods called on this path
|
37
|
+
# @return [String, Adapter::Ruby::Prepend] mapped_path, filesystem
|
38
|
+
#
|
39
|
+
# If an optional filesystem value is returned fuse callbacks will be passed on to this filesystem with the
|
40
|
+
# mapped_path and other callback args unchanged
|
41
|
+
# @return [nil]
|
42
|
+
#
|
43
|
+
# eg on create to indicate the path does not exist
|
44
|
+
|
45
|
+
# Manipulate file attributes
|
46
|
+
#
|
47
|
+
# Default implementation forces read-only permissions
|
48
|
+
# @overload stat_mask(path,stat)
|
49
|
+
# @param [String] path the path received by {#getattr}
|
50
|
+
# @param [FFI::Stat] stat loaded from the mapped file, can be filled, mapped as necessary
|
51
|
+
# @return [FFI::Stat] stat
|
52
|
+
def stat_mask(_path, stat)
|
53
|
+
stat.mask(0o0222)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @!group FUSE Callbacks
|
57
|
+
|
58
|
+
# Pass to real stat and then {#stat_mask}
|
59
|
+
def getattr(path, stat, ffi = nil)
|
60
|
+
if (fd = ffi&.fh&.fileno)
|
61
|
+
stat.fstat(fd)
|
62
|
+
else
|
63
|
+
path_method(__method__, path, stat, ffi) { |rp| stat.stat(rp) }
|
64
|
+
end
|
65
|
+
|
66
|
+
stat_mask(path, stat)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create real file - assuming the path can be mapped before it exists
|
70
|
+
def create(path, perms, ffi)
|
71
|
+
path_method(__method__, path, perms, ffi, error: Errno::EROFS) do |rp|
|
72
|
+
File.open(rp, ffi.flags, perms)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [File] the newly opened file at {#map_path}(path)
|
77
|
+
def open(path, ffi)
|
78
|
+
path_method(__method__, path, ffi) { |rp| File.open(rp, ffi.flags) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Truncates the file handle (or the real file)
|
82
|
+
def truncate(path, size, ffi = nil)
|
83
|
+
return ffi.fh.truncate(size) if ffi&.fh
|
84
|
+
|
85
|
+
path_method(__method__, path, size, ffi) { |rp| File.truncate(rp, size) }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Delete the real file
|
89
|
+
def unlink(path)
|
90
|
+
path_method(__method__, path) { |rp| File.unlink(rp) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# Calls File.utime on an Integer file handle or the real file
|
94
|
+
def utimens(_path, atime, mtime, ffi = nil)
|
95
|
+
return File.utime(atime, mtime, ffi.fh) if ffi&.fh.is_a?(Integer)
|
96
|
+
|
97
|
+
path_method(__method__, atime, mtime, ffi) { |rp| File.utime(atime, mtime, rp) }
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [String] the value of the extended attribute name from the real file
|
101
|
+
def getxattr(path, name)
|
102
|
+
return nil unless HAS_XATTR
|
103
|
+
|
104
|
+
path_method(__method__, path, name) { |rp| Xattr.new(rp)[name] }
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Array<String>] the list of extended attributes from the real file
|
108
|
+
def listxattr(path)
|
109
|
+
return [] unless HAS_XATTR
|
110
|
+
|
111
|
+
path_method(__method__, path) { |rp| Xattr.new(rp).list }
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO: Set xattr
|
115
|
+
# TODO: chmod, change the stat[:mode]
|
116
|
+
# TODO: chown, change the stat[:uid,:gid]
|
117
|
+
|
118
|
+
def statfs(path, statvfs)
|
119
|
+
return accounting&.to_statvfs(statvfs) if root?(path)
|
120
|
+
|
121
|
+
path_method(__method__, path, statvfs) { |rp| statvfs.from(rp) }
|
122
|
+
end
|
123
|
+
# @!endgroup
|
124
|
+
|
125
|
+
# @!visibility private
|
126
|
+
def self.included(mod)
|
127
|
+
mod.prepend(Adapter::Ruby::Prepend)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def path_method(callback, path, *args, error: Errno::ENOENT, block: nil)
|
133
|
+
rp, fs = map_path(path)
|
134
|
+
raise error if error && !rp
|
135
|
+
|
136
|
+
fs ? fs.send(callback, rp, *args, &block) : yield(rp)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'mapped_files'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
module Filesystem
|
8
|
+
# A Filesystem that maps paths to an underlying directory
|
9
|
+
class PassThroughDir
|
10
|
+
include MappedFiles
|
11
|
+
include Adapter::Debug
|
12
|
+
include Adapter::Safe
|
13
|
+
include Utils
|
14
|
+
|
15
|
+
# @return [String] The base directory
|
16
|
+
attr_accessor :base_dir
|
17
|
+
|
18
|
+
# @!group FUSE Callbacks
|
19
|
+
|
20
|
+
# @return [Dir] the directory at {#map_path}(path)
|
21
|
+
def opendir(path, _ffi)
|
22
|
+
Dir.new(map_path(path))
|
23
|
+
end
|
24
|
+
|
25
|
+
# Removes the directory at {#map_path}(path)
|
26
|
+
def rmdir(path)
|
27
|
+
return Dir.rmdir(map_path(path)) unless root?(path)
|
28
|
+
|
29
|
+
accounting&.adjust(0, -1) if root?(path)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates the directory at {#map_path}(path)
|
34
|
+
def mkdir(path, mode)
|
35
|
+
return Dir.mkdir(map_path(path), mode) unless root?(path)
|
36
|
+
|
37
|
+
accounting&.adjust(0, +1)
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
# Creates the File at {#map_path}(path)
|
42
|
+
def create(path, perms = 0o644, ffi = nil)
|
43
|
+
File.open(map_path(path), ffi&.flags, perms)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @!endgroup
|
47
|
+
|
48
|
+
# @return [String] {#base_dir} + path
|
49
|
+
def map_path(path)
|
50
|
+
root?(path) ? @base_dir : "#{@base_dir}#{path}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'accounting'
|
4
|
+
require_relative 'mapped_files'
|
5
|
+
|
6
|
+
module FFI
|
7
|
+
module Libfuse
|
8
|
+
module Filesystem
|
9
|
+
# Represents a single regular file at a given underlying path
|
10
|
+
class PassThroughFile
|
11
|
+
include MappedFiles
|
12
|
+
|
13
|
+
# @param [String] real_path
|
14
|
+
def initialize(real_path)
|
15
|
+
@real_path = real_path
|
16
|
+
end
|
17
|
+
|
18
|
+
# @!visibility private
|
19
|
+
def map_path(path)
|
20
|
+
raise Errno::ENOENT unless root?(path)
|
21
|
+
|
22
|
+
@real_path
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!group FUSE Callbacks
|
26
|
+
|
27
|
+
# Adjust accounting to add a node
|
28
|
+
def create(path, perms, ffi)
|
29
|
+
raise Errno::ENOENT unless root?(path)
|
30
|
+
|
31
|
+
accounting&.adjust(0, +1)
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adjust accounting to remove a node
|
36
|
+
def unlink(path)
|
37
|
+
raise Errno::ENOENT unless root?(path)
|
38
|
+
|
39
|
+
accounting&.adjust(0, -1)
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../fuse_context'
|
4
|
+
|
5
|
+
module FFI
|
6
|
+
module Libfuse
|
7
|
+
module Filesystem
|
8
|
+
# RubySpace File Utilities
|
9
|
+
#
|
10
|
+
# This module provides utility methods for file operations in RubySpace which are useful for creating custom
|
11
|
+
# entries prior to mounting, or otherwise manipulating the filesystem from within the Ruby process that is
|
12
|
+
# running FUSE.
|
13
|
+
#
|
14
|
+
# **Note** You cannot generally call Ruby's {::File} and {::Dir} operations from within the same Ruby process
|
15
|
+
# as the mounted filesystem because MRI will not release the GVL to allow the Fuse callbacks to run.
|
16
|
+
module Utils
|
17
|
+
# Recursive mkdir
|
18
|
+
# @param [:to_s] path
|
19
|
+
# @param [Integer] mode permissions for any dirs that need to be created
|
20
|
+
# @yieldparam [String] the path component being created
|
21
|
+
# @yieldreturn [FuseOperations] optionally a filesystem to mount at path, if the path did not previously exist
|
22
|
+
def mkdir_p(path, mode = (~FuseContext.get.umask & 0o0777), &mount_fs)
|
23
|
+
return if root?(path) # nothing to make
|
24
|
+
|
25
|
+
path.to_s.split('/')[1..].inject('') do |base_path, sub_dir|
|
26
|
+
full_path = "#{base_path}/#{sub_dir}"
|
27
|
+
err = Adapter::Safe.safe_callback(:mkdir) { mkdir(full_path, mode, &mount_fs) }
|
28
|
+
unless [0, -Errno::EEXIST::Errno].include?(err)
|
29
|
+
raise SystemCallError.new("Unexpected err #{err.abs} from mkdir #{full_path}", err.abs)
|
30
|
+
end
|
31
|
+
|
32
|
+
full_path
|
33
|
+
end
|
34
|
+
0
|
35
|
+
end
|
36
|
+
alias mkpath mkdir_p
|
37
|
+
|
38
|
+
# @param [:to_s] path
|
39
|
+
# @return [FFI::Stat]
|
40
|
+
def stat(path)
|
41
|
+
path = path.to_s
|
42
|
+
stat_buf = FFI::Stat.new
|
43
|
+
|
44
|
+
err = Adapter::Safe.safe_callback(:getattr) { getattr(path, stat_buf) }
|
45
|
+
|
46
|
+
return nil if err == -Errno::ENOENT::Errno
|
47
|
+
raise SystemCallError, "Unexpected error from #{fs}.getattr #{path}", err unless err.zero?
|
48
|
+
|
49
|
+
stat_buf
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [:to_s] path
|
53
|
+
# @return [Boolean] true if file or directory exists at path
|
54
|
+
def exists?(path)
|
55
|
+
stat(path) && true
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [:to_s] path
|
59
|
+
# @return [Boolean] true if regular file exists at path
|
60
|
+
def file?(path)
|
61
|
+
stat(path)&.file? || false
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param [:to_s] path
|
65
|
+
# @return [Boolean] true if directory exists at path
|
66
|
+
def directory?(path)
|
67
|
+
stat(path)&.directory? || false
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param [:to_s] path
|
71
|
+
# @return [Boolean] File exists at path and has zero size
|
72
|
+
def empty_file?(path)
|
73
|
+
s = stat(path)
|
74
|
+
(s&.file? && s.size.zero?) || false # rubocop:disable Style/ZeroLengthPredicate
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check if a directory is empty
|
78
|
+
# @param [String] path
|
79
|
+
# @return [Boolean] true if an empty directory exists at path
|
80
|
+
# @raise [Errno::ENOTDIR] if path does not point to a directory
|
81
|
+
def empty_dir?(path)
|
82
|
+
return false unless directory?(path)
|
83
|
+
|
84
|
+
empty = true
|
85
|
+
fake_filler = proc do |_buf, name, _stat = nil, _offset = 0, _fuse_flag = 0|
|
86
|
+
next 0 if %w[. ..].include?(name)
|
87
|
+
|
88
|
+
empty = false
|
89
|
+
-1 # buf full don't send more entries!
|
90
|
+
end
|
91
|
+
readdir(path.to_s, nil, fake_filler, 0, nil, *(fuse3_compat? ? [] : [0]))
|
92
|
+
empty
|
93
|
+
end
|
94
|
+
|
95
|
+
# @!visibility private
|
96
|
+
def fuse3_compat?
|
97
|
+
FUSE_MAJOR_VERSION >= 3
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|