solis 0.103.0 → 0.104.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/README.md +193 -0
- data/lib/solis/model.rb +7 -0
- data/lib/solis/overlay_fs.rb +1093 -0
- data/lib/solis/query/run.rb +3 -0
- data/lib/solis/rdf_edtf_literal.rb +143 -0
- data/lib/solis/shape/data_types.rb +87 -0
- data/lib/solis/shape/reader/sheet.rb +2 -0
- data/lib/solis/shape.rb +4 -1
- data/lib/solis/version.rb +1 -1
- data/lib/solis.rb +1 -0
- data/solis.gemspec +1 -1
- metadata +17 -1
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'monitor'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
require 'forwardable'
|
|
8
|
+
|
|
9
|
+
module Solis
|
|
10
|
+
class OverlayFS
|
|
11
|
+
WHITEOUT_PREFIX = '.wh.'
|
|
12
|
+
OPAQUE_MARKER = '.wh..wh..opq'
|
|
13
|
+
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class LayerError < Error; end
|
|
16
|
+
class ReadOnlyError < Error; end
|
|
17
|
+
|
|
18
|
+
# IO wrapper that handles copy-up on write
|
|
19
|
+
class OverlayIO
|
|
20
|
+
extend Forwardable
|
|
21
|
+
|
|
22
|
+
def_delegators :@io, :read, :gets, :getc, :getbyte, :readlines, :readline,
|
|
23
|
+
:each, :each_line, :each_byte, :each_char, :eof?, :eof,
|
|
24
|
+
:pos, :pos=, :seek, :rewind, :tell, :lineno, :lineno=,
|
|
25
|
+
:sync, :sync=, :binmode, :binmode?, :close_read,
|
|
26
|
+
:external_encoding, :internal_encoding, :set_encoding
|
|
27
|
+
|
|
28
|
+
def initialize(io, overlay_fs: nil, relative_path: nil, mode: 'r')
|
|
29
|
+
@io = io
|
|
30
|
+
@overlay_fs = overlay_fs
|
|
31
|
+
@relative_path = relative_path
|
|
32
|
+
@mode = mode
|
|
33
|
+
@copied_up = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def write(data)
|
|
37
|
+
ensure_copy_up if @overlay_fs && !@copied_up
|
|
38
|
+
@io.write(data)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def puts(*args)
|
|
42
|
+
ensure_copy_up if @overlay_fs && !@copied_up
|
|
43
|
+
@io.puts(*args)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print(*args)
|
|
47
|
+
ensure_copy_up if @overlay_fs && !@copied_up
|
|
48
|
+
@io.print(*args)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def printf(*args)
|
|
52
|
+
ensure_copy_up if @overlay_fs && !@copied_up
|
|
53
|
+
@io.printf(*args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def <<(data)
|
|
57
|
+
ensure_copy_up if @overlay_fs && !@copied_up
|
|
58
|
+
@io << data
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def flush
|
|
63
|
+
@io.flush
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def close
|
|
67
|
+
@io.close
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def closed?
|
|
71
|
+
@io.closed?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_write
|
|
75
|
+
@io.close_write
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def path
|
|
79
|
+
@io.respond_to?(:path) ? @io.path : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_io
|
|
83
|
+
@io
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def ensure_copy_up
|
|
89
|
+
return if @copied_up
|
|
90
|
+
return unless @overlay_fs && @relative_path
|
|
91
|
+
|
|
92
|
+
current_pos = @io.pos rescue 0
|
|
93
|
+
@io.close unless @io.closed?
|
|
94
|
+
|
|
95
|
+
new_path = @overlay_fs.copy_up(@relative_path)
|
|
96
|
+
@io = File.open(new_path, @mode)
|
|
97
|
+
@io.pos = current_pos
|
|
98
|
+
@copied_up = true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Stat wrapper for overlay files
|
|
103
|
+
class OverlayStat
|
|
104
|
+
attr_reader :layer, :real_path, :relative_path
|
|
105
|
+
|
|
106
|
+
def initialize(real_stat, layer:, real_path:, relative_path:)
|
|
107
|
+
@stat = real_stat
|
|
108
|
+
@layer = layer
|
|
109
|
+
@real_path = real_path
|
|
110
|
+
@relative_path = relative_path
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Delegate all stat methods
|
|
114
|
+
%i[
|
|
115
|
+
atime blksize blockdev? blocks chardev? ctime dev dev_major dev_minor
|
|
116
|
+
directory? executable? executable_real? file? ftype gid grpowned? ino
|
|
117
|
+
mode mtime nlink owned? pipe? rdev rdev_major rdev_minor readable?
|
|
118
|
+
readable_real? setgid? setuid? size size? socket? sticky? symlink?
|
|
119
|
+
uid world_readable? world_writable? writable? writable_real? zero?
|
|
120
|
+
].each do |method|
|
|
121
|
+
define_method(method) { @stat.send(method) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_s
|
|
125
|
+
"#<OverlayStat #{@relative_path} (#{@layer})>"
|
|
126
|
+
end
|
|
127
|
+
alias inspect to_s
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Directory entry for enumeration
|
|
131
|
+
Entry = Struct.new(:name, :path, :relative_path, :layer, :type, keyword_init: true) do
|
|
132
|
+
def file?
|
|
133
|
+
type == :file
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def directory?
|
|
137
|
+
type == :directory
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def symlink?
|
|
141
|
+
type == :symlink
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Event hooks
|
|
146
|
+
class Hooks
|
|
147
|
+
def initialize
|
|
148
|
+
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def on(event, &block)
|
|
152
|
+
@callbacks[event] << block
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def trigger(event, **args)
|
|
156
|
+
@callbacks[event].each { |cb| cb.call(**args) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def clear(event = nil)
|
|
160
|
+
event ? @callbacks.delete(event) : @callbacks.clear
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
attr_reader :layers, :hooks
|
|
165
|
+
|
|
166
|
+
def initialize(cache: true)
|
|
167
|
+
@layers = []
|
|
168
|
+
@cache_enabled = cache
|
|
169
|
+
@resolve_cache = {}
|
|
170
|
+
@stat_cache = {}
|
|
171
|
+
@monitor = Monitor.new
|
|
172
|
+
@hooks = Hooks.new
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# --- Layer Management ---
|
|
176
|
+
|
|
177
|
+
def add_layer(path, writable: false, label: nil)
|
|
178
|
+
@monitor.synchronize do
|
|
179
|
+
path = Pathname.new(path).expand_path
|
|
180
|
+
|
|
181
|
+
layer = {
|
|
182
|
+
path: path,
|
|
183
|
+
writable: writable,
|
|
184
|
+
label: label || path.basename.to_s
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
writable ? @layers.unshift(layer) : @layers.push(layer)
|
|
188
|
+
clear_cache
|
|
189
|
+
end
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def remove_layer(label_or_path)
|
|
194
|
+
@monitor.synchronize do
|
|
195
|
+
@layers.reject! do |layer|
|
|
196
|
+
layer[:label] == label_or_path || layer[:path].to_s == label_or_path.to_s
|
|
197
|
+
end
|
|
198
|
+
clear_cache
|
|
199
|
+
end
|
|
200
|
+
self
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def writable_layer
|
|
204
|
+
@layers.find { |l| l[:writable] }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def readonly_layers
|
|
208
|
+
@layers.reject { |l| l[:writable] }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# --- Path Resolution ---
|
|
212
|
+
|
|
213
|
+
def resolve(relative_path)
|
|
214
|
+
relative_path = normalize_path(relative_path)
|
|
215
|
+
|
|
216
|
+
return @resolve_cache[relative_path] if @cache_enabled && @resolve_cache.key?(relative_path)
|
|
217
|
+
|
|
218
|
+
result = @monitor.synchronize do
|
|
219
|
+
@layers.each do |layer|
|
|
220
|
+
# Check for whiteout first
|
|
221
|
+
whiteout_path = layer[:path] / whiteout_name(relative_path)
|
|
222
|
+
return nil if whiteout_path.exist?
|
|
223
|
+
|
|
224
|
+
# Check for opaque parent directory
|
|
225
|
+
return nil if opaque_parent?(layer, relative_path)
|
|
226
|
+
|
|
227
|
+
full_path = layer[:path] / relative_path
|
|
228
|
+
return full_path if full_path.exist? || full_path.symlink?
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
@resolve_cache[relative_path] = result if @cache_enabled
|
|
234
|
+
result
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def writable_path(relative_path)
|
|
238
|
+
layer = writable_layer
|
|
239
|
+
raise ReadOnlyError, "No writable layer configured" unless layer
|
|
240
|
+
layer[:path] / normalize_path(relative_path)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def real_path(relative_path)
|
|
244
|
+
resolve(relative_path)&.realpath
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# --- File Operations ---
|
|
248
|
+
|
|
249
|
+
def read(relative_path, **options)
|
|
250
|
+
path = resolve(relative_path)
|
|
251
|
+
raise Errno::ENOENT, relative_path unless path
|
|
252
|
+
hooks.trigger(:before_read, path: relative_path)
|
|
253
|
+
content = File.read(path, **options)
|
|
254
|
+
hooks.trigger(:after_read, path: relative_path, content: content)
|
|
255
|
+
content
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def read_binary(relative_path)
|
|
259
|
+
read(relative_path, mode: 'rb')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def write(relative_path, content, **options)
|
|
263
|
+
relative_path = normalize_path(relative_path)
|
|
264
|
+
hooks.trigger(:before_write, path: relative_path, content: content)
|
|
265
|
+
|
|
266
|
+
path = writable_path(relative_path)
|
|
267
|
+
ensure_directory(path.dirname)
|
|
268
|
+
remove_whiteout(relative_path)
|
|
269
|
+
|
|
270
|
+
File.write(path, content, **options)
|
|
271
|
+
clear_cache_for(relative_path)
|
|
272
|
+
|
|
273
|
+
hooks.trigger(:after_write, path: relative_path, real_path: path)
|
|
274
|
+
path
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def write_binary(relative_path, content)
|
|
278
|
+
write(relative_path, content, mode: 'wb')
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def append(relative_path, content)
|
|
282
|
+
if exist?(relative_path)
|
|
283
|
+
copy_up(relative_path)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
path = writable_path(relative_path)
|
|
287
|
+
ensure_directory(path.dirname)
|
|
288
|
+
File.open(path, 'a') { |f| f.write(content) }
|
|
289
|
+
clear_cache_for(relative_path)
|
|
290
|
+
path
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def atomic_write(relative_path, content, **options)
|
|
294
|
+
relative_path = normalize_path(relative_path)
|
|
295
|
+
path = writable_path(relative_path)
|
|
296
|
+
ensure_directory(path.dirname)
|
|
297
|
+
|
|
298
|
+
temp_path = "#{path}.tmp.#{Process.pid}.#{Thread.current.object_id}"
|
|
299
|
+
begin
|
|
300
|
+
File.write(temp_path, content, **options)
|
|
301
|
+
File.rename(temp_path, path)
|
|
302
|
+
remove_whiteout(relative_path)
|
|
303
|
+
clear_cache_for(relative_path)
|
|
304
|
+
hooks.trigger(:after_write, path: relative_path, real_path: path)
|
|
305
|
+
path
|
|
306
|
+
rescue
|
|
307
|
+
File.unlink(temp_path) if File.exist?(temp_path)
|
|
308
|
+
raise
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def open(relative_path, mode = 'r', **options, &block)
|
|
313
|
+
relative_path = normalize_path(relative_path)
|
|
314
|
+
writing = mode_writable?(mode)
|
|
315
|
+
|
|
316
|
+
if writing
|
|
317
|
+
if exist?(relative_path) && !in_writable_layer?(relative_path)
|
|
318
|
+
# Copy-up needed, but defer until actual write
|
|
319
|
+
path = resolve(relative_path)
|
|
320
|
+
io = OverlayIO.new(
|
|
321
|
+
File.open(path, readable_mode(mode), **options),
|
|
322
|
+
overlay_fs: self,
|
|
323
|
+
relative_path: relative_path,
|
|
324
|
+
mode: mode
|
|
325
|
+
)
|
|
326
|
+
else
|
|
327
|
+
path = writable_path(relative_path)
|
|
328
|
+
ensure_directory(path.dirname)
|
|
329
|
+
remove_whiteout(relative_path)
|
|
330
|
+
io = OverlayIO.new(File.open(path, mode, **options))
|
|
331
|
+
end
|
|
332
|
+
else
|
|
333
|
+
path = resolve(relative_path)
|
|
334
|
+
raise Errno::ENOENT, relative_path unless path
|
|
335
|
+
io = OverlayIO.new(File.open(path, mode, **options))
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
if block_given?
|
|
339
|
+
begin
|
|
340
|
+
yield io
|
|
341
|
+
ensure
|
|
342
|
+
io.close unless io.closed?
|
|
343
|
+
clear_cache_for(relative_path) if writing
|
|
344
|
+
end
|
|
345
|
+
else
|
|
346
|
+
io
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# --- Existence & Type Checks ---
|
|
351
|
+
|
|
352
|
+
def exist?(relative_path)
|
|
353
|
+
!resolve(relative_path).nil?
|
|
354
|
+
end
|
|
355
|
+
alias exists? exist?
|
|
356
|
+
|
|
357
|
+
def file?(relative_path)
|
|
358
|
+
path = resolve(relative_path)
|
|
359
|
+
path&.file?
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def directory?(relative_path)
|
|
363
|
+
relative_path = normalize_path(relative_path)
|
|
364
|
+
|
|
365
|
+
@layers.any? do |layer|
|
|
366
|
+
whiteout = layer[:path] / whiteout_name(relative_path)
|
|
367
|
+
next false if whiteout.exist?
|
|
368
|
+
|
|
369
|
+
dir_path = layer[:path] / relative_path
|
|
370
|
+
dir_path.directory?
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def symlink?(relative_path)
|
|
375
|
+
path = resolve(relative_path)
|
|
376
|
+
path&.symlink?
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def readable?(relative_path)
|
|
380
|
+
path = resolve(relative_path)
|
|
381
|
+
path&.readable?
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def writable?(relative_path)
|
|
385
|
+
return false unless writable_layer
|
|
386
|
+
|
|
387
|
+
if exist?(relative_path)
|
|
388
|
+
in_writable_layer?(relative_path) || copy_up_possible?(relative_path)
|
|
389
|
+
else
|
|
390
|
+
true # Can create new file
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def executable?(relative_path)
|
|
395
|
+
path = resolve(relative_path)
|
|
396
|
+
path&.executable?
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def empty?(relative_path)
|
|
400
|
+
if directory?(relative_path)
|
|
401
|
+
entries(relative_path).empty?
|
|
402
|
+
elsif file?(relative_path)
|
|
403
|
+
size(relative_path) == 0
|
|
404
|
+
else
|
|
405
|
+
raise Errno::ENOENT, relative_path
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# --- File Info ---
|
|
410
|
+
|
|
411
|
+
def stat(relative_path)
|
|
412
|
+
relative_path = normalize_path(relative_path)
|
|
413
|
+
|
|
414
|
+
return @stat_cache[relative_path] if @cache_enabled && @stat_cache.key?(relative_path)
|
|
415
|
+
|
|
416
|
+
@layers.each do |layer|
|
|
417
|
+
whiteout = layer[:path] / whiteout_name(relative_path)
|
|
418
|
+
return nil if whiteout.exist?
|
|
419
|
+
|
|
420
|
+
full_path = layer[:path] / relative_path
|
|
421
|
+
if full_path.exist? || full_path.symlink?
|
|
422
|
+
stat = OverlayStat.new(
|
|
423
|
+
full_path.stat,
|
|
424
|
+
layer: layer[:label],
|
|
425
|
+
real_path: full_path,
|
|
426
|
+
relative_path: relative_path
|
|
427
|
+
)
|
|
428
|
+
@stat_cache[relative_path] = stat if @cache_enabled
|
|
429
|
+
return stat
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
nil
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def lstat(relative_path)
|
|
436
|
+
relative_path = normalize_path(relative_path)
|
|
437
|
+
|
|
438
|
+
@layers.each do |layer|
|
|
439
|
+
whiteout = layer[:path] / whiteout_name(relative_path)
|
|
440
|
+
return nil if whiteout.exist?
|
|
441
|
+
|
|
442
|
+
full_path = layer[:path] / relative_path
|
|
443
|
+
if full_path.exist? || full_path.symlink?
|
|
444
|
+
return OverlayStat.new(
|
|
445
|
+
full_path.lstat,
|
|
446
|
+
layer: layer[:label],
|
|
447
|
+
real_path: full_path,
|
|
448
|
+
relative_path: relative_path
|
|
449
|
+
)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
nil
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def size(relative_path)
|
|
456
|
+
stat(relative_path)&.size || raise(Errno::ENOENT, relative_path)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def mtime(relative_path)
|
|
460
|
+
stat(relative_path)&.mtime || raise(Errno::ENOENT, relative_path)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def atime(relative_path)
|
|
464
|
+
stat(relative_path)&.atime || raise(Errno::ENOENT, relative_path)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def ctime(relative_path)
|
|
468
|
+
stat(relative_path)&.ctime || raise(Errno::ENOENT, relative_path)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def ftype(relative_path)
|
|
472
|
+
path = resolve(relative_path)
|
|
473
|
+
raise Errno::ENOENT, relative_path unless path
|
|
474
|
+
path.ftype
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def extname(relative_path)
|
|
478
|
+
File.extname(relative_path)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def basename(relative_path, suffix = nil)
|
|
482
|
+
suffix ? File.basename(relative_path, suffix) : File.basename(relative_path)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def dirname(relative_path)
|
|
486
|
+
File.dirname(relative_path)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def checksum(relative_path, algorithm: :sha256)
|
|
490
|
+
content = read_binary(relative_path)
|
|
491
|
+
case algorithm
|
|
492
|
+
when :md5 then Digest::MD5.hexdigest(content)
|
|
493
|
+
when :sha1 then Digest::SHA1.hexdigest(content)
|
|
494
|
+
when :sha256 then Digest::SHA256.hexdigest(content)
|
|
495
|
+
when :sha512 then Digest::SHA512.hexdigest(content)
|
|
496
|
+
else raise ArgumentError, "Unknown algorithm: #{algorithm}"
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# --- Directory Operations ---
|
|
501
|
+
|
|
502
|
+
def mkdir(relative_path, mode: 0755)
|
|
503
|
+
relative_path = normalize_path(relative_path)
|
|
504
|
+
path = writable_path(relative_path)
|
|
505
|
+
|
|
506
|
+
raise Errno::EEXIST, relative_path if directory?(relative_path)
|
|
507
|
+
|
|
508
|
+
remove_whiteout(relative_path)
|
|
509
|
+
FileUtils.mkdir_p(path, mode: mode)
|
|
510
|
+
clear_cache_for(relative_path)
|
|
511
|
+
hooks.trigger(:after_mkdir, path: relative_path)
|
|
512
|
+
path
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def mkdir_p(relative_path, mode: 0755)
|
|
516
|
+
relative_path = normalize_path(relative_path)
|
|
517
|
+
path = writable_path(relative_path)
|
|
518
|
+
|
|
519
|
+
remove_whiteout(relative_path)
|
|
520
|
+
FileUtils.mkdir_p(path, mode: mode)
|
|
521
|
+
clear_cache_for(relative_path)
|
|
522
|
+
path
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def rmdir(relative_path)
|
|
526
|
+
relative_path = normalize_path(relative_path)
|
|
527
|
+
|
|
528
|
+
raise Errno::ENOENT, relative_path unless directory?(relative_path)
|
|
529
|
+
raise Errno::ENOTEMPTY, relative_path unless empty?(relative_path)
|
|
530
|
+
|
|
531
|
+
if in_writable_layer?(relative_path)
|
|
532
|
+
FileUtils.rmdir(writable_path(relative_path))
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# If exists in lower layers, create whiteout
|
|
536
|
+
if exists_in_lower_layers?(relative_path)
|
|
537
|
+
create_whiteout(relative_path)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
clear_cache_for(relative_path)
|
|
541
|
+
hooks.trigger(:after_rmdir, path: relative_path)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def entries(relative_path = '.')
|
|
545
|
+
relative_path = normalize_path(relative_path)
|
|
546
|
+
seen = Set.new
|
|
547
|
+
whiteouts = Set.new
|
|
548
|
+
result = []
|
|
549
|
+
|
|
550
|
+
@layers.each do |layer|
|
|
551
|
+
dir_path = layer[:path] / relative_path
|
|
552
|
+
next unless dir_path.directory?
|
|
553
|
+
|
|
554
|
+
# Check if this directory is opaque - if so, don't descend to lower layers
|
|
555
|
+
opaque = (dir_path / OPAQUE_MARKER).exist?
|
|
556
|
+
|
|
557
|
+
dir_path.children.each do |child|
|
|
558
|
+
name = child.basename.to_s
|
|
559
|
+
|
|
560
|
+
# Track whiteouts
|
|
561
|
+
if name.start_with?(WHITEOUT_PREFIX)
|
|
562
|
+
if name == OPAQUE_MARKER
|
|
563
|
+
next
|
|
564
|
+
else
|
|
565
|
+
whiteouts << name.sub(WHITEOUT_PREFIX, '')
|
|
566
|
+
next
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
next if seen.include?(name)
|
|
571
|
+
next if whiteouts.include?(name)
|
|
572
|
+
|
|
573
|
+
seen << name
|
|
574
|
+
result << Entry.new(
|
|
575
|
+
name: name,
|
|
576
|
+
path: child,
|
|
577
|
+
relative_path: "#{relative_path}/#{name}".sub(%r{^\.?/}, ''),
|
|
578
|
+
layer: layer[:label],
|
|
579
|
+
type: entry_type(child)
|
|
580
|
+
)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
break if opaque
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
result.sort_by(&:name)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def children(relative_path = '.')
|
|
590
|
+
entries(relative_path).map(&:name)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def glob(pattern, flags: 0)
|
|
594
|
+
pattern = normalize_path(pattern)
|
|
595
|
+
seen = Set.new
|
|
596
|
+
whiteouts = collect_whiteouts
|
|
597
|
+
results = []
|
|
598
|
+
|
|
599
|
+
@layers.each do |layer|
|
|
600
|
+
Dir.glob(layer[:path] / pattern, flags).each do |full_path|
|
|
601
|
+
relative = Pathname.new(full_path).relative_path_from(layer[:path]).to_s
|
|
602
|
+
|
|
603
|
+
next if seen.include?(relative)
|
|
604
|
+
next if whiteouts.include?(relative)
|
|
605
|
+
next if File.basename(relative).start_with?(WHITEOUT_PREFIX)
|
|
606
|
+
|
|
607
|
+
seen << relative
|
|
608
|
+
results << relative
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
results.sort
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def find(relative_path = '.', &block)
|
|
616
|
+
results = []
|
|
617
|
+
_find_recursive(normalize_path(relative_path), results, &block)
|
|
618
|
+
results
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def each_file(pattern = '**/*', &block)
|
|
622
|
+
return enum_for(:each_file, pattern) unless block_given?
|
|
623
|
+
|
|
624
|
+
glob(pattern).each do |relative_path|
|
|
625
|
+
next unless file?(relative_path)
|
|
626
|
+
yield relative_path, resolve(relative_path)
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def each_directory(relative_path = '.', &block)
|
|
631
|
+
return enum_for(:each_directory, relative_path) unless block_given?
|
|
632
|
+
|
|
633
|
+
entries(relative_path).each do |entry|
|
|
634
|
+
yield entry if entry.directory?
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# --- File Manipulation ---
|
|
639
|
+
|
|
640
|
+
def copy_up(relative_path)
|
|
641
|
+
relative_path = normalize_path(relative_path)
|
|
642
|
+
|
|
643
|
+
return writable_path(relative_path) if in_writable_layer?(relative_path)
|
|
644
|
+
|
|
645
|
+
source = resolve(relative_path)
|
|
646
|
+
raise Errno::ENOENT, relative_path unless source
|
|
647
|
+
|
|
648
|
+
dest = writable_path(relative_path)
|
|
649
|
+
ensure_directory(dest.dirname)
|
|
650
|
+
|
|
651
|
+
if source.directory?
|
|
652
|
+
FileUtils.mkdir_p(dest)
|
|
653
|
+
# Copy directory metadata
|
|
654
|
+
FileUtils.chmod(source.stat.mode, dest)
|
|
655
|
+
elsif source.symlink?
|
|
656
|
+
FileUtils.ln_s(File.readlink(source), dest)
|
|
657
|
+
else
|
|
658
|
+
FileUtils.cp(source, dest, preserve: true)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
clear_cache_for(relative_path)
|
|
662
|
+
hooks.trigger(:after_copy_up, path: relative_path, from: source, to: dest)
|
|
663
|
+
dest
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def copy(source, destination, preserve: true)
|
|
667
|
+
source = normalize_path(source)
|
|
668
|
+
destination = normalize_path(destination)
|
|
669
|
+
|
|
670
|
+
source_path = resolve(source)
|
|
671
|
+
raise Errno::ENOENT, source unless source_path
|
|
672
|
+
|
|
673
|
+
dest_path = writable_path(destination)
|
|
674
|
+
ensure_directory(dest_path.dirname)
|
|
675
|
+
remove_whiteout(destination)
|
|
676
|
+
|
|
677
|
+
if source_path.directory?
|
|
678
|
+
copy_directory(source, destination, preserve: preserve)
|
|
679
|
+
else
|
|
680
|
+
FileUtils.cp(source_path, dest_path, preserve: preserve)
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
clear_cache_for(destination)
|
|
684
|
+
dest_path
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def move(source, destination)
|
|
688
|
+
source = normalize_path(source)
|
|
689
|
+
destination = normalize_path(destination)
|
|
690
|
+
|
|
691
|
+
copy(source, destination, preserve: true)
|
|
692
|
+
delete(source)
|
|
693
|
+
|
|
694
|
+
writable_path(destination)
|
|
695
|
+
end
|
|
696
|
+
alias rename move
|
|
697
|
+
|
|
698
|
+
def delete(relative_path, force: false)
|
|
699
|
+
relative_path = normalize_path(relative_path)
|
|
700
|
+
|
|
701
|
+
unless exist?(relative_path)
|
|
702
|
+
raise Errno::ENOENT, relative_path unless force
|
|
703
|
+
return
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
hooks.trigger(:before_delete, path: relative_path)
|
|
707
|
+
|
|
708
|
+
if in_writable_layer?(relative_path)
|
|
709
|
+
full_path = writable_path(relative_path)
|
|
710
|
+
if full_path.directory?
|
|
711
|
+
FileUtils.rm_rf(full_path)
|
|
712
|
+
else
|
|
713
|
+
FileUtils.rm(full_path)
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Create whiteout if exists in lower layers
|
|
718
|
+
if exists_in_lower_layers?(relative_path)
|
|
719
|
+
create_whiteout(relative_path)
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
clear_cache_for(relative_path)
|
|
723
|
+
hooks.trigger(:after_delete, path: relative_path)
|
|
724
|
+
end
|
|
725
|
+
alias rm delete
|
|
726
|
+
alias unlink delete
|
|
727
|
+
|
|
728
|
+
def delete_recursive(relative_path)
|
|
729
|
+
relative_path = normalize_path(relative_path)
|
|
730
|
+
|
|
731
|
+
if directory?(relative_path)
|
|
732
|
+
entries(relative_path).each do |entry|
|
|
733
|
+
delete_recursive(entry.relative_path)
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
delete(relative_path)
|
|
738
|
+
end
|
|
739
|
+
alias rm_rf delete_recursive
|
|
740
|
+
|
|
741
|
+
# --- Symlinks ---
|
|
742
|
+
|
|
743
|
+
def symlink(target, link_path)
|
|
744
|
+
link_path = normalize_path(link_path)
|
|
745
|
+
path = writable_path(link_path)
|
|
746
|
+
|
|
747
|
+
ensure_directory(path.dirname)
|
|
748
|
+
remove_whiteout(link_path)
|
|
749
|
+
|
|
750
|
+
FileUtils.ln_s(target, path)
|
|
751
|
+
clear_cache_for(link_path)
|
|
752
|
+
path
|
|
753
|
+
end
|
|
754
|
+
alias ln_s symlink
|
|
755
|
+
|
|
756
|
+
def readlink(relative_path)
|
|
757
|
+
path = resolve(relative_path)
|
|
758
|
+
raise Errno::ENOENT, relative_path unless path
|
|
759
|
+
raise Errno::EINVAL, relative_path unless path.symlink?
|
|
760
|
+
File.readlink(path)
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def realpath(relative_path)
|
|
764
|
+
path = resolve(relative_path)
|
|
765
|
+
raise Errno::ENOENT, relative_path unless path
|
|
766
|
+
path.realpath
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# --- Whiteouts & Opaque Directories ---
|
|
770
|
+
|
|
771
|
+
def whiteout?(relative_path)
|
|
772
|
+
relative_path = normalize_path(relative_path)
|
|
773
|
+
|
|
774
|
+
@layers.any? do |layer|
|
|
775
|
+
(layer[:path] / whiteout_name(relative_path)).exist?
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def create_whiteout(relative_path)
|
|
780
|
+
relative_path = normalize_path(relative_path)
|
|
781
|
+
whiteout = writable_path(whiteout_name(relative_path))
|
|
782
|
+
ensure_directory(whiteout.dirname)
|
|
783
|
+
FileUtils.touch(whiteout)
|
|
784
|
+
clear_cache_for(relative_path)
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def remove_whiteout(relative_path)
|
|
788
|
+
relative_path = normalize_path(relative_path)
|
|
789
|
+
whiteout = writable_path(whiteout_name(relative_path))
|
|
790
|
+
FileUtils.rm(whiteout) if whiteout.exist?
|
|
791
|
+
clear_cache_for(relative_path)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def make_opaque(relative_path)
|
|
795
|
+
relative_path = normalize_path(relative_path)
|
|
796
|
+
dir_path = writable_path(relative_path)
|
|
797
|
+
|
|
798
|
+
ensure_directory(dir_path)
|
|
799
|
+
FileUtils.touch(dir_path / OPAQUE_MARKER)
|
|
800
|
+
clear_cache_for(relative_path)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def remove_opaque(relative_path)
|
|
804
|
+
relative_path = normalize_path(relative_path)
|
|
805
|
+
opaque = writable_path(relative_path) / OPAQUE_MARKER
|
|
806
|
+
FileUtils.rm(opaque) if opaque.exist?
|
|
807
|
+
clear_cache_for(relative_path)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def opaque?(relative_path)
|
|
811
|
+
relative_path = normalize_path(relative_path)
|
|
812
|
+
|
|
813
|
+
@layers.any? do |layer|
|
|
814
|
+
(layer[:path] / relative_path / OPAQUE_MARKER).exist?
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# --- Permissions ---
|
|
819
|
+
|
|
820
|
+
def chmod(mode, relative_path)
|
|
821
|
+
relative_path = normalize_path(relative_path)
|
|
822
|
+
copy_up(relative_path) unless in_writable_layer?(relative_path)
|
|
823
|
+
FileUtils.chmod(mode, writable_path(relative_path))
|
|
824
|
+
clear_cache_for(relative_path)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def chown(user, group, relative_path)
|
|
828
|
+
relative_path = normalize_path(relative_path)
|
|
829
|
+
copy_up(relative_path) unless in_writable_layer?(relative_path)
|
|
830
|
+
FileUtils.chown(user, group, writable_path(relative_path))
|
|
831
|
+
clear_cache_for(relative_path)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def touch(relative_path, mtime: nil)
|
|
835
|
+
relative_path = normalize_path(relative_path)
|
|
836
|
+
|
|
837
|
+
if exist?(relative_path)
|
|
838
|
+
copy_up(relative_path) unless in_writable_layer?(relative_path)
|
|
839
|
+
path = writable_path(relative_path)
|
|
840
|
+
else
|
|
841
|
+
path = writable_path(relative_path)
|
|
842
|
+
ensure_directory(path.dirname)
|
|
843
|
+
remove_whiteout(relative_path)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
if mtime
|
|
847
|
+
FileUtils.touch(path, mtime: mtime)
|
|
848
|
+
else
|
|
849
|
+
FileUtils.touch(path)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
clear_cache_for(relative_path)
|
|
853
|
+
path
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# --- Comparison ---
|
|
857
|
+
|
|
858
|
+
def identical?(path1, path2)
|
|
859
|
+
resolved1 = resolve(path1)
|
|
860
|
+
resolved2 = resolve(path2)
|
|
861
|
+
|
|
862
|
+
return false unless resolved1 && resolved2
|
|
863
|
+
FileUtils.identical?(resolved1, resolved2)
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def diff(relative_path)
|
|
867
|
+
relative_path = normalize_path(relative_path)
|
|
868
|
+
versions = []
|
|
869
|
+
|
|
870
|
+
@layers.each do |layer|
|
|
871
|
+
full_path = layer[:path] / relative_path
|
|
872
|
+
if full_path.exist? && full_path.file?
|
|
873
|
+
versions << {
|
|
874
|
+
layer: layer[:label],
|
|
875
|
+
path: full_path,
|
|
876
|
+
content: full_path.read,
|
|
877
|
+
mtime: full_path.mtime,
|
|
878
|
+
size: full_path.size
|
|
879
|
+
}
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
versions
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
# --- Cache Management ---
|
|
887
|
+
|
|
888
|
+
def clear_cache
|
|
889
|
+
@monitor.synchronize do
|
|
890
|
+
@resolve_cache.clear
|
|
891
|
+
@stat_cache.clear
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def clear_cache_for(relative_path)
|
|
896
|
+
@monitor.synchronize do
|
|
897
|
+
relative_path = normalize_path(relative_path)
|
|
898
|
+
@resolve_cache.delete(relative_path)
|
|
899
|
+
@stat_cache.delete(relative_path)
|
|
900
|
+
|
|
901
|
+
# Also clear parent directories
|
|
902
|
+
parts = relative_path.split('/')
|
|
903
|
+
parts.length.times do |i|
|
|
904
|
+
parent = parts[0...i].join('/')
|
|
905
|
+
@resolve_cache.delete(parent)
|
|
906
|
+
@stat_cache.delete(parent)
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def cache_stats
|
|
912
|
+
{
|
|
913
|
+
resolve_cache_size: @resolve_cache.size,
|
|
914
|
+
stat_cache_size: @stat_cache.size,
|
|
915
|
+
cache_enabled: @cache_enabled
|
|
916
|
+
}
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# --- Layer Inspection ---
|
|
920
|
+
|
|
921
|
+
def which_layer(relative_path)
|
|
922
|
+
relative_path = normalize_path(relative_path)
|
|
923
|
+
|
|
924
|
+
@layers.each do |layer|
|
|
925
|
+
whiteout = layer[:path] / whiteout_name(relative_path)
|
|
926
|
+
return nil if whiteout.exist?
|
|
927
|
+
|
|
928
|
+
full_path = layer[:path] / relative_path
|
|
929
|
+
return layer[:label] if full_path.exist?
|
|
930
|
+
end
|
|
931
|
+
nil
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def in_writable_layer?(relative_path)
|
|
935
|
+
layer = writable_layer
|
|
936
|
+
return false unless layer
|
|
937
|
+
(layer[:path] / normalize_path(relative_path)).exist?
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def exists_in_lower_layers?(relative_path)
|
|
941
|
+
relative_path = normalize_path(relative_path)
|
|
942
|
+
|
|
943
|
+
readonly_layers.any? do |layer|
|
|
944
|
+
(layer[:path] / relative_path).exist?
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def all_versions(relative_path)
|
|
949
|
+
relative_path = normalize_path(relative_path)
|
|
950
|
+
|
|
951
|
+
@layers.filter_map do |layer|
|
|
952
|
+
full_path = layer[:path] / relative_path
|
|
953
|
+
next unless full_path.exist?
|
|
954
|
+
|
|
955
|
+
{
|
|
956
|
+
layer: layer[:label],
|
|
957
|
+
path: full_path,
|
|
958
|
+
writable: layer[:writable]
|
|
959
|
+
}
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# --- Utility ---
|
|
964
|
+
|
|
965
|
+
def to_s
|
|
966
|
+
layers_desc = @layers.map { |l| "#{l[:label]}#{l[:writable] ? ' (rw)' : ''}" }
|
|
967
|
+
"#<OverlayFS layers=[#{layers_desc.join(' -> ')}]>"
|
|
968
|
+
end
|
|
969
|
+
alias inspect to_s
|
|
970
|
+
|
|
971
|
+
def [](relative_path)
|
|
972
|
+
resolve(relative_path)
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def tree(relative_path = '.', depth: nil, prefix: '')
|
|
976
|
+
output = []
|
|
977
|
+
_tree_recursive(normalize_path(relative_path), output, depth, prefix, 0)
|
|
978
|
+
output.join("\n")
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
private
|
|
982
|
+
|
|
983
|
+
def normalize_path(path)
|
|
984
|
+
path.to_s.sub(%r{^\.?/}, '').sub(%r{/$}, '')
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
def whiteout_name(relative_path)
|
|
988
|
+
dir = File.dirname(relative_path)
|
|
989
|
+
name = File.basename(relative_path)
|
|
990
|
+
dir == '.' ? "#{WHITEOUT_PREFIX}#{name}" : "#{dir}/#{WHITEOUT_PREFIX}#{name}"
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def ensure_directory(path)
|
|
994
|
+
FileUtils.mkdir_p(path) unless path.exist?
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
def entry_type(path)
|
|
998
|
+
if path.symlink?
|
|
999
|
+
:symlink
|
|
1000
|
+
elsif path.directory?
|
|
1001
|
+
:directory
|
|
1002
|
+
else
|
|
1003
|
+
:file
|
|
1004
|
+
end
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def mode_writable?(mode)
|
|
1008
|
+
mode.include?('w') || mode.include?('a') || mode.include?('+')
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def readable_mode(mode)
|
|
1012
|
+
mode.gsub(/[wa+]/, 'r').gsub(/rr+/, 'r')
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def opaque_parent?(layer, relative_path)
|
|
1016
|
+
parts = relative_path.split('/')
|
|
1017
|
+
parts[0...-1].each_with_index do |_, i|
|
|
1018
|
+
parent = parts[0..i].join('/')
|
|
1019
|
+
return true if (layer[:path] / parent / OPAQUE_MARKER).exist?
|
|
1020
|
+
end
|
|
1021
|
+
false
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
def collect_whiteouts
|
|
1025
|
+
whiteouts = Set.new
|
|
1026
|
+
|
|
1027
|
+
@layers.each do |layer|
|
|
1028
|
+
Dir.glob(layer[:path] / '**' / "#{WHITEOUT_PREFIX}*").each do |path|
|
|
1029
|
+
name = File.basename(path)
|
|
1030
|
+
next if name == OPAQUE_MARKER
|
|
1031
|
+
|
|
1032
|
+
relative_dir = Pathname.new(path).dirname.relative_path_from(layer[:path]).to_s
|
|
1033
|
+
original_name = name.sub(WHITEOUT_PREFIX, '')
|
|
1034
|
+
|
|
1035
|
+
whiteouts << (relative_dir == '.' ? original_name : "#{relative_dir}/#{original_name}")
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
whiteouts
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
def copy_up_possible?(relative_path)
|
|
1043
|
+
resolve(relative_path) && writable_layer
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
def copy_directory(source, destination, preserve: true)
|
|
1047
|
+
mkdir_p(destination)
|
|
1048
|
+
|
|
1049
|
+
entries(source).each do |entry|
|
|
1050
|
+
src_path = "#{source}/#{entry.name}"
|
|
1051
|
+
dst_path = "#{destination}/#{entry.name}"
|
|
1052
|
+
|
|
1053
|
+
if entry.directory?
|
|
1054
|
+
copy_directory(src_path, dst_path, preserve: preserve)
|
|
1055
|
+
else
|
|
1056
|
+
copy(src_path, dst_path, preserve: preserve)
|
|
1057
|
+
end
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def _find_recursive(relative_path, results, &block)
|
|
1062
|
+
entries(relative_path).each do |entry|
|
|
1063
|
+
if block_given?
|
|
1064
|
+
next unless yield(entry)
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
results << entry.relative_path
|
|
1068
|
+
|
|
1069
|
+
if entry.directory?
|
|
1070
|
+
_find_recursive(entry.relative_path, results, &block)
|
|
1071
|
+
end
|
|
1072
|
+
end
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
def _tree_recursive(relative_path, output, max_depth, prefix, current_depth)
|
|
1076
|
+
return if max_depth && current_depth >= max_depth
|
|
1077
|
+
|
|
1078
|
+
items = entries(relative_path)
|
|
1079
|
+
items.each_with_index do |entry, index|
|
|
1080
|
+
is_last = index == items.length - 1
|
|
1081
|
+
connector = is_last ? '└── ' : '├── '
|
|
1082
|
+
layer_info = " (#{entry.layer})"
|
|
1083
|
+
|
|
1084
|
+
output << "#{prefix}#{connector}#{entry.name}#{layer_info}"
|
|
1085
|
+
|
|
1086
|
+
if entry.directory?
|
|
1087
|
+
next_prefix = prefix + (is_last ? ' ' : '│ ')
|
|
1088
|
+
_tree_recursive(entry.relative_path, output, max_depth, next_prefix, current_depth + 1)
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
end
|