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
@@ -0,0 +1,306 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'accounting'
|
4
|
+
require_relative 'virtual_node'
|
5
|
+
require_relative 'virtual_file'
|
6
|
+
require_relative 'pass_through_file'
|
7
|
+
require_relative 'pass_through_dir'
|
8
|
+
require_relative 'mapped_dir'
|
9
|
+
|
10
|
+
module FFI
|
11
|
+
module Libfuse
|
12
|
+
module Filesystem
|
13
|
+
# A Filesystem of Filesystems
|
14
|
+
#
|
15
|
+
# Implements a recursive Hash based directory of sub filesystems.
|
16
|
+
#
|
17
|
+
# FUSE Callbacks
|
18
|
+
# ===
|
19
|
+
#
|
20
|
+
# If path is root ('/') then the operation applies to this directory itself
|
21
|
+
#
|
22
|
+
# If the path is a simple basename (with leading slash and no others) then the operation applies to an entry in
|
23
|
+
# this directory. The operation is handled by the directory and then passed on to the entry itself
|
24
|
+
# (with path = '/')
|
25
|
+
#
|
26
|
+
# Otherwise it is passed on to the next entry via {#path_method}
|
27
|
+
#
|
28
|
+
# Constraints
|
29
|
+
# ===
|
30
|
+
#
|
31
|
+
# * Expects to be wrapped by {Adapter::Safe}
|
32
|
+
# * Passes on FUSE Callbacks to sub filesystems agnostic of {FUSE_MAJOR_VERSION}. Sub-filesystems should use
|
33
|
+
# {Adapter::Fuse2Compat} or {Adapter::Fuse3Support} as required
|
34
|
+
#
|
35
|
+
class VirtualDir < VirtualNode
|
36
|
+
include Utils
|
37
|
+
|
38
|
+
# @return [Hash<String,FuseOperations>] our directory entries
|
39
|
+
attr_reader :entries
|
40
|
+
|
41
|
+
def initialize(accounting: Accounting.new)
|
42
|
+
@entries = {}
|
43
|
+
@mounted = false
|
44
|
+
super(accounting: accounting)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Boolean] true if this dir been mounted
|
48
|
+
def mounted?
|
49
|
+
@mounted
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!endgroup
|
53
|
+
|
54
|
+
# @!group FUSE Callbacks
|
55
|
+
|
56
|
+
# For the root path provides this directory's stat information, otherwise passes on to the next filesystem
|
57
|
+
def getattr(path, stat_buf = nil, _ffi = nil)
|
58
|
+
return path_method(__method__, path, stat_buf) unless root?(path)
|
59
|
+
|
60
|
+
stat_buf&.directory(**virtual_stat.merge({ nlink: entries.size + 2 }))
|
61
|
+
end
|
62
|
+
|
63
|
+
# Safely passes on file open to next filesystem
|
64
|
+
#
|
65
|
+
# @raise [Errno::EISDIR] for the root path since we are a directory rather than a file
|
66
|
+
# @return [Object] the result of {#path_method} for the sub filesystem
|
67
|
+
# @return [nil] for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS
|
68
|
+
def open(path, *args)
|
69
|
+
raise Errno::EISDIR if root?(path)
|
70
|
+
|
71
|
+
path_method(__method__, path, *args, notsup: nil)
|
72
|
+
rescue Errno::ENOTSUP, Errno::ENOSYS
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Safely handle file release
|
77
|
+
#
|
78
|
+
# Passes on to next filesystem, rescuing ENOTSUP or ENOSYS
|
79
|
+
# @raise [Errno::EISDIR] for the root path since we are a directory rather than a file
|
80
|
+
def release(path, *args)
|
81
|
+
raise Errno::EISDIR if root?(path)
|
82
|
+
|
83
|
+
path_method(__method__, path, *args, notsup: nil)
|
84
|
+
rescue Errno::ENOTSUP, Errno::ENOSYS
|
85
|
+
# do nothing
|
86
|
+
end
|
87
|
+
|
88
|
+
# Safely handles directory open to next filesystem
|
89
|
+
#
|
90
|
+
# @return [self] for the root path, which helps shortcut future operations. See {#readdir}
|
91
|
+
# @return [Object] the result of {#path_method} for all other paths
|
92
|
+
# @return [nil] for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS
|
93
|
+
def opendir(path, ffi)
|
94
|
+
return path_method(__method__, path, ffi, notsup: nil) unless root?(path)
|
95
|
+
|
96
|
+
ffi.fh = self
|
97
|
+
rescue Errno::ENOTSUP, Errno::ENOSYS
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Safely handles directory release
|
102
|
+
#
|
103
|
+
# Does nothing for the root path
|
104
|
+
#
|
105
|
+
# Otherwise safely passes on to next filesystem, rescuing ENOTSUP or ENOSYS
|
106
|
+
def releasedir(path, *args)
|
107
|
+
path_method(__method__, path, *args, notsup: nil) unless root?(path)
|
108
|
+
rescue Errno::ENOTSUP, Errno::ENOSYS
|
109
|
+
# do nothing
|
110
|
+
end
|
111
|
+
|
112
|
+
# If path is root fills the directory from the keys in {#entries}
|
113
|
+
#
|
114
|
+
# If ffi.fh is itself a filesystem then try to call its :readdir directly
|
115
|
+
#
|
116
|
+
# Otherwise passes to the next filesystem in path
|
117
|
+
def readdir(path, buf, filler, offset, ffi, *flag)
|
118
|
+
return %w[. ..].concat(entries.keys).each(&Adapter::Ruby::ReaddirFiller.new(buf, filler)) if root?(path)
|
119
|
+
|
120
|
+
return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if entry_fuse_respond_to?(ffi.fh, :readdir)
|
121
|
+
|
122
|
+
path_method(:readdir, path, buf, filler, offset, ffi, *flag) unless root?(path)
|
123
|
+
end
|
124
|
+
|
125
|
+
# For root path validates we are empty and removes a node link from {#accounting}
|
126
|
+
# For our entries, passes on the call to the entry (with path='/') and then removes the entry. If available
|
127
|
+
# :destroy will be called on the deleted entry
|
128
|
+
# @raise [Errno::ENOTEMPTY] if path is root and our entries list is not empty
|
129
|
+
# @raise [Errno::ENOENT] if the entry does not exist
|
130
|
+
# @raise [Errno::ENOTDIR] if the entry does not respond to :readdir (ie: is not a directory)
|
131
|
+
def rmdir(path)
|
132
|
+
if root?(path)
|
133
|
+
raise Errno::ENOTEMPTY unless entries.empty?
|
134
|
+
|
135
|
+
accounting.adjust(0, -1)
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
entry_key = entry_key(path)
|
140
|
+
return path_method(__method__, path) unless entry_key
|
141
|
+
|
142
|
+
dir = entries[entry_key]
|
143
|
+
raise Errno::ENOENT unless dir
|
144
|
+
raise Errno::ENOTDIR unless entry_fuse_respond_to?(dir, :readdir)
|
145
|
+
|
146
|
+
entry_send(dir, :rmdir, '/')
|
147
|
+
|
148
|
+
dir = entries.delete(entry_key)
|
149
|
+
entry_send(dir, :destroy, init_results.delete(entry_key)) if dir && mounted?
|
150
|
+
end
|
151
|
+
|
152
|
+
# For our entries, validates the entry exists and is not a directory, then passes on unlink (with path = '/')
|
153
|
+
# and finally deletes.
|
154
|
+
# @raise [Errno:EISDIR] if the request entry responds to :readdir
|
155
|
+
def unlink(path)
|
156
|
+
entry_key = entry_key(path)
|
157
|
+
return path_method(__method__, path) unless entry_key
|
158
|
+
|
159
|
+
entry = entries[entry_key]
|
160
|
+
raise Errno::ENOENT unless entry
|
161
|
+
raise Errno::EISDIR if entry_fuse_respond_to?(entry, :readdir)
|
162
|
+
|
163
|
+
entry_send(entry, :unlink, '/')
|
164
|
+
entries.delete(entry_key) && true
|
165
|
+
end
|
166
|
+
|
167
|
+
# For our entries, creates a new file
|
168
|
+
# @raise [Errno::EISDIR] if the entry exists and responds_to?(:readdir)
|
169
|
+
# @raise [Errno::EEXIST] if the entry exists
|
170
|
+
# @yield []
|
171
|
+
# @yieldreturn [Object] something that quacks with the FUSE Callbacks of a regular file
|
172
|
+
#
|
173
|
+
# :create or :mknod + :open will be attempted with path = '/' on this file
|
174
|
+
# @return [Object] the result of the supplied block, or if not given a new {VirtualFile}
|
175
|
+
def create(path, mode = FuseContext.get.mask(0o644), ffi = nil, &file)
|
176
|
+
file_name = entry_key(path)
|
177
|
+
|
178
|
+
# fuselib will fallback to mknod on ENOSYS on a case by case basis
|
179
|
+
return path_method(__method__, path, mode, ffi, notsup: Errno::ENOSYS, &file) unless file_name
|
180
|
+
|
181
|
+
existing = entries[file_name]
|
182
|
+
raise Errno::EISDIR if entry_fuse_respond_to?(existing, :readdir)
|
183
|
+
raise Errno::EEXIST if existing
|
184
|
+
|
185
|
+
# TODO: Strictly should understand setgid and sticky bits of this dir's mode when creating new files
|
186
|
+
new_file = file ? file.call(name) : VirtualFile.new(accounting: accounting)
|
187
|
+
if entry_fuse_respond_to?(new_file, :create)
|
188
|
+
new_file.public_send(:create, '/', mode, ffi)
|
189
|
+
else
|
190
|
+
# TODO: generate a sensible device number
|
191
|
+
entry_send(new_file, :mknod, '/', mode, 0)
|
192
|
+
entry_send(new_file, :open, '/', ffi)
|
193
|
+
end
|
194
|
+
entries[file_name] = new_file
|
195
|
+
end
|
196
|
+
|
197
|
+
# Creates a new directory entry in this directory
|
198
|
+
# @param [String] path
|
199
|
+
# @param [Integer] mode
|
200
|
+
# @yield []
|
201
|
+
# @yieldreturn [Object] something that quacks with the FUSE Callbacks representing a directory
|
202
|
+
# @return [Object] the result of the block if given, otherwise the newly created sub {VirtualDir}
|
203
|
+
# @raise [Errno::EEXIST] if the entry already exists at path
|
204
|
+
def mkdir(path, mode = FuseContext.get.mask(0o777), &dir)
|
205
|
+
return init_node(mode) if root?(path)
|
206
|
+
|
207
|
+
dir_name = entry_key(path)
|
208
|
+
return path_method(__method__, path, mode, &dir) unless dir_name
|
209
|
+
|
210
|
+
existing = entries[dir_name]
|
211
|
+
raise Errno::EEXIST if existing
|
212
|
+
|
213
|
+
new_dir = dir ? dir.call : VirtualDir.new(accounting: accounting)
|
214
|
+
init_dir(dir_name, new_dir) if mounted?
|
215
|
+
entry_send(new_dir, :mkdir, '/', mode)
|
216
|
+
entries[dir_name] = new_dir
|
217
|
+
end
|
218
|
+
|
219
|
+
# Calls init on all current entries, keeping track of their init objects for use with {destroy}
|
220
|
+
def init(*args)
|
221
|
+
@mounted = true
|
222
|
+
@init_args = args
|
223
|
+
@init_results = {}
|
224
|
+
entries.each_pair { |name, d| init_dir(name, d) }
|
225
|
+
nil
|
226
|
+
end
|
227
|
+
|
228
|
+
# Calls destroy on all current entries
|
229
|
+
def destroy(*_args)
|
230
|
+
entries.each_pair { |name, d| entry_send(d, :destroy, init_results[name]) }
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
234
|
+
# @!endgroup
|
235
|
+
|
236
|
+
# Looks up the first path component in {#entries} and then sends the remainder of the path to the callback
|
237
|
+
# on that entry
|
238
|
+
# @param [Symbol] callback a FUSE Callback
|
239
|
+
# @param [String] path
|
240
|
+
# @param [Array] args callback arguments
|
241
|
+
# @param [Proc] invoke optional block to keep passing down. See {#mkdir}, {#create}
|
242
|
+
# @param [Class<SystemCallError>] notsup
|
243
|
+
# @raise [Errno:ENOENT] if the next entry does not exist
|
244
|
+
# @raise [SystemCallError] error from notsup if the next entry does not respond to ths callback
|
245
|
+
def path_method(callback, path, *args, notsup: Errno::ENOTSUP, &invoke)
|
246
|
+
path = path.to_s
|
247
|
+
# Fuse paths always start with a leading slash and never have a trailing slash
|
248
|
+
sep_index = path.index('/', 1)
|
249
|
+
entry_key = sep_index ? path[1..sep_index - 1] : path[1..]
|
250
|
+
entry = entries[entry_key]
|
251
|
+
|
252
|
+
raise Errno::ENOENT unless entry
|
253
|
+
|
254
|
+
responds = entry_fuse_respond_to?(entry, callback)
|
255
|
+
return unless responds || notsup
|
256
|
+
raise notsup unless responds
|
257
|
+
|
258
|
+
if mounted? && (init_obj = init_results[entry_key])
|
259
|
+
FuseContext.get.overrides.merge!(private_data: init_obj)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Pass remaining path components to the next filesystem
|
263
|
+
next_path = sep_index ? path[sep_index..] : '/'
|
264
|
+
entry.public_send(callback, next_path, *args, &invoke)
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
attr_reader :init_args, :init_results
|
270
|
+
|
271
|
+
def method_missing(method, *args, &invoke)
|
272
|
+
return super unless FuseOperations.path_callbacks.include?(method)
|
273
|
+
|
274
|
+
raise Errno::ENOTSUP if root?(args.first)
|
275
|
+
|
276
|
+
path_method(method, *args, &invoke)
|
277
|
+
end
|
278
|
+
|
279
|
+
def respond_to_missing?(method, inc_private = false)
|
280
|
+
return true if FuseOperations.path_callbacks.include?(method)
|
281
|
+
|
282
|
+
super
|
283
|
+
end
|
284
|
+
|
285
|
+
def entry_key(path)
|
286
|
+
path[1..] unless path.index('/', 1)
|
287
|
+
end
|
288
|
+
|
289
|
+
def init_dir(name, dir)
|
290
|
+
init_result = entry_fuse_respond_to?(dir, :init) ? dir.init(*init_args) : nil
|
291
|
+
init_results[name] = init_result if init_result
|
292
|
+
end
|
293
|
+
|
294
|
+
def entry_fuse_respond_to?(entry_fs, method)
|
295
|
+
entry_fs.respond_to?(:fuse_respond_to?) ? entry_fs.fuse_respond_to?(method) : entry_fs.respond_to?(method)
|
296
|
+
end
|
297
|
+
|
298
|
+
def entry_send(entry, callback, *args)
|
299
|
+
return unless entry_fuse_respond_to?(entry, callback)
|
300
|
+
|
301
|
+
entry.public_send(callback, *args)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'accounting'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
module FFI
|
7
|
+
module Libfuse
|
8
|
+
module Filesystem
|
9
|
+
# A Filesystem representing a single synthetic file at the root
|
10
|
+
class VirtualFile < VirtualNode
|
11
|
+
prepend Adapter::Ruby::Prepend
|
12
|
+
include Fuse2Compat
|
13
|
+
|
14
|
+
# @return [String] the (binary) content of the synthetic file
|
15
|
+
attr_reader :content
|
16
|
+
|
17
|
+
# Create an empty synthetic file
|
18
|
+
def initialize(accounting: nil)
|
19
|
+
super(accounting: accounting)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
def path_method(_method, *_args)
|
24
|
+
raise Errno::ENOENT
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!group FUSE Callbacks
|
28
|
+
|
29
|
+
def getattr(path, stat, ffi = nil)
|
30
|
+
# We don't exist until create or otherwise or virtual stat exists
|
31
|
+
raise Errno::ENOENT unless root?(path) && virtual_stat
|
32
|
+
|
33
|
+
stat.file(size: (ffi&.fh || content).size, **virtual_stat)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [String] _path ignored, expected to be '/'
|
38
|
+
# @param [Integer] mode
|
39
|
+
# @param [FuseFileInfo] ffi
|
40
|
+
# @return [Object] a file handled (captured by {Adapter::Ruby::Prepend})
|
41
|
+
def create(_path, mode, ffi = nil)
|
42
|
+
init_node(mode)
|
43
|
+
@content = String.new(encoding: 'binary')
|
44
|
+
sio(ffi) if ffi
|
45
|
+
end
|
46
|
+
|
47
|
+
def open(_path, ffi)
|
48
|
+
virtual_stat[:atime] = Time.now.utc
|
49
|
+
sio(ffi)
|
50
|
+
end
|
51
|
+
|
52
|
+
# op[:read] = [:pointer, :size_t, :off_t, FuseFileInfo.by_ref]
|
53
|
+
def read(path, size, off, ffi)
|
54
|
+
raise Errno::ENOENT unless root?(path)
|
55
|
+
|
56
|
+
io = sio(ffi)
|
57
|
+
io.seek(off)
|
58
|
+
io.read(size)
|
59
|
+
end
|
60
|
+
|
61
|
+
# write(const char* path, char *buf, size_t size, off_t offset, struct fuse_file_info* fi)
|
62
|
+
def write(path, data, offset = 0, ffi = nil)
|
63
|
+
raise Errno::ENOENT unless root?(path)
|
64
|
+
|
65
|
+
accounting&.write(content.size, data.size, offset)
|
66
|
+
io = sio(ffi)
|
67
|
+
io.seek(offset)
|
68
|
+
io.write(data)
|
69
|
+
virtual_stat[:mtime] = Time.now.utc
|
70
|
+
end
|
71
|
+
|
72
|
+
def truncate(path, size, ffi = nil)
|
73
|
+
raise Errno::ENOENT unless root?(path)
|
74
|
+
|
75
|
+
accounting&.truncate(content.size, size)
|
76
|
+
sio(ffi).truncate(size)
|
77
|
+
virtual_stat[:mtime] = Time.now.utc
|
78
|
+
end
|
79
|
+
|
80
|
+
def unlink(path)
|
81
|
+
raise Errno::ENOENT unless root?(path)
|
82
|
+
|
83
|
+
accounting&.adjust(-content.size, -1)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def sio(ffi)
|
89
|
+
ffi&.fh || StringIO.new(content, ffi&.flags)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utils'
|
4
|
+
require_relative 'virtual_dir'
|
5
|
+
require_relative 'accounting'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
module FFI
|
9
|
+
module Libfuse
|
10
|
+
module Filesystem
|
11
|
+
# A configurable main Filesystem that delegates FUSE Callbacks to another filesystem
|
12
|
+
#
|
13
|
+
# This class registers support for all available fuse callbacks, subject to the options below.
|
14
|
+
#
|
15
|
+
# Delegate filesystems like {VirtualDir} may raise ENOTSUP to indicate a callback is not handled at runtime
|
16
|
+
# although the behaviour of C libfuse varies in this regard.
|
17
|
+
#
|
18
|
+
# Filesystem options
|
19
|
+
# ===
|
20
|
+
#
|
21
|
+
# Passed by -o options to {Libfuse.main}
|
22
|
+
#
|
23
|
+
# * :max_space used for #{FuseOperations#statfs}. See {#accounting}
|
24
|
+
# * :max_nodes used for #{FuseOperations#statfs}. See {#accounting}
|
25
|
+
# * :no_buf do not register :read_buf, :write_buf
|
26
|
+
#
|
27
|
+
# when set all filesystems must implement :read/:write
|
28
|
+
#
|
29
|
+
# when not set all filesystems must implement :read_buf, :write_buf which enables C libfuse to handle
|
30
|
+
# file descriptor based io, eg. {MappedFiles}, but means Ruby FFI is doing memory allocations for string
|
31
|
+
# based io, eg. {VirtualFile}.
|
32
|
+
#
|
33
|
+
# Note that {VirtualFile} and {MappedFiles} both prepend {Adapter::Ruby::Prepend} which implements
|
34
|
+
# the logic to fallback from :read/:write_buf to plain :read/:write as necessary to support this option.
|
35
|
+
#
|
36
|
+
# It is writable to the user that mounted it may create and edit files within it
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# class MyFS < FFI::Libfuse::Filesystem::VirtualFS
|
40
|
+
# def fuse_configure
|
41
|
+
# build({ 'hello' => { 'world.txt' => 'Hello World'}})
|
42
|
+
# mkdir("/hello")
|
43
|
+
# create("/hello/world").write("Hello World!\n")
|
44
|
+
# create("/hello/everybody").write("Hello Everyone!\n")
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# exit(FFI::Libfuse.fuse_main(operations: MyFS.new))
|
49
|
+
#
|
50
|
+
class VirtualFS
|
51
|
+
include Utils
|
52
|
+
include Adapter::Context
|
53
|
+
include Adapter::Debug
|
54
|
+
include Adapter::Safe
|
55
|
+
|
56
|
+
# @return [Object] the root filesystem that quacks like a {FuseOperations}
|
57
|
+
attr_reader :root
|
58
|
+
|
59
|
+
# @return [Hash{Symbol => String,Boolean}] custom options captured as defined by {fuse_options}
|
60
|
+
attr_reader :options
|
61
|
+
|
62
|
+
# @return [Accounting|:max_space=,:max_nodes=] an accumulator of filesystem statistics used to consume the
|
63
|
+
# max_space and max_nodes options
|
64
|
+
def accounting
|
65
|
+
@accounting ||= Accounting.new
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param [FuseOperations] root the root filesystem
|
69
|
+
# Subclasses can override the no-arg method and call super to pass in a different root.
|
70
|
+
def fuse_configure(root = VirtualDir.new(accounting: accounting).mkdir('/'))
|
71
|
+
@root = root
|
72
|
+
end
|
73
|
+
|
74
|
+
# @overload build(files)
|
75
|
+
# Adds files directly to the filesystem
|
76
|
+
# @param [Hash] files map of paths to content responding to
|
77
|
+
#
|
78
|
+
# * :each_pair is treated as a subdir of files
|
79
|
+
# * :readdir (eg {PassThroughDir}) is treated as a directory- sent via mkdir
|
80
|
+
# * :getattr (eg {PassThroughFile}) is treated as a file - sent via create
|
81
|
+
# * :to_str (eg {::String} ) is created as a {VirtualFile}
|
82
|
+
def build(files, base_path = Pathname.new('/'))
|
83
|
+
files.each_pair do |path, content|
|
84
|
+
path = (base_path + path).cleanpath
|
85
|
+
@root.mkdir_p(path.dirname) unless path.dirname == base_path
|
86
|
+
|
87
|
+
rt = %i[each_pair readdir getattr to_str].detect { |m| content.respond_to?(m) }
|
88
|
+
raise "Unsupported initial content for #{self.class.name}: #{content.class.name}- #{content}" unless rt
|
89
|
+
|
90
|
+
send("build_#{rt}", content, path)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# @!group Fuse Configuration
|
95
|
+
|
96
|
+
# TODO: Raise bug on libfuse (or fuse kernel module - ouch!) create to also fallback to mknod on ENOTSUP
|
97
|
+
|
98
|
+
# Respond to all FUSE Callbacks except deprecated, noting that ..
|
99
|
+
#
|
100
|
+
# * :read_buf, :write_buf can be excluded by the 'no_buf' mount option
|
101
|
+
# * :access already has a libfuse mount option (default_permissions)
|
102
|
+
# * :create falls back to :mknod on ENOSYS (as raised by {VirtualDir})
|
103
|
+
# * :copy_file_range can raise ENOTSUP to trigger glibc to fallback to inefficient copy
|
104
|
+
def fuse_respond_to?(method)
|
105
|
+
case method
|
106
|
+
when :getdir, :fgetattr
|
107
|
+
# TODO: Find out if fgetattr works on linux, something wrong with stat values on OSX.
|
108
|
+
# https://github.com/osxfuse/osxfuse/issues/887
|
109
|
+
false
|
110
|
+
when :read_buf, :write_buf
|
111
|
+
!no_buf
|
112
|
+
else
|
113
|
+
true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Default fuse options
|
118
|
+
# Subclasses can override this method and call super with the additional options:
|
119
|
+
# @param [Hash] opts additional options to parse into the {#options} attribute
|
120
|
+
def fuse_options(args, opts = {})
|
121
|
+
@options = {}
|
122
|
+
opts = opts.merge({ 'no_buf' => :no_buf }).merge(Accounting::OPTIONS)
|
123
|
+
args.parse!(opts) do |key:, value:, **|
|
124
|
+
case key
|
125
|
+
when *Accounting::OPTIONS.values.uniq
|
126
|
+
next accounting.fuse_opt_proc(key: key, value: value)
|
127
|
+
when :no_buf
|
128
|
+
@no_buf = true
|
129
|
+
else
|
130
|
+
options[key] = value
|
131
|
+
end
|
132
|
+
:handled
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Subclasses can override this method to add descriptions for additional options
|
137
|
+
def fuse_help
|
138
|
+
<<~END_HELP
|
139
|
+
#{Accounting::HELP}
|
140
|
+
#{self.class.name} options:
|
141
|
+
-o no_buf always use read, write instead of read_buf, write_buf
|
142
|
+
END_HELP
|
143
|
+
end
|
144
|
+
|
145
|
+
# Subclasses can override to produce a nice version string for -V
|
146
|
+
def fuse_version
|
147
|
+
self.class.name
|
148
|
+
end
|
149
|
+
|
150
|
+
# @!endgroup
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def build_each_pair(content, path)
|
155
|
+
build(content, path)
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_readdir(content, path)
|
159
|
+
@root.mkdir(path.to_s) { content }
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_getattr(content, path)
|
163
|
+
@root.create(path.to_s) { content }
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_to_str(content, path)
|
167
|
+
@root.create(path.to_s) { content }
|
168
|
+
end
|
169
|
+
|
170
|
+
# Passes FUSE Callbacks on to the {#root} filesystem
|
171
|
+
def method_missing(method, *args, &block)
|
172
|
+
return @root.public_send(method, *args, &block) if @root.respond_to?(method)
|
173
|
+
|
174
|
+
# This is not always reliable but better than raising NoMethodError
|
175
|
+
return -Errno::ENOTSUP::Errno if FuseOperations.fuse_callbacks.include?(method)
|
176
|
+
|
177
|
+
super
|
178
|
+
end
|
179
|
+
|
180
|
+
def respond_to_missing?(method, private = false)
|
181
|
+
FuseOperations.fuse_callbacks.include?(method) || @root.respond_to?(method, private) || super
|
182
|
+
end
|
183
|
+
|
184
|
+
attr_reader :no_buf
|
185
|
+
|
186
|
+
# This class does not implement any fuse methods, ensure they are passed to method missing.
|
187
|
+
# eg Kernel.open
|
188
|
+
FFI::Libfuse::FuseOperations.fuse_callbacks.each do |c|
|
189
|
+
undef_method(c)
|
190
|
+
rescue StandardError
|
191
|
+
nil
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'accounting'
|
4
|
+
require_relative '../adapter/ruby'
|
5
|
+
|
6
|
+
module FFI
|
7
|
+
module Libfuse
|
8
|
+
module Filesystem
|
9
|
+
# @abstract
|
10
|
+
# Common FUSE Callbacks for a virtual inode
|
11
|
+
#
|
12
|
+
# **Note** this class is used by both {VirtualFile} which is under {Adapter::Ruby::Prepend}
|
13
|
+
# and {VirtualDir} which passes on native {FuseOperations} calls
|
14
|
+
class VirtualNode
|
15
|
+
# @return [Hash<Symbol,Integer>] base file or directory stat information used for :getattr of this node
|
16
|
+
attr_reader :virtual_stat
|
17
|
+
|
18
|
+
# @return [Hash<String,String>] virtual extended attributes
|
19
|
+
attr_reader :virtual_xattr
|
20
|
+
|
21
|
+
# @return [Accounting|nil] file system statistcs accumulator
|
22
|
+
attr_reader :accounting
|
23
|
+
|
24
|
+
# @param [Accounting] accounting accumulator of filesystem statistics
|
25
|
+
def initialize(accounting: Accounting.new)
|
26
|
+
@accounting = accounting
|
27
|
+
|
28
|
+
@virtual_xattr = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# @!method path_method(callback, *args)
|
32
|
+
# @abstract
|
33
|
+
# called if this node cannot handle the callback (ie path is not root or an entry in this directory)
|
34
|
+
|
35
|
+
# @!group FUSE Callbacks
|
36
|
+
|
37
|
+
def utimens(path, *args)
|
38
|
+
return path_method(__method__, path, *args) unless root?(path)
|
39
|
+
|
40
|
+
atime, mtime, *_fuse3 = args
|
41
|
+
# if native fuse call atime will be Array<Stat::TimeSpec>
|
42
|
+
atime, mtime = Stat::TimeSpec.fill_times(atime[0, 2], 2).map(&:time) if atime.is_a?(Array)
|
43
|
+
virtual_stat[:atime] = atime if atime
|
44
|
+
virtual_stat[:mtime] = mtime if mtime
|
45
|
+
virtual_stat[:ctime] = mtime if mtime
|
46
|
+
end
|
47
|
+
|
48
|
+
def chmod(path, mode, *args)
|
49
|
+
return path_method(__method__, path, mode, *args) unless root?(path)
|
50
|
+
|
51
|
+
virtual_stat[:mode] = mode
|
52
|
+
virtual_stat[:ctime] = Time.now
|
53
|
+
end
|
54
|
+
|
55
|
+
def chown(path, uid, gid, *args)
|
56
|
+
return path_method(__method__, path, uid, gid, *args) unless root?(path)
|
57
|
+
|
58
|
+
virtual_stat[:uid] = uid
|
59
|
+
virtual_stat[:gid] = gid
|
60
|
+
virtual_stat[:ctime] = Time.now
|
61
|
+
end
|
62
|
+
|
63
|
+
def statfs(path, statfs_buf)
|
64
|
+
return path_method(__method__, path, statfs_buf) unless root?(path)
|
65
|
+
raise Errno::ENOTSUP unless accounting
|
66
|
+
|
67
|
+
accounting.to_statvfs(statfs_buf)
|
68
|
+
end
|
69
|
+
|
70
|
+
def getxattr(path, name, buf = nil, size = nil)
|
71
|
+
return path_method(__method__, path, name, buf, size) unless root?(path)
|
72
|
+
return virtual_xattr[name] unless buf
|
73
|
+
|
74
|
+
Adapter::Ruby.getxattr(buf, size) { virtual_xattr[name] }
|
75
|
+
end
|
76
|
+
|
77
|
+
def listxattr(path, buf = nil, size = nil)
|
78
|
+
return path_method(__method__, path) unless root?(path)
|
79
|
+
return virtual_xattr.keys unless buf
|
80
|
+
|
81
|
+
Adapter::Ruby.listxattr(buf, size) { virtual_xattr.keys }
|
82
|
+
end
|
83
|
+
|
84
|
+
# @!endgroup
|
85
|
+
|
86
|
+
# Initialise the stat information for the node - should only be called once (eg from create or mkdir)
|
87
|
+
def init_node(mode, ctx: FuseContext.get, now: Time.now)
|
88
|
+
@virtual_stat = { mode: mode & ~ctx.umask, uid: ctx.uid, gid: ctx.gid, ctime: now, mtime: now, atime: now }
|
89
|
+
accounting&.adjust(0, +1)
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def root?(path)
|
96
|
+
path.to_s == '/'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|