rufio 0.33.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.
@@ -1,260 +1,309 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ffi'
4
- require 'json'
5
-
6
3
  module Rufio
7
- # NativeScanner - Rust/Goのネイティブライブラリを使った高速ディレクトリスキャナー
8
- class NativeScanner
9
- # ライブラリパス
10
- LIB_DIR = File.expand_path('native', __dir__)
11
- RUST_LIB = File.join(LIB_DIR, 'librufio_scanner.dylib')
12
- GO_LIB = File.join(LIB_DIR, 'libscanner.dylib')
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
13
17
 
14
- @mode = nil
15
- @current_library = nil
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
+ }
16
53
 
17
- # Rustライブラリ用のFFIモジュール
18
- module RustLib
19
- extend FFI::Library
20
-
21
- begin
22
- ffi_lib RUST_LIB
23
- attach_function :scan_directory, [:string], :pointer
24
- attach_function :scan_directory_fast, [:string, :int], :pointer
25
- attach_function :get_version, [], :pointer
26
- @available = true
27
- rescue LoadError, FFI::NotFoundError
28
- @available = false
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
29
76
  end
30
77
 
31
- def self.available?
32
- @available
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
33
141
  end
142
+
143
+ self
34
144
  end
35
145
 
36
- # Goライブラリ用のFFIモジュール
37
- module GoLib
38
- extend FFI::Library
39
-
40
- begin
41
- ffi_lib GO_LIB
42
- attach_function :ScanDirectory, [:string], :pointer
43
- attach_function :ScanDirectoryFast, [:string, :int], :pointer
44
- attach_function :GetVersion, [], :pointer
45
- attach_function :FreeCString, [:pointer], :void
46
- @available = true
47
- rescue LoadError, FFI::NotFoundError
48
- @available = false
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
49
209
  end
210
+ end
211
+
212
+ # 結果取得(完了後)
213
+ def get_results
214
+ @mutex.synchronize { @results.dup }
215
+ end
50
216
 
51
- def self.available?
52
- @available
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'
53
235
  end
54
236
  end
237
+ end
238
+
239
+ # NativeScanner - Rubyベースのディレクトリスキャナー(ネイティブライブラリは削除済み)
240
+ class NativeScanner
241
+ @mode = 'ruby'
242
+ @current_library = nil
55
243
 
56
244
  class << self
57
- # モード設定
245
+ # モード設定(常にRubyモードを使用)
58
246
  def mode=(value)
59
- case value
60
- when 'rust'
61
- if RustLib.available?
62
- @mode = 'rust'
63
- @current_library = RustLib
64
- else
65
- @mode = 'ruby'
66
- @current_library = nil
67
- end
68
- when 'go'
69
- if GoLib.available?
70
- @mode = 'go'
71
- @current_library = GoLib
72
- else
73
- @mode = 'ruby'
74
- @current_library = nil
75
- end
76
- when 'auto'
77
- # 優先順位: Rust > Go > Ruby
78
- if RustLib.available?
79
- @mode = 'rust'
80
- @current_library = RustLib
81
- elsif GoLib.available?
82
- @mode = 'go'
83
- @current_library = GoLib
84
- else
85
- @mode = 'ruby'
86
- @current_library = nil
87
- end
88
- when 'ruby'
89
- @mode = 'ruby'
90
- @current_library = nil
91
- else
92
- # 無効なモードはrubyにフォールバック
93
- @mode = 'ruby'
94
- @current_library = nil
95
- end
247
+ @mode = 'ruby'
248
+ @current_library = nil
96
249
  end
97
250
 
98
251
  # 現在のモード取得
99
252
  def mode
100
- # 初回アクセス時はautoモードに設定
101
- self.mode = 'auto' if @mode.nil?
102
- @mode
253
+ @mode ||= 'ruby'
103
254
  end
104
255
 
105
- # 利用可能なライブラリをチェック
256
+ # 利用可能なライブラリをチェック(Rubyのみ)
106
257
  def available_libraries
107
258
  {
108
- rust: RustLib.available?,
109
- go: GoLib.available?
259
+ ruby: true
110
260
  }
111
261
  end
112
262
 
113
263
  # ディレクトリをスキャン
114
264
  def scan_directory(path)
115
- # モードが未設定の場合は自動設定
116
- mode if @mode.nil?
117
-
118
- case @mode
119
- when 'rust'
120
- scan_with_rust(path)
121
- when 'go'
122
- scan_with_go(path)
123
- else
124
- scan_with_ruby(path)
125
- end
265
+ scan_with_ruby(path)
126
266
  end
127
267
 
128
268
  # 高速スキャン(エントリ数制限付き)
129
269
  def scan_directory_fast(path, max_entries = 1000)
130
- # モードが未設定の場合は自動設定
131
- mode if @mode.nil?
132
-
133
- case @mode
134
- when 'rust'
135
- scan_fast_with_rust(path, max_entries)
136
- when 'go'
137
- scan_fast_with_go(path, max_entries)
138
- else
139
- scan_fast_with_ruby(path, max_entries)
140
- end
270
+ scan_fast_with_ruby(path, max_entries)
141
271
  end
142
272
 
143
273
  # バージョン情報取得
144
274
  def version
145
- # モードが未設定の場合は自動設定
146
- mode if @mode.nil?
147
-
148
- case @mode
149
- when 'rust'
150
- ptr = RustLib.get_version
151
- ptr.read_string
152
- when 'go'
153
- ptr = GoLib.GetVersion
154
- result = ptr.read_string
155
- GoLib.FreeCString(ptr)
156
- result
157
- else
158
- "Ruby #{RUBY_VERSION}"
159
- end
275
+ "Ruby #{RUBY_VERSION}"
160
276
  end
161
277
 
162
278
  private
163
279
 
164
- # Rustライブラリでスキャン
165
- def scan_with_rust(path)
166
- raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
167
-
168
- ptr = RustLib.scan_directory(path)
169
- json_str = ptr.read_string
170
- parse_scan_result(json_str)
171
- rescue StandardError => e
172
- raise StandardError, "Rust scan failed: #{e.message}"
173
- end
174
-
175
- # Rustライブラリで高速スキャン
176
- def scan_fast_with_rust(path, max_entries)
177
- raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
178
-
179
- ptr = RustLib.scan_directory_fast(path, max_entries)
180
- json_str = ptr.read_string
181
- parse_scan_result(json_str)
182
- rescue StandardError => e
183
- raise StandardError, "Rust fast scan failed: #{e.message}"
184
- end
185
-
186
- # Goライブラリでスキャン
187
- def scan_with_go(path)
188
- raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
189
-
190
- ptr = GoLib.ScanDirectory(path)
191
- json_str = ptr.read_string
192
- GoLib.FreeCString(ptr)
193
- parse_scan_result(json_str)
194
- rescue StandardError => e
195
- raise StandardError, "Go scan failed: #{e.message}"
196
- end
197
-
198
- # Goライブラリで高速スキャン
199
- def scan_fast_with_go(path, max_entries)
200
- raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
201
-
202
- ptr = GoLib.ScanDirectoryFast(path, max_entries)
203
- json_str = ptr.read_string
204
- GoLib.FreeCString(ptr)
205
- parse_scan_result(json_str)
206
- rescue StandardError => e
207
- raise StandardError, "Go fast scan failed: #{e.message}"
208
- end
209
-
210
- # Rubyでスキャン(フォールバック実装)
280
+ # Rubyでスキャン(ポーリング方式)
211
281
  def scan_with_ruby(path)
212
282
  raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
213
283
 
214
- entries = []
215
- Dir.foreach(path) do |entry|
216
- next if entry == '.' || entry == '..'
217
-
218
- full_path = File.join(path, entry)
219
- stat = File.lstat(full_path)
220
-
221
- entries << {
222
- name: entry,
223
- type: file_type(stat),
224
- size: stat.size,
225
- mtime: stat.mtime.to_i,
226
- mode: stat.mode
227
- }
284
+ # 非同期スキャナーを作成してスキャン
285
+ scanner = NativeScannerRubyCore.new
286
+ begin
287
+ scanner.scan_async(path)
288
+ scanner.wait(timeout: 60)
289
+ ensure
290
+ scanner.close
228
291
  end
229
- entries
230
292
  rescue StandardError => e
231
293
  raise StandardError, "Ruby scan failed: #{e.message}"
232
294
  end
233
295
 
234
- # Ruby高速スキャン(エントリ数制限付き)
296
+ # Ruby高速スキャン(エントリ数制限付き、ポーリング方式)
235
297
  def scan_fast_with_ruby(path, max_entries)
236
298
  raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
237
299
 
238
- entries = []
239
- count = 0
240
-
241
- Dir.foreach(path) do |entry|
242
- next if entry == '.' || entry == '..'
243
- break if count >= max_entries
244
-
245
- full_path = File.join(path, entry)
246
- stat = File.lstat(full_path)
247
-
248
- entries << {
249
- name: entry,
250
- type: file_type(stat),
251
- size: stat.size,
252
- mtime: stat.mtime.to_i,
253
- mode: stat.mode
254
- }
255
- count += 1
300
+ scanner = NativeScannerRubyCore.new
301
+ begin
302
+ scanner.scan_fast_async(path, max_entries)
303
+ scanner.wait(timeout: 60)
304
+ ensure
305
+ scanner.close
256
306
  end
257
- entries
258
307
  rescue StandardError => e
259
308
  raise StandardError, "Ruby fast scan failed: #{e.message}"
260
309
  end
@@ -271,36 +320,6 @@ module Rufio
271
320
  'other'
272
321
  end
273
322
  end
274
-
275
- # JSONレスポンスをパース
276
- def parse_scan_result(json_str)
277
- entries = JSON.parse(json_str, symbolize_names: true)
278
-
279
- # エラーチェック(配列ではなくハッシュが返された場合)
280
- if entries.is_a?(Hash) && entries[:error]
281
- raise StandardError, entries[:error]
282
- end
283
-
284
- # 配列が返された場合は各エントリを変換
285
- if entries.is_a?(Array)
286
- return entries.map do |entry|
287
- {
288
- name: entry[:name],
289
- type: entry[:is_dir] ? 'directory' : 'file',
290
- size: entry[:size],
291
- mtime: entry[:mtime],
292
- mode: 0, # Rustライブラリはmodeを返さない
293
- executable: entry[:executable],
294
- hidden: entry[:hidden]
295
- }
296
- end
297
- end
298
-
299
- # それ以外の場合は空配列
300
- []
301
- rescue JSON::ParserError => e
302
- raise StandardError, "Failed to parse scan result: #{e.message}"
303
- end
304
323
  end
305
324
  end
306
325
  end