ffi-libfuse 0.3.4 → 0.4.0

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.
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