ffi-libfuse 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +1 -1
  4. data/lib/ffi/accessors.rb +21 -7
  5. data/lib/ffi/boolean_int.rb +1 -1
  6. data/lib/ffi/devt.rb +3 -3
  7. data/lib/ffi/libfuse/adapter/debug.rb +53 -15
  8. data/lib/ffi/libfuse/adapter/fuse2_compat.rb +38 -21
  9. data/lib/ffi/libfuse/adapter/fuse3_support.rb +0 -1
  10. data/lib/ffi/libfuse/adapter/ruby.rb +210 -159
  11. data/lib/ffi/libfuse/adapter/safe.rb +69 -21
  12. data/lib/ffi/libfuse/callbacks.rb +2 -1
  13. data/lib/ffi/libfuse/filesystem/accounting.rb +1 -1
  14. data/lib/ffi/libfuse/filesystem/mapped_files.rb +33 -7
  15. data/lib/ffi/libfuse/filesystem/pass_through_dir.rb +0 -1
  16. data/lib/ffi/libfuse/filesystem/virtual_dir.rb +293 -126
  17. data/lib/ffi/libfuse/filesystem/virtual_file.rb +85 -79
  18. data/lib/ffi/libfuse/filesystem/virtual_fs.rb +34 -15
  19. data/lib/ffi/libfuse/filesystem/virtual_link.rb +60 -0
  20. data/lib/ffi/libfuse/filesystem/virtual_node.rb +104 -87
  21. data/lib/ffi/libfuse/filesystem.rb +1 -1
  22. data/lib/ffi/libfuse/fuse2.rb +3 -2
  23. data/lib/ffi/libfuse/fuse3.rb +1 -1
  24. data/lib/ffi/libfuse/fuse_args.rb +5 -2
  25. data/lib/ffi/libfuse/fuse_buf.rb +112 -0
  26. data/lib/ffi/libfuse/fuse_buf_vec.rb +228 -0
  27. data/lib/ffi/libfuse/fuse_common.rb +10 -4
  28. data/lib/ffi/libfuse/fuse_config.rb +16 -7
  29. data/lib/ffi/libfuse/fuse_operations.rb +86 -41
  30. data/lib/ffi/libfuse/gem_helper.rb +2 -9
  31. data/lib/ffi/libfuse/io.rb +56 -0
  32. data/lib/ffi/libfuse/main.rb +27 -24
  33. data/lib/ffi/libfuse/test_helper.rb +68 -60
  34. data/lib/ffi/libfuse/version.rb +1 -1
  35. data/lib/ffi/libfuse.rb +1 -1
  36. data/lib/ffi/stat/native.rb +4 -4
  37. data/lib/ffi/stat.rb +19 -3
  38. data/lib/ffi/struct_array.rb +2 -1
  39. data/sample/hello_fs.rb +1 -1
  40. metadata +6 -3
  41. data/lib/ffi/libfuse/fuse_buffer.rb +0 -257
@@ -14,6 +14,11 @@ module FFI
14
14
  #
15
15
  # Implements callbacks satisfying {Adapter::Ruby} which is automatically included.
16
16
  module MappedFiles
17
+ # @!visibility private
18
+ def self.included(mod)
19
+ mod.prepend(Adapter::Ruby::Prepend)
20
+ end
21
+
17
22
  # Do we have ffi-xattr to handle extended attributes in real files
18
23
  HAS_XATTR =
19
24
  begin
@@ -34,7 +39,7 @@ module FFI
34
39
  # @return [String] mapped_path in an underlying filesystem
35
40
  #
36
41
  # Fuse callbacks are fulfilled using Ruby's native File methods called on this path
37
- # @return [String, Adapter::Ruby::Prepend] mapped_path, filesystem
42
+ # @return [String, Adapter::Ruby] mapped_path, filesystem
38
43
  #
39
44
  # If an optional filesystem value is returned fuse callbacks will be passed on to this filesystem with the
40
45
  # mapped_path and other callback args unchanged
@@ -78,6 +83,26 @@ module FFI
78
83
  path_method(__method__, path, ffi) { |rp| File.open(rp, ffi.flags) }
79
84
  end
80
85
 
86
+ # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
87
+ def read(path, size, offset, ffi)
88
+ path_method(__method__, path, size, offset, ffi) { |_rp| nil }
89
+ end
90
+
91
+ # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
92
+ def read_buf(path, size, offset, ffi)
93
+ path_method(__method__, path, size, offset, ffi, error: nil) { |_rp| nil }
94
+ end
95
+
96
+ # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
97
+ def write(path, size, offset, ffi)
98
+ path_method(__method__, path, size, offset, ffi) { |_rp| nil }
99
+ end
100
+
101
+ # implemented to allow for virtual files within the mapped fs, but we just rely om the result of open
102
+ def write_buf(path, offset, ffi, &buffer)
103
+ path_method(__method__, path, offset, ffi, block: buffer, error: nil) { |_rp| nil }
104
+ end
105
+
81
106
  # Truncates the file handle (or the real file)
82
107
  def truncate(path, size, ffi = nil)
83
108
  return ffi.fh.truncate(size) if ffi&.fh
@@ -122,18 +147,19 @@ module FFI
122
147
  end
123
148
  # @!endgroup
124
149
 
125
- # @!visibility private
126
- def self.included(mod)
127
- mod.prepend(Adapter::Ruby::Prepend)
128
- end
129
-
130
150
  private
131
151
 
132
152
  def path_method(callback, path, *args, error: Errno::ENOENT, block: nil)
133
153
  rp, fs = map_path(path)
154
+
134
155
  raise error if error && !rp
156
+ return nil unless rp
157
+ return yield(rp) unless fs
158
+
159
+ return fs.send(callback, rp, *args, &block) if fs.respond_to?(callback)
160
+ raise error if error
135
161
 
136
- fs ? fs.send(callback, rp, *args, &block) : yield(rp)
162
+ nil
137
163
  end
138
164
  end
139
165
  end
@@ -9,7 +9,6 @@ module FFI
9
9
  class PassThroughDir
10
10
  include MappedFiles
11
11
  include Adapter::Debug
12
- include Adapter::Safe
13
12
  include Utils
14
13
 
15
14
  # @return [String] The base directory
@@ -3,6 +3,7 @@
3
3
  require_relative 'accounting'
4
4
  require_relative 'virtual_node'
5
5
  require_relative 'virtual_file'
6
+ require_relative 'virtual_link'
6
7
  require_relative 'pass_through_file'
7
8
  require_relative 'pass_through_dir'
8
9
  require_relative 'mapped_dir'
@@ -12,7 +13,7 @@ module FFI
12
13
  module Filesystem
13
14
  # A Filesystem of Filesystems
14
15
  #
15
- # Implements a recursive Hash based directory of sub filesystems.
16
+ # Implements a simple Hash based directory of sub filesystems.
16
17
  #
17
18
  # FUSE Callbacks
18
19
  # ===
@@ -40,24 +41,21 @@ module FFI
40
41
 
41
42
  def initialize(accounting: Accounting.new)
42
43
  @entries = {}
43
- @mounted = false
44
44
  super(accounting: accounting)
45
45
  end
46
46
 
47
- # @return [Boolean] true if this dir been mounted
48
- def mounted?
49
- @mounted
50
- end
51
-
52
47
  # @!endgroup
53
48
 
54
49
  # @!group FUSE Callbacks
55
50
 
56
51
  # 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)
52
+ def getattr(path, stat_buf = nil, ffi = nil)
53
+ if root?(path)
54
+ stat_buf&.directory(nlink: entries.size + 2, **virtual_stat)
55
+ return self
56
+ end
59
57
 
60
- stat_buf&.directory(**virtual_stat.merge({ nlink: entries.size + 2 }))
58
+ path_method(__method__, path, stat_buf, ffi, notsup: Errno::ENOSYS)
61
59
  end
62
60
 
63
61
  # Safely passes on file open to next filesystem
@@ -69,8 +67,6 @@ module FFI
69
67
  raise Errno::EISDIR if root?(path)
70
68
 
71
69
  path_method(__method__, path, *args, notsup: nil)
72
- rescue Errno::ENOTSUP, Errno::ENOSYS
73
- nil
74
70
  end
75
71
 
76
72
  # Safely handle file release
@@ -81,8 +77,6 @@ module FFI
81
77
  raise Errno::EISDIR if root?(path)
82
78
 
83
79
  path_method(__method__, path, *args, notsup: nil)
84
- rescue Errno::ENOTSUP, Errno::ENOSYS
85
- # do nothing
86
80
  end
87
81
 
88
82
  # Safely handles directory open to next filesystem
@@ -91,11 +85,9 @@ module FFI
91
85
  # @return [Object] the result of {#path_method} for all other paths
92
86
  # @return [nil] for sub-filesystems that do not implement this callback or raise ENOTSUP or ENOSYS
93
87
  def opendir(path, ffi)
94
- return path_method(__method__, path, ffi, notsup: nil) unless root?(path)
88
+ return (ffi.fh = self) if root?(path)
95
89
 
96
- ffi.fh = self
97
- rescue Errno::ENOTSUP, Errno::ENOSYS
98
- nil
90
+ path_method(__method__, path, ffi, notsup: nil)
99
91
  end
100
92
 
101
93
  # Safely handles directory release
@@ -104,9 +96,9 @@ module FFI
104
96
  #
105
97
  # Otherwise safely passes on to next filesystem, rescuing ENOTSUP or ENOSYS
106
98
  def releasedir(path, *args)
107
- path_method(__method__, path, *args, notsup: nil) unless root?(path)
108
- rescue Errno::ENOTSUP, Errno::ENOSYS
109
- # do nothing
99
+ return if root?(path)
100
+
101
+ path_method(__method__, path, *args, notsup: nil)
110
102
  end
111
103
 
112
104
  # If path is root fills the directory from the keys in {#entries}
@@ -117,14 +109,13 @@ module FFI
117
109
  def readdir(path, buf, filler, offset, ffi, *flag)
118
110
  return %w[. ..].concat(entries.keys).each(&Adapter::Ruby::ReaddirFiller.new(buf, filler)) if root?(path)
119
111
 
120
- return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if entry_fuse_respond_to?(ffi.fh, :readdir)
112
+ return ffi.fh.readdir('/', buf, filler, offset, ffi, *flag) if dir_entry?(ffi.fh)
121
113
 
122
- path_method(:readdir, path, buf, filler, offset, ffi, *flag) unless root?(path)
114
+ path_method(:readdir, path, buf, filler, offset, ffi, *flag, notsup: Errno::ENOTDIR)
123
115
  end
124
116
 
125
117
  # 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
118
+ # For our entries, passes on the call to the entry (with path='/') and then removes the entry.
128
119
  # @raise [Errno::ENOTEMPTY] if path is root and our entries list is not empty
129
120
  # @raise [Errno::ENOENT] if the entry does not exist
130
121
  # @raise [Errno::ENOTDIR] if the entry does not respond to :readdir (ie: is not a directory)
@@ -136,144 +127,275 @@ module FFI
136
127
  return
137
128
  end
138
129
 
139
- entry_key = entry_key(path)
140
- return path_method(__method__, path) unless entry_key
130
+ path_method(__method__, path) do |entry_key, dir|
131
+ raise Errno::ENOENT unless dir
132
+ raise Errno::ENOTDIR unless dir_entry?(dir)
141
133
 
142
- dir = entries[entry_key]
143
- raise Errno::ENOENT unless dir
144
- raise Errno::ENOTDIR unless entry_fuse_respond_to?(dir, :readdir)
134
+ entry_send(dir, :rmdir, '/')
145
135
 
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
136
+ entries.delete(entry_key)
137
+ dir
138
+ end
165
139
  end
166
140
 
167
141
  # For our entries, creates a new file
168
142
  # @raise [Errno::EISDIR] if the entry exists and responds_to?(:readdir)
169
143
  # @raise [Errno::EEXIST] if the entry exists
170
- # @yield []
171
- # @yieldreturn [Object] something that quacks with the FUSE Callbacks of a regular file
144
+ # @yield [String] filename the name of the file in this directory
145
+ # @yieldreturn [:getattr] something that quacks with the FUSE Callbacks of a regular file
172
146
  #
173
147
  # :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}
148
+ # @return the result of the supplied block, or if not given a new {VirtualFile}
175
149
  def create(path, mode = FuseContext.get.mask(0o644), ffi = nil, &file)
176
- file_name = entry_key(path)
150
+ raise Errno::EISDIR if root?(path)
177
151
 
178
152
  # 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)
153
+ path_method(__method__, path, mode, ffi, notsup: Errno::ENOSYS, block: file) do |name, existing|
154
+ raise Errno::EISDIR if dir_entry?(existing)
155
+ raise Errno::EEXIST if existing
156
+
157
+ # TODO: Strictly should understand setgid and sticky bits of this dir's mode when creating new files
158
+ new_file = file ? file.call(name) : new_file(name)
159
+ if entry_fuse_respond_to?(new_file, :create)
160
+ new_file.public_send(:create, '/', mode, ffi)
161
+ else
162
+ # TODO: generate a sensible device number
163
+ entry_send(new_file, :mknod, '/', mode, 0)
164
+ entry_send(new_file, :open, '/', ffi)
165
+ end
166
+ entries[name] = new_file
193
167
  end
194
- entries[file_name] = new_file
168
+ end
169
+
170
+ # Method for creating a new file
171
+ # @param [String] _name
172
+ # @return [FuseOperations] something representing a regular file
173
+ def new_file(_name)
174
+ VirtualFile.new(accounting: accounting)
195
175
  end
196
176
 
197
177
  # Creates a new directory entry in this directory
198
178
  # @param [String] path
199
179
  # @param [Integer] mode
200
- # @yield []
180
+ # @yield [String] name the name of the directory in this filesystem
201
181
  # @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}
182
+ # @return the result of the block if given, otherwise the newly created sub {VirtualDir}
203
183
  # @raise [Errno::EEXIST] if the entry already exists at path
204
184
  def mkdir(path, mode = FuseContext.get.mask(0o777), &dir)
205
185
  return init_node(mode) if root?(path)
206
186
 
207
- dir_name = entry_key(path)
208
- return path_method(__method__, path, mode, &dir) unless dir_name
187
+ path_method(__method__, path, mode, block: dir) do |dir_name, existing|
188
+ raise Errno::EEXIST if existing
209
189
 
210
- existing = entries[dir_name]
211
- raise Errno::EEXIST if existing
190
+ new_dir = dir ? dir.call(dir_name) : new_dir(dir_name)
191
+ entry_send(new_dir, :mkdir, '/', mode)
192
+ entries[dir_name] = new_dir
193
+ end
194
+ end
212
195
 
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
196
+ # Method for creating a new directory, called from mkdir
197
+ # @param [String] _name
198
+ # @return [FuseOperations] something representing a directory
199
+ def new_dir(_name)
200
+ VirtualDir.new(accounting: accounting)
217
201
  end
218
202
 
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
203
+ # Create a new hard link in this filesystem
204
+ #
205
+ # @param [String, nil] from_path
206
+ # @param [String] to_path
207
+ # @yield [existing]
208
+ # Used to retrieve the filesystem object at from_path to be linked at to_path
209
+ #
210
+ # If not supplied, a proc wrapping #{new_link} is created and used or passed on to sub-filesystems
211
+ # @yieldparam [FuseOperations] existing the object currently at to_path
212
+ # @yieldreturn [FuseOperations] an object representing an inode to be linked at to_path
213
+ # @raise [Errno::EISDIR] if this object is trying to be added as a link (since you can't hard link directories)
214
+ # @see new_link
215
+ def link(from_path, to_path, &linker)
216
+ # Can't link to a directory
217
+ raise Errno::EISDIR if root?(to_path)
218
+ raise Errno::ENOSYS unless from_path || linker
219
+
220
+ same_filesystem_method(__method__, from_path, to_path) do
221
+ linker ||= proc { |replacing| new_link(from_path, replacing) }
222
+ path_method(__method__, from_path, to_path, block: linker) do |link_name, existing|
223
+ linked_entry = linker.call(existing)
224
+ entries[link_name] = linked_entry
225
+ end
226
+ end
226
227
  end
227
228
 
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
229
+ # Called from within #{link}
230
+ # Uses #{getattr}(from_path) to find the filesystem object at from_path.
231
+ # Calls #{link}(nil, '/') on this object to signal that a new link has been created to it.
232
+ # Filesystem objects that do not support linking should raise `Errno::EPERM` if the object should not be hard
233
+ # linked (eg directories)
234
+ # @return [FuseOperations]
235
+ # @raise Errno::EXIST if there is an existing object to replace
236
+ # @raise Errno::EPERM if the object at from_path is not a filesystem (does not itself respond to #getattr)
237
+ def new_link(from_path, replacing)
238
+ raise Errno::EEXIST if replacing
239
+
240
+ linked_entry = getattr(from_path)
241
+
242
+ # the linked entry itself must represent a filesystem inode
243
+ raise Errno::EPERM unless entry_fuse_respond_to?(linked_entry, :getattr)
244
+
245
+ entry_send(linked_entry, :link, nil, '/')
246
+ linked_entry
247
+ end
248
+
249
+ # For our entries validates the entry exists and calls unlink('/') on it to do any cleanup
250
+ # before removing the entry from our entries list.
251
+ #
252
+ # If a block is supplied (eg #{rename}) it will be called before the entry is deleted
253
+ #
254
+ # @raise [Errno:EISDIR] if we are unlinking ourself (use rmdir instead)
255
+ # @raise [Errno::ENOENT] if the entry does not exist at path (and no block is provided)
256
+ # @return the unlinked filesystem object
257
+ # @yield(file_name, entry)
258
+ # @yieldparam [FuseOperations] entry a filesystem like object representing the file being unlinked
259
+ # @yieldreturn [void]
260
+ def unlink(path, &rename)
261
+ raise Errno::EISDIR if root?(path)
262
+
263
+ path_method(__method__, path, block: rename) do |entry_key, entry|
264
+ if rename
265
+ rename.call(entry)
266
+ elsif entry
267
+ entry_send(entry, :unlink, '/')
268
+ else
269
+ raise Errno::ENOENT
270
+ end
271
+
272
+ entries.delete(entry_key)
273
+ end
274
+ end
275
+
276
+ # Rename is handled via #{link} and #{unlink} using their respective block arguments to handle validation
277
+ # and retrieve the object at from_path. Intermediate directory filesystems are only required to pass on the
278
+ # block, while the final directory target of from_path and to_path must call these blocks as this class does.
279
+ #
280
+ # If to_path is being replaced the existing entry will be signaled via #{unlink}('/'), or #{rmdir}('/')
281
+ # @raise Errno::EINVAL if trying to rename the root object OR from_path is a directory prefix of to_path
282
+ # @raise Errno::ENOENT if the filesystem at from_path does not exist
283
+ # @raise Errno::ENOSYS if the filesystem at from_path or directory of to_path does not support rename
284
+ # @raise Errno::EEXIST if the filesystem at to_path already exists and is not a symlink
285
+ # @see POSIX rename(2)
286
+ # @note As per POSIX raname(2) silently succeeds if from_path and to_path are hard links to the
287
+ # same filesystem object (ie without unlinking from_path)
288
+ def rename(from_path, to_path)
289
+ return if from_path == to_path
290
+ raise Errno::EINVAL if root?(from_path)
291
+
292
+ same_filesystem_method(__method__, from_path, to_path, rescue_notsup: true) do
293
+ # Can't rename into a subdirectory of itself
294
+ raise Errno::EINVAL if to_path.start_with?("#{from_path}/")
295
+
296
+ # POSIX rename(2) requires to silently abandon, without unlinking from_path,
297
+ # if the inodes at from_path and to_path are the same object (ie hard linked to each other))
298
+ catch :same_hard_link do
299
+ link(nil, to_path) do |replacing|
300
+ check_rename_unlink(from_path)
301
+ unlink(from_path) do |source|
302
+ raise Errno::ENOENT unless source
303
+
304
+ throw :same_hard_link if source.equal?(replacing)
305
+ rename_cleanup_overwritten(replacing)
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ # Common between {#link} and {#rename} are callbacks that might have different semantics
313
+ # if called within the same sub-filesystem.
314
+ # While from_path and to_path have a common top level directory, we pass the callback on
315
+ # to the entry at that directory
316
+ def same_filesystem_method(callback, from_path, to_path, rescue_notsup: false)
317
+ return yield unless from_path # no from_path to traverse
318
+
319
+ to_dir, next_to_path = entry_path(to_path)
320
+ return yield if root?(next_to_path) # target is our entry, no more directories to traverse
321
+
322
+ from_dir, next_from_path = entry_path(from_path)
323
+ return yield if from_dir != to_dir # from and to in different directories, we need to handle it ourself
324
+
325
+ # try traverse into sub-fs, which must itself be a directory
326
+ begin
327
+ entry_send(
328
+ entries[to_dir], callback,
329
+ next_from_path, next_to_path,
330
+ notsup: Errno::ENOSYS, notdir: Errno::ENOTDIR, rescue_notsup: rescue_notsup
331
+ )
332
+ rescue Errno::ENOSYS, Errno::ENOTSUP
333
+ raise unless rescue_notsup
334
+
335
+ yield
336
+ end
337
+ end
338
+
339
+ # Creates a new symbolic link in this directory
340
+ # @param [String] target - an absolute path for the operating system or relative to path
341
+ # @param [String] path - the path to create the link at
342
+ def symlink(target, path)
343
+ path_method(__method__, target, path) do |link_name, existing|
344
+ raise Errno::EEXIST if existing
345
+
346
+ new_link = new_symlink(link_name)
347
+ entry_send(new_link, :symlink, target, '/')
348
+ entries[link_name] = new_link
349
+ end
350
+ end
351
+
352
+ def new_symlink(_name)
353
+ VirtualLink.new(accounting: accounting)
232
354
  end
233
355
 
234
356
  # @!endgroup
235
357
 
236
- # Looks up the first path component in {#entries} and then sends the remainder of the path to the callback
237
- # on that entry
358
+ # Finds the path argument of the callback and splits it into an entry in this directory and a remaining path
359
+ #
360
+ # If a block is given and there is no remaining path (ie our entry) the block is called and its value returned
361
+ #
362
+ # If the path is not our entry, the callback is passed on to the sub filesystem entry with the remaining path
363
+ #
364
+ # If the path is our entry, but not block is provided, the callback is passed to our entry with a path of '/'
365
+ #
238
366
  # @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
367
+ # @param [Array] args callback arguments (first argument is typically 'path')
368
+ # @param [Errno] notsup an error to raise if this callback is not supported by our entry
369
+ # @param [Proc] block optional block to keep passing down. See {#mkdir}, {#create}, {#link}
243
370
  # @raise [Errno:ENOENT] if the next entry does not exist
371
+ # @raise [Errno::ENOTDIR] if the next entry must be a directory, but does not respond to :raaddir
244
372
  # @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]
373
+ # @yield(entry_key, entry)
374
+ # @yieldparam [String,nil] entry_key the name of the entry in this directory or nil, if path is '/'
375
+ # @yieldparam [FuseOperations,nil] entry the filesystem object currently stored at entry_key
376
+ def path_method(callback, *args, notsup: Errno::ENOTSUP, block: nil)
377
+ # Inside path_method
378
+ _read_arg_method, path_arg_method, next_arg_method = FuseOperations.path_arg_methods(callback)
379
+ path = args.send(path_arg_method)
251
380
 
252
- raise Errno::ENOENT unless entry
381
+ entry_key, next_path = entry_path(path)
382
+ our_entry = root?(next_path)
253
383
 
254
- responds = entry_fuse_respond_to?(entry, callback)
255
- return unless responds || notsup
256
- raise notsup unless responds
384
+ return yield entry_key, entries[entry_key] if block_given? && our_entry
257
385
 
258
- if mounted? && (init_obj = init_results[entry_key])
259
- FuseContext.get.overrides.merge!(private_data: init_obj)
260
- end
386
+ # Pass to our entry
387
+ args.send(next_arg_method, next_path)
261
388
 
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)
389
+ notdir = Errno::ENOTDIR unless our_entry
390
+ entry_send(entries[entry_key], callback, *args, notsup: notsup, notdir: notdir, &block)
265
391
  end
266
392
 
267
393
  private
268
394
 
269
- attr_reader :init_args, :init_results
270
-
271
- def method_missing(method, *args, &invoke)
395
+ def method_missing(method, *args, &block)
272
396
  return super unless FuseOperations.path_callbacks.include?(method)
273
397
 
274
- raise Errno::ENOTSUP if root?(args.first)
275
-
276
- path_method(method, *args, &invoke)
398
+ path_method(method, *args, block: block)
277
399
  end
278
400
 
279
401
  def respond_to_missing?(method, inc_private = false)
@@ -282,23 +404,68 @@ module FFI
282
404
  super
283
405
  end
284
406
 
285
- def entry_key(path)
286
- path[1..] unless path.index('/', 1)
287
- end
407
+ # Split path into an entry key and remaining path
408
+ # @param [:to_s] path
409
+ # @return [nil] if path is root (or nil)
410
+ # @return [Array<String, String] entry_key and '/' if path refers to an entry in this directory
411
+ # @return [Array<String, String>] entry key and remaining path when path refers to an entry in a sub-directory
412
+ def entry_path(path)
413
+ return nil unless path
414
+
415
+ path = path.to_s
416
+ return nil if root?(path)
417
+
418
+ # Fuse paths always start with a leading slash and never have a trailing slash
419
+ sep_index = path.index('/', 1)
420
+
421
+ return [path[1..], '/'] unless sep_index
288
422
 
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
423
+ [path[1..sep_index - 1], path[sep_index..]]
292
424
  end
293
425
 
294
426
  def entry_fuse_respond_to?(entry_fs, method)
295
427
  entry_fs.respond_to?(:fuse_respond_to?) ? entry_fs.fuse_respond_to?(method) : entry_fs.respond_to?(method)
296
428
  end
297
429
 
298
- def entry_send(entry, callback, *args)
299
- return unless entry_fuse_respond_to?(entry, callback)
430
+ def dir_entry?(entry)
431
+ entry_fuse_respond_to?(entry, :readdir)
432
+ end
433
+
434
+ def entry_send(entry, callback, *args, notsup: nil, notdir: nil, rescue_notsup: notsup.nil?, &blk)
435
+ raise Errno::ENOENT unless entry
436
+ raise notdir if notdir && !dir_entry?(entry)
437
+
438
+ responds = entry_fuse_respond_to?(entry, callback)
439
+ return unless responds || notsup
440
+ raise notsup unless responds
441
+
442
+ entry.public_send(callback, *args, &blk)
443
+ rescue Errno::ENOTSUP, Errno::ENOSYS
444
+ raise unless rescue_notsup
445
+
446
+ nil
447
+ end
448
+
449
+ def check_rename_unlink(from_path)
450
+ # Safety check that the unlink proc is passed through to the final directory
451
+ # to explicitly support our rename proc.
452
+ rename_support = false
453
+ unlink("#{from_path}.__unlink_rename__") do |source|
454
+ rename_support = source.nil?
455
+ end
456
+ raise Errno::ENOSYS, 'rename via unlink not supported' unless rename_support
457
+ end
458
+
459
+ # Cleanup the object being overwritten, including potentially raising SystemCallError
460
+ # to prevent the rename going ahead
461
+ def rename_cleanup_overwritten(replacing)
462
+ return unless replacing
463
+
464
+ entry_send(replacing, dir_entry?(replacing) ? :rmdir : :unlink, '/')
465
+ end
300
466
 
301
- entry.public_send(callback, *args)
467
+ def root?(path)
468
+ path ? super : true
302
469
  end
303
470
  end
304
471
  end