fs_cache 0.0.1
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 +7 -0
- data/lib/fs_cache.rb +364 -0
- data/lib/fs_cache/attribute.rb +20 -0
- data/lib/fs_cache/attributes/crc.rb +105 -0
- data/lib/fs_cache/attributes/size.rb +38 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c7307ee577d076235b7d885087638f0576b59d408339a5c02150eeeaec12bc00
|
4
|
+
data.tar.gz: 8f66ac733ff6144a2fdcfc0f1fd052996daa24d78d8d64fc4e11415f46352d3a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9889b8e9d5371facd30e07d7cf46c6d84ddb8bbc8b5059bf7a702bb5bf122c86bba5b01f0f2b37581ffbfaba77ebba7ec80be43ab4d219d02fa15e1de1b25fad
|
7
|
+
data.tar.gz: b660da8740076a449a15a8dca09080b5b89e3819181ebce93b335de544dc365bc51043eafdae1dffdca6a9286082206af3594390042779355cc82aedcc4f0741
|
data/lib/fs_cache.rb
ADDED
@@ -0,0 +1,364 @@
|
|
1
|
+
require 'progressbar'
|
2
|
+
require 'fs_cache/attribute'
|
3
|
+
|
4
|
+
# Implement a cache of the file system: directories and files presence.
|
5
|
+
# Plugins can be used to also cache attributes of the files, like crc, size...
|
6
|
+
class FsCache
|
7
|
+
|
8
|
+
ATTRIBUTE_PLUGINS_MODULE = FsCache::Attributes
|
9
|
+
|
10
|
+
# Constructor
|
11
|
+
#
|
12
|
+
# Parameters::
|
13
|
+
# * *attribute_plugins_dirs* (Array<String>): List of directories containing possible attribute plugins [default = []]
|
14
|
+
def initialize(attribute_plugins_dirs: [])
|
15
|
+
# List of possible attribute plugins, per attribute name
|
16
|
+
# Hash<Symbol, Attribute>
|
17
|
+
@attribute_plugins = {}
|
18
|
+
# Tree of dependent attributes: for each attribute in this tree, the list of attributes to be invalidated if this attribute changes.
|
19
|
+
# Hash<Symbol, Array<Symbol> >
|
20
|
+
@dependent_attributes = {}
|
21
|
+
# Big database of files information
|
22
|
+
# Hash<String, Hash<Symbol,Object> >: For each file name, the file info (can be incomplete if it was never fetched):
|
23
|
+
# * *exist* (Boolean): Does the file exist?
|
24
|
+
# * *size* (Integer): File size.
|
25
|
+
# * *crc* (String): File CRC.
|
26
|
+
# * *corruption* (false or Object): Info about this file's corruption, or false if sane.
|
27
|
+
@files = Hash.new { |h, k| h[k] = {} }
|
28
|
+
# Directories information
|
29
|
+
# Hash<String, Hash<Symbol,Object> >: For each directory name, the dir info (can be incomplete if it was never fetched):
|
30
|
+
# * *files* (Hash<String,nil>): Set of files (base names)
|
31
|
+
# * *dirs* (Hash<String,nil>): Set of directories (base names)
|
32
|
+
# * *recursive_dirs* (Hash<String,nil>): Set of recursive sub-directories (full paths)
|
33
|
+
# * *recursive_files* (Hash<String,nil>): Set of recursive files (full paths)
|
34
|
+
@dirs = Hash.new { |h, k| h[k] = {} }
|
35
|
+
|
36
|
+
# Automatically register attributes from the plugins dirs
|
37
|
+
(["#{__dir__}/fs_cache/attributes"] + attribute_plugins_dirs).each do |attribute_plugins_dir|
|
38
|
+
Dir.glob("#{attribute_plugins_dir}/*.rb") do |attribute_plugin_file|
|
39
|
+
attribute = File.basename(attribute_plugin_file)[0..-4].to_sym
|
40
|
+
require attribute_plugin_file
|
41
|
+
class_name = attribute.to_s.split('_').collect(&:capitalize).join.to_sym
|
42
|
+
if ATTRIBUTE_PLUGINS_MODULE.const_defined?(class_name)
|
43
|
+
plugin_class = ATTRIBUTE_PLUGINS_MODULE.const_get(class_name)
|
44
|
+
register_attribute_plugin(attribute, plugin_class.new)
|
45
|
+
else
|
46
|
+
raise "Attributes plugin #{attribute_plugin_file} does not define the class #{class_name} inside #{ATTRIBUTE_PLUGINS_MODULE}" if plugin_class.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Register a new attributes' plugin.
|
53
|
+
# The constructor already registers all plugins found in the plugins directories.
|
54
|
+
# This method exists in order to register plugins that could be dynamically instantiated.
|
55
|
+
#
|
56
|
+
# Parameters::
|
57
|
+
# * *attribute* (Symbol): The attribute
|
58
|
+
# * *plugin* (Attribute): The attribute plugin
|
59
|
+
def register_attribute_plugin(attribute, plugin)
|
60
|
+
puts "Registering attribute plugin #{attribute}..."
|
61
|
+
raise "Attributes plugin #{attribute} is already defined (by class #{@attribute_plugins[attribute].class.name})." if @attribute_plugins.key?(attribute)
|
62
|
+
@attribute_plugins[attribute] = plugin
|
63
|
+
# Define the getter methods for this attribute, directly in the base class for performance purposes
|
64
|
+
|
65
|
+
# Get the attribute for a given file.
|
66
|
+
# Use the cache if possible.
|
67
|
+
#
|
68
|
+
# Parameters::
|
69
|
+
# * *file* (String): File path for which we look for the attribute
|
70
|
+
# Result::
|
71
|
+
# * Object: Corresponding attribute value, or nil if the file does not exist
|
72
|
+
define_singleton_method("#{attribute}_for".to_sym) do |file|
|
73
|
+
@files[file][attribute] = plugin.attribute_for(file) if !@files[file].key?(attribute) && exist?(file)
|
74
|
+
@files[file][attribute]
|
75
|
+
end
|
76
|
+
|
77
|
+
# If there are some helpers, register them too
|
78
|
+
if plugin.class.const_defined?(:Helpers)
|
79
|
+
helpers_module = plugin.class.const_get(:Helpers)
|
80
|
+
self.class.include helpers_module unless helpers_module.nil?
|
81
|
+
end
|
82
|
+
# If this attribute is dependent on others, remember it too
|
83
|
+
plugin.invalidated_on_change_of.each do |parent_attribute|
|
84
|
+
@dependent_attributes[parent_attribute] = [] unless @dependent_attributes.key?(parent_attribute)
|
85
|
+
@dependent_attributes[parent_attribute] << attribute
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Is a file existing?
|
90
|
+
#
|
91
|
+
# Parameters::
|
92
|
+
# * *file* (String): File name
|
93
|
+
# Result::
|
94
|
+
# * String: Is the file existing?
|
95
|
+
def exist?(file)
|
96
|
+
# If there is at least 1 attribute for this file it means that it exists
|
97
|
+
unless @files[file].key?(:exist)
|
98
|
+
@files[file][:exist] =
|
99
|
+
# If we have an attribute for this file, it means it exist
|
100
|
+
if @files[file].size > 0
|
101
|
+
true
|
102
|
+
else
|
103
|
+
dir = File.dirname(file)
|
104
|
+
if @dirs.key?(dir)
|
105
|
+
# We know about its directory, so we should know if it is there
|
106
|
+
@dirs[dir][:files].key?(File.basename(file))
|
107
|
+
else
|
108
|
+
File.exist?(file)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
@files[file][:exist]
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get list of files from a directory (base names)
|
116
|
+
#
|
117
|
+
# Parameters::
|
118
|
+
# * *dir* (String): The directory to get files from
|
119
|
+
# Result::
|
120
|
+
# * Array<String>: List of file base names
|
121
|
+
def files_in(dir)
|
122
|
+
ensure_dir_data(dir)
|
123
|
+
@dirs[dir][:files].keys
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get recursive list of directories from a directory
|
127
|
+
#
|
128
|
+
# Parameters::
|
129
|
+
# * *dir* (String): The directory to get other directories from
|
130
|
+
# Result::
|
131
|
+
# * Array<String>: List of directories
|
132
|
+
def dirs_from(dir)
|
133
|
+
unless @dirs[dir].key?(:recursive_dirs)
|
134
|
+
ensure_dir_data(dir)
|
135
|
+
recursive_dirs = {}
|
136
|
+
@dirs[dir][:dirs].keys.each do |subdir|
|
137
|
+
full_subdir = "#{dir}/#{subdir}"
|
138
|
+
recursive_dirs[full_subdir] = nil
|
139
|
+
recursive_dirs.merge!(Hash[dirs_from(full_subdir).map { |subsubdir| [subsubdir, nil] }])
|
140
|
+
end
|
141
|
+
@dirs[dir][:recursive_dirs] = recursive_dirs
|
142
|
+
end
|
143
|
+
@dirs[dir][:recursive_dirs].keys
|
144
|
+
end
|
145
|
+
|
146
|
+
# Get recursive list of files from a directory
|
147
|
+
#
|
148
|
+
# Parameters::
|
149
|
+
# * *dir* (String): The directory to get other directories from
|
150
|
+
# Result::
|
151
|
+
# * Array<String>: List of files
|
152
|
+
def files_from(dir)
|
153
|
+
unless @dirs[dir].key?(:recursive_files)
|
154
|
+
ensure_dir_data(dir)
|
155
|
+
recursive_files = Hash[@dirs[dir][:files].keys.map { |file| ["#{dir}/#{file}", nil] }]
|
156
|
+
@dirs[dir][:dirs].keys.each do |subdir|
|
157
|
+
recursive_files.merge!(Hash[files_from("#{dir}/#{subdir}").map { |file| [file, nil] }])
|
158
|
+
end
|
159
|
+
@dirs[dir][:recursive_files] = recursive_files
|
160
|
+
end
|
161
|
+
@dirs[dir][:recursive_files].keys
|
162
|
+
end
|
163
|
+
|
164
|
+
# Scan files and directories from a list of directories.
|
165
|
+
# Use a progress bar.
|
166
|
+
#
|
167
|
+
# Parameters::
|
168
|
+
# * *dirs* (Array<String>): List of directories to preload
|
169
|
+
# * *include_attributes* (Array<Symbol> or nil): List of attributes to scan, or nil for all [default = nil]
|
170
|
+
# * *exclude_attributes* (Array<Symbol>): List of attributes to ignore while scanning [default = []]
|
171
|
+
def scan(dirs, include_attributes: nil, exclude_attributes: [])
|
172
|
+
progressbar = ProgressBar.create(title: 'Indexing files')
|
173
|
+
attributes_to_scan = (include_attributes.nil? ? @attribute_plugins.keys : include_attributes) - exclude_attributes
|
174
|
+
files = dirs.
|
175
|
+
map do |dir|
|
176
|
+
dirs_from(dir)
|
177
|
+
files_from(dir)
|
178
|
+
end.
|
179
|
+
flatten
|
180
|
+
progressbar.total = files.size
|
181
|
+
files.each do |file|
|
182
|
+
exist?(file)
|
183
|
+
attributes_to_scan.each do |attribute|
|
184
|
+
self.send "#{attribute}_for", file
|
185
|
+
end
|
186
|
+
progressbar.increment
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Serialize into JSON.
|
191
|
+
#
|
192
|
+
# Result::
|
193
|
+
# * Object: JSON object
|
194
|
+
def to_json
|
195
|
+
{
|
196
|
+
files: @files,
|
197
|
+
dirs: @dirs
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
# Get data from JSON.
|
202
|
+
#
|
203
|
+
# Parameters::
|
204
|
+
# * *json* (Object): JSON object
|
205
|
+
def from_json(json)
|
206
|
+
json = json.transform_keys(&:to_sym)
|
207
|
+
@files = Hash[json[:files].map { |file, file_info| [file, file_info.transform_keys(&:to_sym)] }]
|
208
|
+
@files.default_proc = proc { |h, k| h[k] = {} }
|
209
|
+
@dirs = Hash[json[:dirs].map { |dir, dir_info| [dir, dir_info.transform_keys(&:to_sym)] }]
|
210
|
+
@dirs.default_proc = proc { |h, k| h[k] = {} }
|
211
|
+
end
|
212
|
+
|
213
|
+
# Notify the file system that a given file has been deleted
|
214
|
+
#
|
215
|
+
# Parameters::
|
216
|
+
# * *file* (String): File being deleted
|
217
|
+
def notify_file_rm(file)
|
218
|
+
@files[file] = { exist: false }
|
219
|
+
unregister_file_from_dirs(file)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Notify the file system of a file copy
|
223
|
+
#
|
224
|
+
# Parameters::
|
225
|
+
# * *src* (String): Origin file
|
226
|
+
# * *dst* (String): Destination file
|
227
|
+
def notify_file_cp(src, dst)
|
228
|
+
if @files.key?(src)
|
229
|
+
@files[dst] = @files[src].clone
|
230
|
+
else
|
231
|
+
@files[src] = { exist: true }
|
232
|
+
@files[dst] = { exist: true }
|
233
|
+
end
|
234
|
+
register_file_in_dirs(dst)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Notify the file system of a file move
|
238
|
+
#
|
239
|
+
# Parameters::
|
240
|
+
# * *src* (String): Origin file
|
241
|
+
# * *dst* (String): Destination file
|
242
|
+
def notify_file_mv(src, dst)
|
243
|
+
notify_file_cp(src, dst)
|
244
|
+
notify_file_rm(src)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Check our info against file system changes.
|
248
|
+
# This detects
|
249
|
+
# * files that have been deleted,
|
250
|
+
# * any change in the directories structure,
|
251
|
+
# * any change in the attributes that are already part of the cache and that are not ignored explicitely.
|
252
|
+
#
|
253
|
+
# Parameters::
|
254
|
+
# * *include_attributes* (Array<Symbol> or nil): List of attributes to scan, or nil for all [default = nil]
|
255
|
+
# * *exclude_attributes* (Array<Symbol>): List of attributes to ignore while scanning [default = []]
|
256
|
+
def check(include_attributes: nil, exclude_attributes: [])
|
257
|
+
progressbar = ProgressBar.create(title: 'Refreshing files info')
|
258
|
+
attributes_to_scan = (include_attributes.nil? ? @attribute_plugins.keys : include_attributes) - exclude_attributes
|
259
|
+
progressbar.total = @files.size
|
260
|
+
@files.each do |file, file_info|
|
261
|
+
if File.exist?(file)
|
262
|
+
if file_info.key?(:exist) && !file_info[:exist]
|
263
|
+
# This file has been added when we thought it was missing
|
264
|
+
file_info.replace(exist: true)
|
265
|
+
else
|
266
|
+
# Check attributes that are already present
|
267
|
+
(file_info.keys & attributes_to_scan).each do |attribute|
|
268
|
+
current_attribute = file_info[attribute]
|
269
|
+
new_attribute = @attribute_plugins[attribute].attribute_for(file)
|
270
|
+
if current_attribute != new_attribute
|
271
|
+
# Attribute has changed
|
272
|
+
file_info[attribute] = new_attribute
|
273
|
+
# If some other attributes were depending on this one, invalidate them
|
274
|
+
if @dependent_attributes.key?(attribute)
|
275
|
+
@dependent_attributes[attribute].each do |dependent_attribute|
|
276
|
+
file_info.delete(dependent_attribute)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
elsif !file_info.key?(:exist) || file_info[:exist]
|
283
|
+
# This file has been removed when we thought it was there
|
284
|
+
file_info.replace(exist: false)
|
285
|
+
end
|
286
|
+
progressbar.increment
|
287
|
+
end
|
288
|
+
# Rebuilding @dirs structure needs to make the Dir.glob commands once again. Therefore there is no need to check it. Removing it will rebuild it anyway.
|
289
|
+
@dirs.clear
|
290
|
+
end
|
291
|
+
|
292
|
+
# Remove attributes for a list of files
|
293
|
+
#
|
294
|
+
# Parameters::
|
295
|
+
# * *files* (Array<String>): The list of files to invalidate attributes for
|
296
|
+
# * *include_attributes* (Array<Symbol> or nil): List of attributes to scan, or nil for all [default = nil]
|
297
|
+
# * *exclude_attributes* (Array<Symbol>): List of attributes to ignore while scanning [default = []]
|
298
|
+
def invalidate(files, include_attributes: nil, exclude_attributes: [])
|
299
|
+
attributes_to_invalidate = ((include_attributes.nil? ? @attribute_plugins.keys : include_attributes) - exclude_attributes)
|
300
|
+
files.each do |file|
|
301
|
+
if @files.key?(file)
|
302
|
+
attributes_to_invalidate.each do |attribute|
|
303
|
+
@files[file].delete(attribute)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
private
|
310
|
+
|
311
|
+
# Register a file in the @dirs structure
|
312
|
+
#
|
313
|
+
# Parameters::
|
314
|
+
# * *file* (String): File to register in @dirs
|
315
|
+
def register_file_in_dirs(file)
|
316
|
+
file_dir = File.dirname(file)
|
317
|
+
split_dir = file_dir.split('/')
|
318
|
+
split_dir.size.times do |idx|
|
319
|
+
dir = split_dir[0..idx].join('/')
|
320
|
+
@dirs[dir][:recursive_files][file] = nil if @dirs.key?(dir) && @dirs[dir].key?(:recursive_files) && !@dirs[dir][:recursive_files].key?(file)
|
321
|
+
end
|
322
|
+
base_name = File.basename(file)
|
323
|
+
@dirs[file_dir][:files][base_name] = nil if @dirs.key?(file_dir) && @dirs[file_dir].key?(:files) && !@dirs[file_dir][:files].key?(base_name)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Unregister a file in the @dirs structure
|
327
|
+
#
|
328
|
+
# Parameters::
|
329
|
+
# * *file* (String): File to unregister from @dirs
|
330
|
+
def unregister_file_from_dirs(file)
|
331
|
+
file_dir = File.dirname(file)
|
332
|
+
split_dir = file_dir.split('/')
|
333
|
+
split_dir.size.times do |idx|
|
334
|
+
dir = split_dir[0..idx].join('/')
|
335
|
+
# Remove any reference of our file to this dir info
|
336
|
+
@dirs[dir][:recursive_files].delete(file) if @dirs.key?(dir) && @dirs[dir].key?(:recursive_files)
|
337
|
+
end
|
338
|
+
@dirs[file_dir][:files].delete(File.basename(file)) if @dirs.key?(file_dir) && @dirs[file_dir].key?(:files)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Populate a given directory data (files and dirs)
|
342
|
+
#
|
343
|
+
# Parameters::
|
344
|
+
# * *dir* (String): Directory to get data from
|
345
|
+
def ensure_dir_data(dir)
|
346
|
+
unless @dirs[dir].key?(:files)
|
347
|
+
files = {}
|
348
|
+
dirs = {}
|
349
|
+
Dir.glob("#{dir}/*", File::FNM_DOTMATCH).each do |file|
|
350
|
+
base_name = File.basename(file)
|
351
|
+
if File.directory?(file)
|
352
|
+
dirs[base_name] = nil if base_name != '.' && base_name != '..'
|
353
|
+
else
|
354
|
+
files[base_name] = nil
|
355
|
+
end
|
356
|
+
end
|
357
|
+
@dirs[dir] = {
|
358
|
+
files: files,
|
359
|
+
dirs: dirs
|
360
|
+
}
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class FsCache
|
2
|
+
|
3
|
+
module Attributes
|
4
|
+
end
|
5
|
+
|
6
|
+
# Common ancestor for all attributes
|
7
|
+
class Attribute
|
8
|
+
|
9
|
+
# Get the list of other attributes that invalidate this one.
|
10
|
+
# If any of those attributes is chaning on a file, then reset our attribute for the file.
|
11
|
+
#
|
12
|
+
# Result::
|
13
|
+
# * Array<Symbol>: List of dependent attributes
|
14
|
+
def invalidated_on_change_of
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
class FsCache
|
4
|
+
|
5
|
+
module Attributes
|
6
|
+
|
7
|
+
# CRC attribute. Can be:
|
8
|
+
# * String: Internal CRC computed from files by blocks
|
9
|
+
class Crc < Attribute
|
10
|
+
|
11
|
+
# Size of blocks to compute CRCs in bytes. Changing this value will invalidate previously computed CRCs.
|
12
|
+
CRC_BLOCK_SIZE = 32 * 1024 * 1024 # 32 MB
|
13
|
+
|
14
|
+
# Get the attribute for a given file
|
15
|
+
#
|
16
|
+
# Parameters::
|
17
|
+
# * *file* (String): File to get the attribute for
|
18
|
+
# Result::
|
19
|
+
# * Object: Corresponding attribute value
|
20
|
+
def attribute_for(file)
|
21
|
+
blocks_crc = ''
|
22
|
+
File.open(file, 'rb') do |file_io|
|
23
|
+
buffer = nil
|
24
|
+
while (buffer = file_io.read(CRC_BLOCK_SIZE))
|
25
|
+
blocks_crc << Zlib.crc32(buffer, 0).to_s(16).upcase
|
26
|
+
end
|
27
|
+
end
|
28
|
+
Zlib.crc32(blocks_crc, 0).to_s(16).upcase
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the list of other attributes that invalidate this one.
|
32
|
+
# If any of those attributes is chaning on a file, then reset our attribute for the file.
|
33
|
+
#
|
34
|
+
# Result::
|
35
|
+
# * Array<Symbol>: List of dependent attributes
|
36
|
+
def invalidated_on_change_of
|
37
|
+
[:size]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add helpers to the cache
|
41
|
+
module Helpers
|
42
|
+
|
43
|
+
# Provide info on the differences between 2 directories.
|
44
|
+
#
|
45
|
+
# Parameters::
|
46
|
+
# * *dir1* (String): First directory
|
47
|
+
# * *dir2* (String): Second directory
|
48
|
+
# Result::
|
49
|
+
# * Hash<Symbol,Object>: Difference between the 2 directories (dir2 - dir1):
|
50
|
+
# * *same* (Array<String>): Same files
|
51
|
+
# * *renamed* (Array<[String,String]>): Renamed files (from dir1 to dir2: [file_base1, file_base2])
|
52
|
+
# * *added* (Array<String>): Added files
|
53
|
+
# * *deleted* (Array<String>): Deleted files
|
54
|
+
# * *different* (Array<String>): Different files
|
55
|
+
def diff_dirs(dir1, dir2)
|
56
|
+
files1 = Hash[files_in(dir1).map { |file| [file, "#{dir1}/#{file}"] }]
|
57
|
+
files2 = Hash[files_in(dir2).map { |file| [file, "#{dir2}/#{file}"] }]
|
58
|
+
same = []
|
59
|
+
different = []
|
60
|
+
renamed = []
|
61
|
+
# First process files having the same names
|
62
|
+
files1.delete_if do |file_base1, file1|
|
63
|
+
if files2.key?(file_base1)
|
64
|
+
# A file with same name exists in dir2
|
65
|
+
if crc_for(files2[file_base1]) == crc_for(file1)
|
66
|
+
same << file_base1
|
67
|
+
else
|
68
|
+
different << file_base1
|
69
|
+
end
|
70
|
+
files2.delete(file_base1)
|
71
|
+
true
|
72
|
+
else
|
73
|
+
false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# Then process files having the same CRC among the remaining ones
|
77
|
+
files1.delete_if do |file_base1, file1|
|
78
|
+
crc1 = crc_for(file1)
|
79
|
+
found_file_base2, _found_file2 = files2.find { |_file_base2, file2| crc_for(file2) == crc1 }
|
80
|
+
if found_file_base2.nil?
|
81
|
+
false
|
82
|
+
else
|
83
|
+
renamed << [file_base1, found_file_base2]
|
84
|
+
files2.delete(found_file_base2)
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
remaining_files1 = files1.keys
|
89
|
+
remaining_files2 = files2.keys
|
90
|
+
{
|
91
|
+
same: same,
|
92
|
+
renamed: renamed,
|
93
|
+
added: remaining_files2 - remaining_files1,
|
94
|
+
deleted: remaining_files1 - remaining_files2,
|
95
|
+
different: different
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class FsCache
|
2
|
+
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
# Size attribute. Can be:
|
6
|
+
# * Integer: File size
|
7
|
+
class Size < Attribute
|
8
|
+
|
9
|
+
# Get the attribute for a given file
|
10
|
+
#
|
11
|
+
# Parameters::
|
12
|
+
# * *file* (String): File to get the attribute for
|
13
|
+
# Result::
|
14
|
+
# * Object: Corresponding attribute value
|
15
|
+
def attribute_for(file)
|
16
|
+
File.stat(file).size
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add helpers to the cache
|
20
|
+
module Helpers
|
21
|
+
|
22
|
+
# Is a file empty?
|
23
|
+
#
|
24
|
+
# Parameters::
|
25
|
+
# * *file* (String): File name
|
26
|
+
# Result::
|
27
|
+
# * String: Is the file empty?
|
28
|
+
def empty?(file)
|
29
|
+
size_for(file) == 0
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fs_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Muriel Salvan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: progressbar
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- muriel@x-aeon.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/fs_cache.rb
|
35
|
+
- lib/fs_cache/attribute.rb
|
36
|
+
- lib/fs_cache/attributes/crc.rb
|
37
|
+
- lib/fs_cache/attributes/size.rb
|
38
|
+
homepage: http://x-aeon.com
|
39
|
+
licenses:
|
40
|
+
- BSD-3-Clause
|
41
|
+
metadata:
|
42
|
+
homepage_uri: http://x-aeon.com
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.7.6
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Simple file system caching to perform huge and repetitive accesses to files,
|
63
|
+
directories and various files' content analysis
|
64
|
+
test_files: []
|