rufio 0.32.0 → 0.34.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.
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # 非同期スキャナークラス(Pure Ruby実装、ポーリングベース)
5
+ class NativeScannerRubyCore
6
+ POLL_INTERVAL = 0.01 # 10ms
7
+
8
+ def initialize
9
+ @thread = nil
10
+ @state = :idle
11
+ @results = []
12
+ @error = nil
13
+ @current_progress = 0
14
+ @total_progress = 0
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ # 非同期スキャン開始
19
+ def scan_async(path)
20
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
21
+ raise StandardError, "Scanner is already running" unless @state == :idle
22
+
23
+ @mutex.synchronize do
24
+ @state = :scanning
25
+ @results = []
26
+ @error = nil
27
+ @current_progress = 0
28
+ @total_progress = 0
29
+ end
30
+
31
+ @thread = Thread.new do
32
+ begin
33
+ # ディレクトリをスキャン
34
+ entries = []
35
+ Dir.foreach(path) do |entry|
36
+ next if entry == '.' || entry == '..'
37
+
38
+ # キャンセルチェック
39
+ break if @state == :cancelled
40
+
41
+ full_path = File.join(path, entry)
42
+ stat = File.lstat(full_path)
43
+
44
+ entries << {
45
+ name: entry,
46
+ type: file_type(stat),
47
+ size: stat.size,
48
+ mtime: stat.mtime.to_i,
49
+ mode: stat.mode,
50
+ executable: stat.executable?,
51
+ hidden: entry.start_with?('.')
52
+ }
53
+
54
+ # 進捗を更新
55
+ @mutex.synchronize do
56
+ @current_progress += 1
57
+ @total_progress = entries.length + 1
58
+ end
59
+ end
60
+
61
+ # 結果を保存
62
+ @mutex.synchronize do
63
+ if @state == :cancelled
64
+ @state = :cancelled
65
+ else
66
+ @results = entries
67
+ @state = :done
68
+ end
69
+ end
70
+ rescue StandardError => e
71
+ @mutex.synchronize do
72
+ @error = e
73
+ @state = :failed
74
+ end
75
+ end
76
+ end
77
+
78
+ self
79
+ end
80
+
81
+ # 高速スキャン(エントリ数制限付き)
82
+ def scan_fast_async(path, max_entries)
83
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
84
+ raise StandardError, "Scanner is already running" unless @state == :idle
85
+
86
+ @mutex.synchronize do
87
+ @state = :scanning
88
+ @results = []
89
+ @error = nil
90
+ @current_progress = 0
91
+ @total_progress = max_entries
92
+ end
93
+
94
+ @thread = Thread.new do
95
+ begin
96
+ entries = []
97
+ count = 0
98
+
99
+ Dir.foreach(path) do |entry|
100
+ next if entry == '.' || entry == '..'
101
+ break if count >= max_entries
102
+
103
+ # キャンセルチェック
104
+ break if @state == :cancelled
105
+
106
+ full_path = File.join(path, entry)
107
+ stat = File.lstat(full_path)
108
+
109
+ entries << {
110
+ name: entry,
111
+ type: file_type(stat),
112
+ size: stat.size,
113
+ mtime: stat.mtime.to_i,
114
+ mode: stat.mode,
115
+ executable: stat.executable?,
116
+ hidden: entry.start_with?('.')
117
+ }
118
+ count += 1
119
+
120
+ # 進捗を更新
121
+ @mutex.synchronize do
122
+ @current_progress = count
123
+ end
124
+ end
125
+
126
+ # 結果を保存
127
+ @mutex.synchronize do
128
+ if @state == :cancelled
129
+ @state = :cancelled
130
+ else
131
+ @results = entries
132
+ @state = :done
133
+ end
134
+ end
135
+ rescue StandardError => e
136
+ @mutex.synchronize do
137
+ @error = e
138
+ @state = :failed
139
+ end
140
+ end
141
+ end
142
+
143
+ self
144
+ end
145
+
146
+ # ポーリングして完了待ち
147
+ def wait(timeout: nil)
148
+ start_time = Time.now
149
+ loop do
150
+ state = get_state
151
+
152
+ case state
153
+ when :done
154
+ return get_results
155
+ when :failed
156
+ raise @error || StandardError.new("Scan failed")
157
+ when :cancelled
158
+ raise StandardError, "Scan cancelled"
159
+ end
160
+
161
+ if timeout && (Time.now - start_time) > timeout
162
+ raise StandardError, "Timeout"
163
+ end
164
+
165
+ sleep POLL_INTERVAL
166
+ end
167
+ end
168
+
169
+ # 進捗報告付きで完了待ち
170
+ def wait_with_progress(&block)
171
+ loop do
172
+ state = get_state
173
+ progress = get_progress
174
+
175
+ yield(progress[:current], progress[:total]) if block_given?
176
+
177
+ case state
178
+ when :done
179
+ return get_results
180
+ when :failed
181
+ raise @error || StandardError.new("Scan failed")
182
+ when :cancelled
183
+ raise StandardError, "Scan cancelled"
184
+ end
185
+
186
+ sleep POLL_INTERVAL
187
+ end
188
+ end
189
+
190
+ # 状態確認
191
+ def get_state
192
+ @mutex.synchronize { @state }
193
+ end
194
+
195
+ # 進捗取得
196
+ def get_progress
197
+ @mutex.synchronize do
198
+ {
199
+ current: @current_progress,
200
+ total: @total_progress
201
+ }
202
+ end
203
+ end
204
+
205
+ # キャンセル
206
+ def cancel
207
+ @mutex.synchronize do
208
+ @state = :cancelled if @state == :scanning
209
+ end
210
+ end
211
+
212
+ # 結果取得(完了後)
213
+ def get_results
214
+ @mutex.synchronize { @results.dup }
215
+ end
216
+
217
+ # スキャナーを明示的に破棄
218
+ def close
219
+ @thread&.join if @thread&.alive?
220
+ @thread = nil
221
+ end
222
+
223
+ private
224
+
225
+ # ファイルタイプを判定
226
+ def file_type(stat)
227
+ if stat.directory?
228
+ 'directory'
229
+ elsif stat.symlink?
230
+ 'symlink'
231
+ elsif stat.file?
232
+ 'file'
233
+ else
234
+ 'other'
235
+ end
236
+ end
237
+ end
238
+
239
+ # NativeScanner - Rubyベースのディレクトリスキャナー(ネイティブライブラリは削除済み)
240
+ class NativeScanner
241
+ @mode = 'ruby'
242
+ @current_library = nil
243
+
244
+ class << self
245
+ # モード設定(常にRubyモードを使用)
246
+ def mode=(value)
247
+ @mode = 'ruby'
248
+ @current_library = nil
249
+ end
250
+
251
+ # 現在のモード取得
252
+ def mode
253
+ @mode ||= 'ruby'
254
+ end
255
+
256
+ # 利用可能なライブラリをチェック(Rubyのみ)
257
+ def available_libraries
258
+ {
259
+ ruby: true
260
+ }
261
+ end
262
+
263
+ # ディレクトリをスキャン
264
+ def scan_directory(path)
265
+ scan_with_ruby(path)
266
+ end
267
+
268
+ # 高速スキャン(エントリ数制限付き)
269
+ def scan_directory_fast(path, max_entries = 1000)
270
+ scan_fast_with_ruby(path, max_entries)
271
+ end
272
+
273
+ # バージョン情報取得
274
+ def version
275
+ "Ruby #{RUBY_VERSION}"
276
+ end
277
+
278
+ private
279
+
280
+ # Rubyでスキャン(ポーリング方式)
281
+ def scan_with_ruby(path)
282
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
283
+
284
+ # 非同期スキャナーを作成してスキャン
285
+ scanner = NativeScannerRubyCore.new
286
+ begin
287
+ scanner.scan_async(path)
288
+ scanner.wait(timeout: 60)
289
+ ensure
290
+ scanner.close
291
+ end
292
+ rescue StandardError => e
293
+ raise StandardError, "Ruby scan failed: #{e.message}"
294
+ end
295
+
296
+ # Ruby高速スキャン(エントリ数制限付き、ポーリング方式)
297
+ def scan_fast_with_ruby(path, max_entries)
298
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
299
+
300
+ scanner = NativeScannerRubyCore.new
301
+ begin
302
+ scanner.scan_fast_async(path, max_entries)
303
+ scanner.wait(timeout: 60)
304
+ ensure
305
+ scanner.close
306
+ end
307
+ rescue StandardError => e
308
+ raise StandardError, "Ruby fast scan failed: #{e.message}"
309
+ end
310
+
311
+ # ファイルタイプを判定
312
+ def file_type(stat)
313
+ if stat.directory?
314
+ 'directory'
315
+ elsif stat.symlink?
316
+ 'symlink'
317
+ elsif stat.file?
318
+ 'file'
319
+ else
320
+ 'other'
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+ require 'fiddle/import'
5
+
6
+ module Rufio
7
+ # Zig拡張の非同期FFI層
8
+ # Ruby側は「ハンドル(u64)だけ」を持つ
9
+ module NativeScannerZigFFI
10
+ extend Fiddle::Importer
11
+
12
+ LIB_PATH = File.expand_path('native/rufio_zig.bundle', __dir__)
13
+
14
+ @loaded = false
15
+ @available = false
16
+
17
+ class << self
18
+ def load!
19
+ return @available if @loaded
20
+
21
+ @loaded = true
22
+
23
+ if File.exist?(LIB_PATH)
24
+ begin
25
+ # 動的ライブラリをロード
26
+ dlload LIB_PATH
27
+
28
+ # ABI Boundary: Ruby ABI非依存のC関数(非同期版)
29
+ extern 'uint64_t core_async_create()'
30
+ extern 'int32_t core_async_scan(uint64_t, const char*)'
31
+ extern 'int32_t core_async_scan_fast(uint64_t, const char*, size_t)'
32
+ extern 'uint8_t core_async_get_state(uint64_t)'
33
+ extern 'void core_async_get_progress(uint64_t, void*, void*)'
34
+ extern 'void core_async_cancel(uint64_t)'
35
+ extern 'size_t core_async_get_count(uint64_t)'
36
+ extern 'size_t core_async_get_name(uint64_t, size_t, void*, size_t)'
37
+ extern 'int32_t core_async_get_attrs(uint64_t, size_t, void*, void*, void*, void*, void*)'
38
+ extern 'void core_async_destroy(uint64_t)'
39
+ extern 'char* core_async_version()'
40
+
41
+ @available = true
42
+ rescue StandardError => e
43
+ warn "Failed to load zig extension: #{e.message}" if ENV['RUFIO_DEBUG']
44
+ @available = false
45
+ end
46
+ else
47
+ @available = false
48
+ end
49
+
50
+ @available
51
+ end
52
+
53
+ def available?
54
+ load! unless @loaded
55
+ @available
56
+ end
57
+
58
+ # バージョン取得
59
+ def version
60
+ core_async_version.to_s
61
+ end
62
+ end
63
+ end
64
+
65
+ # 非同期スキャナークラス(ポーリングベース)
66
+ class NativeScannerZigCore
67
+ POLL_INTERVAL = 0.01 # 10ms
68
+
69
+ def initialize
70
+ @handle = NativeScannerZigFFI.core_async_create
71
+ raise StandardError, "Failed to create scanner" if @handle.zero?
72
+ end
73
+
74
+ # 非同期スキャン開始
75
+ def scan_async(path)
76
+ result = NativeScannerZigFFI.core_async_scan(@handle, path)
77
+ raise StandardError, "Failed to start scan" if result != 0
78
+ self
79
+ end
80
+
81
+ # 高速スキャン(エントリ数制限付き)
82
+ def scan_fast_async(path, max_entries)
83
+ result = NativeScannerZigFFI.core_async_scan_fast(@handle, path, max_entries)
84
+ raise StandardError, "Failed to start scan" if result != 0
85
+ self
86
+ end
87
+
88
+ # ポーリングして完了待ち
89
+ def wait(timeout: nil)
90
+ start_time = Time.now
91
+ loop do
92
+ state = get_state
93
+
94
+ case state
95
+ when :done
96
+ return get_results
97
+ when :failed
98
+ raise StandardError, "Scan failed"
99
+ when :cancelled
100
+ raise StandardError, "Scan cancelled"
101
+ end
102
+
103
+ if timeout && (Time.now - start_time) > timeout
104
+ raise StandardError, "Timeout"
105
+ end
106
+
107
+ sleep POLL_INTERVAL
108
+ end
109
+ end
110
+
111
+ # 進捗報告付きで完了待ち
112
+ def wait_with_progress(&block)
113
+ loop do
114
+ state = get_state
115
+ progress = get_progress
116
+
117
+ yield(progress[:current], progress[:total]) if block_given?
118
+
119
+ case state
120
+ when :done
121
+ return get_results
122
+ when :failed
123
+ raise StandardError, "Scan failed"
124
+ when :cancelled
125
+ raise StandardError, "Scan cancelled"
126
+ end
127
+
128
+ sleep POLL_INTERVAL
129
+ end
130
+ end
131
+
132
+ # 状態確認
133
+ def get_state
134
+ state_code = NativeScannerZigFFI.core_async_get_state(@handle)
135
+ [:idle, :scanning, :done, :cancelled, :failed][state_code] || :failed
136
+ end
137
+
138
+ # 進捗取得
139
+ def get_progress
140
+ current = Fiddle::Pointer.malloc(8)
141
+ total = Fiddle::Pointer.malloc(8)
142
+ NativeScannerZigFFI.core_async_get_progress(@handle, current, total)
143
+ {
144
+ current: current[0, 8].unpack1('Q'),
145
+ total: total[0, 8].unpack1('Q')
146
+ }
147
+ end
148
+
149
+ # キャンセル
150
+ def cancel
151
+ NativeScannerZigFFI.core_async_cancel(@handle)
152
+ end
153
+
154
+ # 結果取得(完了後)
155
+ def get_results
156
+ count = NativeScannerZigFFI.core_async_get_count(@handle)
157
+ entries = []
158
+ count.times { |i| entries << get_entry(i) }
159
+ entries
160
+ end
161
+
162
+ # スキャナーを明示的に破棄
163
+ def close
164
+ return if @handle.zero?
165
+
166
+ NativeScannerZigFFI.core_async_destroy(@handle)
167
+ @handle = 0
168
+ end
169
+
170
+ private
171
+
172
+ # 指定インデックスのエントリを取得
173
+ def get_entry(index)
174
+ # 名前を取得
175
+ name_buf = Fiddle::Pointer.malloc(256)
176
+ name_len = NativeScannerZigFFI.core_async_get_name(@handle, index, name_buf, 256)
177
+ name = name_buf[0, name_len].force_encoding('UTF-8')
178
+
179
+ # 属性を取得
180
+ is_dir = Fiddle::Pointer.malloc(1)
181
+ size = Fiddle::Pointer.malloc(8)
182
+ mtime = Fiddle::Pointer.malloc(8)
183
+ executable = Fiddle::Pointer.malloc(1)
184
+ hidden = Fiddle::Pointer.malloc(1)
185
+
186
+ NativeScannerZigFFI.core_async_get_attrs(@handle, index, is_dir, size, mtime, executable, hidden)
187
+
188
+ {
189
+ name: name,
190
+ is_dir: is_dir[0, 1].unpack1('C') != 0,
191
+ size: size[0, 8].unpack1('Q'),
192
+ mtime: mtime[0, 8].unpack1('q'),
193
+ executable: executable[0, 1].unpack1('C') != 0,
194
+ hidden: hidden[0, 1].unpack1('C') != 0
195
+ }
196
+ end
197
+ end
198
+
199
+ # Zig拡張が利用可能な場合のみ、NativeScannerに統合
200
+ if NativeScannerZigFFI.load!
201
+ class NativeScanner
202
+ class << self
203
+ # zigモードを追加
204
+ alias_method :original_mode=, :mode= unless method_defined?(:original_mode=)
205
+
206
+ def mode=(value)
207
+ case value
208
+ when 'zig'
209
+ if NativeScannerZigFFI.available?
210
+ @mode = 'zig'
211
+ @current_library = nil
212
+ else
213
+ @mode = 'ruby'
214
+ @current_library = nil
215
+ end
216
+ else
217
+ original_mode=(value)
218
+ end
219
+ end
220
+
221
+ # zigスキャン(ポーリング方式)
222
+ def scan_directory_with_zig(path)
223
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
224
+
225
+ # 非同期スキャナーを作成してスキャン
226
+ scanner = NativeScannerZigCore.new
227
+ begin
228
+ scanner.scan_async(path)
229
+ entries = scanner.wait(timeout: 60)
230
+
231
+ # 結果の形式を統一(type フィールドを追加)
232
+ entries.map do |entry|
233
+ {
234
+ name: entry[:name],
235
+ type: entry[:is_dir] ? 'directory' : 'file',
236
+ size: entry[:size],
237
+ mtime: entry[:mtime],
238
+ mode: 0,
239
+ executable: entry[:executable],
240
+ hidden: entry[:hidden]
241
+ }
242
+ end
243
+ ensure
244
+ scanner.close
245
+ end
246
+ rescue StandardError => e
247
+ raise StandardError, "Zig scan failed: #{e.message}"
248
+ end
249
+
250
+ # zigで高速スキャン(ポーリング方式)
251
+ def scan_directory_fast_with_zig(path, max_entries)
252
+ raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
253
+
254
+ scanner = NativeScannerZigCore.new
255
+ begin
256
+ scanner.scan_fast_async(path, max_entries)
257
+ entries = scanner.wait(timeout: 60)
258
+
259
+ entries.map do |entry|
260
+ {
261
+ name: entry[:name],
262
+ type: entry[:is_dir] ? 'directory' : 'file',
263
+ size: entry[:size],
264
+ mtime: entry[:mtime],
265
+ mode: 0,
266
+ executable: entry[:executable],
267
+ hidden: entry[:hidden]
268
+ }
269
+ end
270
+ ensure
271
+ scanner.close
272
+ end
273
+ rescue StandardError => e
274
+ raise StandardError, "Zig fast scan failed: #{e.message}"
275
+ end
276
+
277
+ # scan_directoryメソッドを拡張
278
+ alias_method :original_scan_directory, :scan_directory unless method_defined?(:original_scan_directory)
279
+
280
+ def scan_directory(path)
281
+ mode if @mode.nil?
282
+
283
+ case @mode
284
+ when 'zig'
285
+ scan_directory_with_zig(path)
286
+ else
287
+ original_scan_directory(path)
288
+ end
289
+ end
290
+
291
+ # scan_directory_fastメソッドを拡張
292
+ alias_method :original_scan_directory_fast, :scan_directory_fast unless method_defined?(:original_scan_directory_fast)
293
+
294
+ def scan_directory_fast(path, max_entries = 1000)
295
+ mode if @mode.nil?
296
+
297
+ case @mode
298
+ when 'zig'
299
+ scan_directory_fast_with_zig(path, max_entries)
300
+ else
301
+ original_scan_directory_fast(path, max_entries)
302
+ end
303
+ end
304
+
305
+ # versionメソッドを拡張
306
+ alias_method :original_version, :version unless method_defined?(:original_version)
307
+
308
+ def version
309
+ mode if @mode.nil?
310
+
311
+ case @mode
312
+ when 'zig'
313
+ NativeScannerZigFFI.version
314
+ else
315
+ original_version
316
+ end
317
+ end
318
+
319
+ # available_librariesを更新
320
+ alias_method :original_available_libraries, :available_libraries unless method_defined?(:original_available_libraries)
321
+
322
+ def available_libraries
323
+ original = original_available_libraries
324
+ original.merge(zig: NativeScannerZigFFI.available?)
325
+ end
326
+
327
+ # autoモードの優先順位を更新(zig > ruby)
328
+ def mode=(value)
329
+ case value
330
+ when 'auto'
331
+ # 優先順位: Zig > Ruby
332
+ if NativeScannerZigFFI.available?
333
+ @mode = 'zig'
334
+ @current_library = nil
335
+ else
336
+ @mode = 'ruby'
337
+ @current_library = nil
338
+ end
339
+ when 'zig'
340
+ if NativeScannerZigFFI.available?
341
+ @mode = 'zig'
342
+ @current_library = nil
343
+ else
344
+ @mode = 'ruby'
345
+ @current_library = nil
346
+ end
347
+ else
348
+ send(:original_mode=, value)
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end