ffi-libfuse 0.0.1.pre → 0.1.0.rc20220550
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/.yardopts +3 -1
- data/CHANGES.md +14 -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/gnu_extensions.rb +1 -1
- data/lib/ffi/libfuse/ackbar.rb +6 -8
- 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 +0 -1
- data/lib/ffi/libfuse/adapter/ruby.rb +499 -148
- data/lib/ffi/libfuse/adapter/safe.rb +1 -1
- 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 +188 -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 +21 -21
- data/lib/ffi/libfuse/fuse3.rb +12 -12
- data/lib/ffi/libfuse/fuse_args.rb +69 -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 +55 -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 +43 -19
- data/lib/ffi/libfuse/fuse_version.rb +10 -6
- data/lib/ffi/libfuse/main.rb +80 -37
- data/lib/ffi/libfuse/thread_pool.rb +1 -1
- data/lib/ffi/libfuse/version.rb +1 -1
- data/lib/ffi/libfuse.rb +13 -4
- data/lib/ffi/ruby_object.rb +1 -1
- data/lib/ffi/stat/constants.rb +9 -0
- data/lib/ffi/stat/native.rb +36 -6
- data/lib/ffi/stat/time_spec.rb +28 -12
- 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 +66 -7
- data/lib/ffi/libfuse/adapter/thread_local_context.rb +0 -36
@@ -48,7 +48,7 @@ module FFI
|
|
48
48
|
-e.errno
|
49
49
|
rescue StandardError, ScriptError => e
|
50
50
|
# rubocop:disable Layout/LineLength
|
51
|
-
warn "FFI::
|
51
|
+
warn ["FFI::Libfuse error in #{fuse_method}", *e.backtrace.reverse, "#{e.class.name}:#{e.message}"].join("\n\t")
|
52
52
|
# rubocop:enable Layout/LineLength
|
53
53
|
-Errno::ENOTRECOVERABLE::Errno
|
54
54
|
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 = (0o0777 & ~FuseContext.get.umask), &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
|
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
|