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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +63 -7
- data/README_EN.md +25 -0
- data/docs/CHANGELOG_v0.34.0.md +444 -0
- data/docs/file-preview-optimization-analysis.md +759 -0
- data/docs/file-preview-performance-issue-FIXED.md +547 -0
- data/lib/rufio/application.rb +9 -1
- data/lib/rufio/async_scanner_fiber.rb +154 -0
- data/lib/rufio/async_scanner_promise.rb +66 -0
- data/lib/rufio/background_command_executor.rb +98 -0
- data/lib/rufio/command_logger.rb +121 -0
- data/lib/rufio/command_mode.rb +17 -2
- data/lib/rufio/directory_listing.rb +60 -12
- data/lib/rufio/keybind_handler.rb +73 -2
- data/lib/rufio/native/rufio_zig.bundle +0 -0
- data/lib/rufio/native_scanner.rb +325 -0
- data/lib/rufio/native_scanner_zig.rb +354 -0
- data/lib/rufio/parallel_scanner.rb +173 -0
- data/lib/rufio/terminal_ui.rb +66 -16
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +7 -0
- data/lib_zig/rufio_native/Makefile +34 -0
- data/lib_zig/rufio_native/build.zig +45 -0
- data/lib_zig/rufio_native/src/main.zig +378 -0
- data/lib_zig/rufio_native/src/main.zig.sync +205 -0
- metadata +17 -2
|
@@ -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
|