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
@@ -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,188 @@
|
|
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
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
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
|