libis-tools 1.0.5-java
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/.coveralls.yml +2 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.travis.yml +40 -0
- data/Gemfile +7 -0
- data/README.md +202 -0
- data/Rakefile +11 -0
- data/bin/libis_tool +5 -0
- data/lib/libis-tools.rb +1 -0
- data/lib/libis/tools.rb +25 -0
- data/lib/libis/tools/assert.rb +52 -0
- data/lib/libis/tools/checksum.rb +106 -0
- data/lib/libis/tools/cli/cli_helper.rb +189 -0
- data/lib/libis/tools/cli/reorg.rb +416 -0
- data/lib/libis/tools/command.rb +133 -0
- data/lib/libis/tools/command_line.rb +23 -0
- data/lib/libis/tools/config.rb +147 -0
- data/lib/libis/tools/config_file.rb +85 -0
- data/lib/libis/tools/csv.rb +38 -0
- data/lib/libis/tools/deep_struct.rb +71 -0
- data/lib/libis/tools/extend/array.rb +16 -0
- data/lib/libis/tools/extend/empty.rb +7 -0
- data/lib/libis/tools/extend/hash.rb +147 -0
- data/lib/libis/tools/extend/kernel.rb +25 -0
- data/lib/libis/tools/extend/ostruct.rb +3 -0
- data/lib/libis/tools/extend/roo.rb +91 -0
- data/lib/libis/tools/extend/string.rb +94 -0
- data/lib/libis/tools/extend/struct.rb +29 -0
- data/lib/libis/tools/extend/symbol.rb +8 -0
- data/lib/libis/tools/logger.rb +130 -0
- data/lib/libis/tools/mets_dnx.rb +61 -0
- data/lib/libis/tools/mets_file.rb +504 -0
- data/lib/libis/tools/mets_objects.rb +547 -0
- data/lib/libis/tools/parameter.rb +372 -0
- data/lib/libis/tools/spreadsheet.rb +196 -0
- data/lib/libis/tools/temp_file.rb +42 -0
- data/lib/libis/tools/thread_safe.rb +31 -0
- data/lib/libis/tools/version.rb +5 -0
- data/lib/libis/tools/xml_document.rb +583 -0
- data/libis-tools.gemspec +55 -0
- data/spec/assert_spec.rb +65 -0
- data/spec/checksum_spec.rb +68 -0
- data/spec/command_spec.rb +90 -0
- data/spec/config_file_spec.rb +83 -0
- data/spec/config_spec.rb +113 -0
- data/spec/csv_spec.rb +159 -0
- data/spec/data/test-headers.csv +2 -0
- data/spec/data/test-headers.tsv +2 -0
- data/spec/data/test-noheaders.csv +1 -0
- data/spec/data/test-noheaders.tsv +1 -0
- data/spec/data/test.data +9 -0
- data/spec/data/test.xlsx +0 -0
- data/spec/data/test.xml +8 -0
- data/spec/data/test.yml +2 -0
- data/spec/data/test_config.yml +15 -0
- data/spec/deep_struct_spec.rb +138 -0
- data/spec/logger_spec.rb +165 -0
- data/spec/mets_file_spec.rb +223 -0
- data/spec/parameter_container_spec.rb +152 -0
- data/spec/parameter_spec.rb +148 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/spreadsheet_spec.rb +1820 -0
- data/spec/temp_file_spec.rb +76 -0
- data/spec/test.xsd +20 -0
- data/spec/thread_safe_spec.rb +64 -0
- data/spec/xmldocument_spec.rb +421 -0
- data/test/test_helper.rb +7 -0
- data/test/webservices/test_ca_item_info.rb +59 -0
- data/test/webservices/test_ca_search.rb +35 -0
- metadata +437 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'tty-prompt'
|
2
|
+
require 'tty-config'
|
3
|
+
require 'pastel'
|
4
|
+
|
5
|
+
module Libis
|
6
|
+
module Tools
|
7
|
+
module Cli
|
8
|
+
module Helper
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def exit_on_failure?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(base)
|
19
|
+
base.extend(ClassMethods)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :prompt, :config, :pastel, :config_file_prefix
|
23
|
+
|
24
|
+
def initialize(*args)
|
25
|
+
@prompt = TTY::Prompt.new
|
26
|
+
@config = TTY::Config.new
|
27
|
+
@pastel = Pastel.new
|
28
|
+
@config.append_path Dir.home
|
29
|
+
@config_file_prefix = '.tools.'
|
30
|
+
prompt.warn "Default config file: #{config.filename}"
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def index_of(list, value)
|
39
|
+
i = list.index(value)
|
40
|
+
i += 1 if i
|
41
|
+
i || 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def config_write(name = nil)
|
45
|
+
set_config_name(name || new_config)
|
46
|
+
unless get_config_name
|
47
|
+
prompt.error 'Could not write the configuration file: configuration not set'
|
48
|
+
return
|
49
|
+
end
|
50
|
+
config.write force: true
|
51
|
+
end
|
52
|
+
|
53
|
+
def config_read(name = nil)
|
54
|
+
config.filename = name ?
|
55
|
+
"#{config_file_prefix}#{name}" :
|
56
|
+
select_config_file(with_new: false)
|
57
|
+
unless get_config_name
|
58
|
+
prompt.error 'Could not read the configuration file: configuration not set'
|
59
|
+
return
|
60
|
+
end
|
61
|
+
config.read
|
62
|
+
rescue TTY::Config::ReadError
|
63
|
+
prompt.error('Could not read the configuration file.')
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
|
67
|
+
def toggle_config(field)
|
68
|
+
config.set(field, value: !config.fetch(field))
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_config_name
|
72
|
+
return $1 if get_config_file.match(config_file_regex)
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_config_file
|
77
|
+
config.filename
|
78
|
+
end
|
79
|
+
|
80
|
+
def config_file_regex(with_ext: false)
|
81
|
+
/^#{Regexp.quote(config_file_prefix)}(.+)#{Regexp.quote(config.extname) if with_ext}$/
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_config_name(name)
|
85
|
+
config.filename = "#{config_file_prefix}#{name}" if name && !name.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_config_file(name)
|
89
|
+
config.filename = name if name && !name.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def select_config_file(*args)
|
93
|
+
"#{config_file_prefix}#{select_config_name *args}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def select_config_name(with_new: true, force_select: false)
|
97
|
+
current_cfg = get_config_name
|
98
|
+
return current_cfg if !force_select && current_cfg
|
99
|
+
|
100
|
+
cfgs = []
|
101
|
+
cfgs << {
|
102
|
+
name: '-- new configuration --',
|
103
|
+
value: -> do
|
104
|
+
new_config
|
105
|
+
end
|
106
|
+
} if with_new
|
107
|
+
cfgs += Dir.glob(File.join(Dir.home, "#{config_file_prefix}*")).reduce([]) do |a, x|
|
108
|
+
a.push($1) if File.basename(x).match(config_file_regex(with_ext: true))
|
109
|
+
a
|
110
|
+
end
|
111
|
+
|
112
|
+
return nil if cfgs.empty?
|
113
|
+
|
114
|
+
prompt.select '[ Select config menu ]', cfgs, default: index_of(cfgs, current_cfg), filter: true
|
115
|
+
end
|
116
|
+
|
117
|
+
def new_config
|
118
|
+
while true
|
119
|
+
name = prompt.ask('Enter a name for the configuration:', modify: :trim)
|
120
|
+
return name unless File.exist?(File.join(Dir.home, "#{config_file_prefix}#{name}#{config.extname}")) &&
|
121
|
+
!prompt.yes?("Configuration '#{name}' already exists. Overwrite?")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def ask(question, field, bool: false, enum: nil, default: nil, mask: false, if_empty: false)
|
126
|
+
cmd, args, opts = :ask, [question], {}
|
127
|
+
default ||= config.fetch(field)
|
128
|
+
if enum
|
129
|
+
cmd = :select
|
130
|
+
args << enum
|
131
|
+
# Change default to its index in the enum
|
132
|
+
default = index_of(enum, default)
|
133
|
+
# Force the question if the supplied value is not valid
|
134
|
+
config.delete field unless !if_empty || enum.include?(config.fetch field)
|
135
|
+
end
|
136
|
+
cmd = :mask if mask
|
137
|
+
opts[:default] = config.fetch(field)
|
138
|
+
opts[:default] = default if default
|
139
|
+
cmd = (opts[:default] ? :yes? : :no?) if bool
|
140
|
+
config.set(field, value: prompt.send(cmd, *args, opts)) unless if_empty && config.fetch(field)
|
141
|
+
end
|
142
|
+
|
143
|
+
def tree_select(path, question: nil, file: false, page_size: 22, filter: true, cycle: false, create: false,
|
144
|
+
default_choices: nil)
|
145
|
+
path = Pathname.new(path) unless path.is_a? Pathname
|
146
|
+
|
147
|
+
return path unless path.exist?
|
148
|
+
path = path.realpath
|
149
|
+
|
150
|
+
dirs = path.children.select(&:directory?).sort
|
151
|
+
files = file ? path.children.select(&:file?).sort : []
|
152
|
+
|
153
|
+
choices = []
|
154
|
+
choices << {name: "Folder: #{path}", value: path, disabled: file ? '' : false}
|
155
|
+
choices += default_choices if default_choices
|
156
|
+
choices << {name: '-- new directory --', value: -> do
|
157
|
+
new_name = prompt.ask('new directory name:', modify: :trim, required: true)
|
158
|
+
new_path = path + new_name
|
159
|
+
FileUtils.mkdir(new_path.to_path)
|
160
|
+
new_path
|
161
|
+
end
|
162
|
+
} if create
|
163
|
+
|
164
|
+
choices << {name: "-- new file --", value: -> do
|
165
|
+
new_name = prompt.ask('new file name:', modify: :trim, required: true)
|
166
|
+
path + new_name
|
167
|
+
end
|
168
|
+
} if file && create
|
169
|
+
|
170
|
+
choices << {name: '[..]', value: path.parent}
|
171
|
+
|
172
|
+
dirs.each {|d| choices << {name: "[#{d.basename}]", value: d}}
|
173
|
+
files.each {|f| choices << {name: f.basename.to_path, value: f}}
|
174
|
+
|
175
|
+
question ||= "Select #{'file or ' if files}directory"
|
176
|
+
selection = prompt.select question, choices,
|
177
|
+
per_page: page_size, filter: filter, cycle: cycle, default: file ? 2 : 1
|
178
|
+
|
179
|
+
return selection unless selection.is_a? Pathname
|
180
|
+
return selection.to_path if selection == path || selection.file?
|
181
|
+
|
182
|
+
tree_select selection, question: question, file: file, page_size: page_size, filter: filter,
|
183
|
+
cycle: cycle, create: create, default_choices: default_choices
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,416 @@
|
|
1
|
+
require 'libis/tools/spreadsheet'
|
2
|
+
require 'awesome_print'
|
3
|
+
|
4
|
+
module Libis
|
5
|
+
module Tools
|
6
|
+
module Cli
|
7
|
+
module Reorg
|
8
|
+
|
9
|
+
# noinspection RubyExpressionInStringInspection
|
10
|
+
DEFAULT_CONFIG = {
|
11
|
+
base: '.',
|
12
|
+
filter: '^(.*)$',
|
13
|
+
expression: 'target/#{file_name}',
|
14
|
+
action: 'move',
|
15
|
+
overwrite: false,
|
16
|
+
interactive: false,
|
17
|
+
report: nil,
|
18
|
+
dummy: false,
|
19
|
+
config: nil,
|
20
|
+
unattended: false
|
21
|
+
}
|
22
|
+
|
23
|
+
# noinspection RubyStringKeysInHashInspection
|
24
|
+
VALID_ACTIONS = {
|
25
|
+
'move' => 'moved',
|
26
|
+
'copy' => 'copied',
|
27
|
+
'link' => 'linked'
|
28
|
+
}
|
29
|
+
|
30
|
+
STRING_CONFIG = {
|
31
|
+
base: "Source Directory to organize",
|
32
|
+
filter: "File matching filter",
|
33
|
+
expression: "New file path expression",
|
34
|
+
action: "Action to perform",
|
35
|
+
overwrite: "Overwite target files if newer",
|
36
|
+
interactive: "Ask for action on changed files",
|
37
|
+
report: "Report file",
|
38
|
+
dummy: "Perform phantom actions (not affecting files)",
|
39
|
+
config: "Load saved configuration parameters"
|
40
|
+
}
|
41
|
+
|
42
|
+
REQ_HEADERS = {term: 'Term'}
|
43
|
+
OPT_HEADERS = {pid: 'Pid', filename: 'File'}
|
44
|
+
|
45
|
+
def self.included(klass)
|
46
|
+
klass.class_exec do
|
47
|
+
def klass.description(field)
|
48
|
+
"#{STRING_CONFIG[field]}." + (DEFAULT_CONFIG[field].nil? ? '' : " default: #{DEFAULT_CONFIG[field]}")
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'reorg [options]', 'Reorganize files'
|
52
|
+
long_desc <<-DESC
|
53
|
+
|
54
|
+
'reorg [options]' will reorganize files based on the name of the files.
|
55
|
+
|
56
|
+
The base directory will be scanned for files that match the FILTER regular expression. For each matching
|
57
|
+
file, an action will be performed. The outcome of the action is determined by the expression that is given.
|
58
|
+
|
59
|
+
The expression will be evaluated as a Ruby string expression and supports string interpolation in the form
|
60
|
+
'\#{<thing>}', where <thing> can be any of:
|
61
|
+
|
62
|
+
. $x : refers to the x-th group in the FILTER. Groups are numbered by the order of the opening '('
|
63
|
+
|
64
|
+
. file_name : the original file name
|
65
|
+
|
66
|
+
The action that will be performed on the action depens on the configured ACTION. The valid ACTIONs are;
|
67
|
+
'move', copy' and 'link'. Please note that in the latter case only the files will be soft-linked and any
|
68
|
+
directory in the target path will be created. The tool will therefore never create soft-links to directories.
|
69
|
+
The soft-links are created with an absolute reference path. This allows you to later move and rename the
|
70
|
+
soft-links later as you seem fit without affecting the source files. You could for instance run this tool on
|
71
|
+
the soft-links with the 'move' action to do so.
|
72
|
+
|
73
|
+
By default, if the target file already exists, the file ACTION will not be performed. The '--overwrite'
|
74
|
+
option will cause the tool to compare the file dates and checksums of source and target files in that case.
|
75
|
+
Only if the checksums are different and the source file has a more recent modification date, the target file
|
76
|
+
will be overwritten. If you want to be asked for overwrite confirmation for each such file, you can add the
|
77
|
+
'--interactive' option.
|
78
|
+
|
79
|
+
The tool can generate a report on all the file actions that have been performed. To do so, specify a file
|
80
|
+
name for the '--report' option. The format of the report will be determined by the file extension you supply:
|
81
|
+
|
82
|
+
- *.csv : comma-separated file
|
83
|
+
|
84
|
+
- *.tsv : tab-separated file
|
85
|
+
|
86
|
+
- *.yml : YAML file
|
87
|
+
|
88
|
+
- *.xml : XML file
|
89
|
+
|
90
|
+
By adding the --dummy option, you can test your settings without performing the real actions on the file.
|
91
|
+
The tool will still report on its progress as if it would perform the actions.
|
92
|
+
|
93
|
+
All the options can be saved into a configuration file to be reused later. You can specify which
|
94
|
+
configuration file you want to use with the '--config' option. If you specify a configuration file, the tool
|
95
|
+
will first load the options from the configuration file and then process the command-line options. The
|
96
|
+
command-line options therefore have priority over the options in the configuration file.
|
97
|
+
|
98
|
+
By default the tool allows you to review the activated options and gives you the opportunity to modify them
|
99
|
+
before continuing of bailing out. If you are confident the settings are fine, you can skip this with the
|
100
|
+
'--unatttended' option. Handle with care!
|
101
|
+
|
102
|
+
Unless you have specified the '--unattended' options, you will be presented with a menu that allows you to
|
103
|
+
change the configuration parameters, run the tool with the current config or bail out.
|
104
|
+
|
105
|
+
DESC
|
106
|
+
|
107
|
+
method_option :base, aliases: '-b',
|
108
|
+
desc: description(:base)
|
109
|
+
method_option :filter, aliases: '-f',
|
110
|
+
desc: description(:filter)
|
111
|
+
method_option :expression, aliases: '-e',
|
112
|
+
desc: description(:expression)
|
113
|
+
|
114
|
+
method_option :action, aliases: '-a', enum: VALID_ACTIONS.keys,
|
115
|
+
desc: description(:action)
|
116
|
+
method_option :overwrite, aliases: '-o', type: :boolean,
|
117
|
+
desc: description(:overwrite)
|
118
|
+
method_option :interactive, aliases: '-i', type: :boolean,
|
119
|
+
desc: description(:interactive)
|
120
|
+
|
121
|
+
method_option :report, aliases: '-r', banner: 'FILE',
|
122
|
+
desc: description(:report)
|
123
|
+
|
124
|
+
method_option :dummy, aliases: '-d', type: :boolean,
|
125
|
+
desc: description(:dummy)
|
126
|
+
|
127
|
+
method_option :config, aliases: '-c', type: :string,
|
128
|
+
desc: description(:config)
|
129
|
+
|
130
|
+
method_option :unattended, aliases: '-u', type: :boolean,
|
131
|
+
desc: description(:unattended)
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
def reorg
|
138
|
+
@config_file_prefix = '.reorg.'
|
139
|
+
|
140
|
+
# return config_write
|
141
|
+
|
142
|
+
DEFAULT_CONFIG.each {|key, value| config.set(key, value: value) unless value.nil?}
|
143
|
+
config_read(options[:config]) if options[:config]
|
144
|
+
DEFAULT_CONFIG.each {|key, _| config.set(key, value: options[key]) if options.has_key?(key.to_s)}
|
145
|
+
run_menu unless options[:unattended]
|
146
|
+
do_reorg
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
|
151
|
+
def run_menu
|
152
|
+
|
153
|
+
begin
|
154
|
+
choices = []
|
155
|
+
|
156
|
+
choices << {name: "Configuration editor",
|
157
|
+
value: -> {config_menu; 1}
|
158
|
+
}
|
159
|
+
|
160
|
+
choices << {name: "Run", value: nil}
|
161
|
+
choices << {name: "Exit", value: -> {exit}}
|
162
|
+
|
163
|
+
selection = prompt.select "[ LIBIS Tool - ReOrg ]",
|
164
|
+
choices, cycle: true, default: 1
|
165
|
+
|
166
|
+
end until selection.nil?
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
def print_field(field)
|
171
|
+
value = config.fetch(field)
|
172
|
+
value = 'Yes' if value.is_a?(TrueClass)
|
173
|
+
value = 'No' if value.is_a?(FalseClass)
|
174
|
+
"#{STRING_CONFIG[field]} : #{pastel.green(value)}"
|
175
|
+
end
|
176
|
+
|
177
|
+
def config_menu
|
178
|
+
|
179
|
+
selection = 1
|
180
|
+
|
181
|
+
begin
|
182
|
+
choices = []
|
183
|
+
choices << {name: print_field(:base),
|
184
|
+
value: -> do
|
185
|
+
config.set :base,
|
186
|
+
value: tree_select(config.fetch(:base) || '.', question: 'Select source directory:')
|
187
|
+
1
|
188
|
+
end
|
189
|
+
}
|
190
|
+
choices << {name: print_field(:filter),
|
191
|
+
value: -> {ask 'File filter regex:', :filter; 2}
|
192
|
+
}
|
193
|
+
choices << {name: print_field(:expression),
|
194
|
+
value: -> {ask 'New path expression:', :expression; 3}
|
195
|
+
}
|
196
|
+
choices << {name: print_field(:action),
|
197
|
+
value: -> {ask 'Action:', :action, enum: VALID_ACTIONS.keys; 4}
|
198
|
+
}
|
199
|
+
choices << {name: print_field(:overwrite),
|
200
|
+
value: -> {toggle_config(:overwrite); prompt.say print_field(:overwrite); 5}
|
201
|
+
}
|
202
|
+
choices << {name: print_field(:interactive),
|
203
|
+
value: -> {toggle_config(:interactive); prompt.say print_field(:interactive); 6}
|
204
|
+
}
|
205
|
+
choices << {name: print_field(:report),
|
206
|
+
value: -> do
|
207
|
+
report = config.fetch(:report)
|
208
|
+
default = '.'
|
209
|
+
default = File.dirname(report) if report && File.file?(report)
|
210
|
+
report = tree_select(default, question: 'Select source directory',
|
211
|
+
file: true, create: true,
|
212
|
+
default_choices: [{name: "-- no report --", value: nil}])
|
213
|
+
if report
|
214
|
+
config.set(:report, value: report)
|
215
|
+
else
|
216
|
+
config.delete(:report)
|
217
|
+
end
|
218
|
+
7
|
219
|
+
end
|
220
|
+
}
|
221
|
+
choices << {name: print_field(:dummy),
|
222
|
+
value: -> {toggle_config(:dummy); prompt.say print_field(:dummy); 8}
|
223
|
+
}
|
224
|
+
choices << {name: "-- save configuration '#{get_config_name}' --",
|
225
|
+
value: -> {config_write get_config_name; 9}
|
226
|
+
} if get_config_name
|
227
|
+
choices << {name: "-- save to new configuration --",
|
228
|
+
value: -> {config_write new_config; 10}
|
229
|
+
}
|
230
|
+
choices << {name: "-- read configuration --",
|
231
|
+
value: -> {config_read; 11}
|
232
|
+
}
|
233
|
+
choices << {name: "-- return to main menu --", value: nil}
|
234
|
+
|
235
|
+
selection = prompt.select "[ Configuration menu ]",
|
236
|
+
choices, per_page: 20, cycle: true, default: selection
|
237
|
+
|
238
|
+
end until selection.nil?
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
def do_reorg
|
243
|
+
prompt.ok 'This can take a while. Please sit back and relax, grab a cup of coffee, have a quick nap or read a good book ...'
|
244
|
+
|
245
|
+
# keeps track of folders created
|
246
|
+
require 'set'
|
247
|
+
target_dir_list = Set.new
|
248
|
+
|
249
|
+
open_report
|
250
|
+
|
251
|
+
require 'fileutils'
|
252
|
+
count = {move: 0, duplicate: 0, update: 0, reject: 0, skipped_dir: 0, unmatched_file: 0}
|
253
|
+
|
254
|
+
base_dir = config.fetch(:base)
|
255
|
+
parse_regex = Regexp.new(config.fetch(:filter))
|
256
|
+
path_expression = "#{config.fetch(:expression)}"
|
257
|
+
dummy_operation = config.fetch(:dummy)
|
258
|
+
interactive = config.fetch(:interactive)
|
259
|
+
overwrite = config.fetch(:overwrite)
|
260
|
+
file_operation = config.fetch(:action)
|
261
|
+
Dir.new(base_dir).entries.each do |file_name|
|
262
|
+
next if file_name =~ /^\.\.?$/
|
263
|
+
entry = File.join(File.absolute_path(base_dir), file_name)
|
264
|
+
unless File.file?(entry)
|
265
|
+
prompt.say "Skipping directory #{entry}." unless @report
|
266
|
+
write_report(entry, '', '', 'Directory - skipped.')
|
267
|
+
count[:skipped_dir] += 1
|
268
|
+
next
|
269
|
+
end
|
270
|
+
unless file_name =~ parse_regex
|
271
|
+
prompt.say "Skipping file #{file_name}. File name does not match expression." unless @report
|
272
|
+
write_report(entry, '', '', 'Mismatch - skipped.')
|
273
|
+
count[:unmatched_file] += 1
|
274
|
+
next
|
275
|
+
end
|
276
|
+
target = eval('"' + path_expression + '"')
|
277
|
+
target_file = File.basename(target)
|
278
|
+
target_dir = File.dirname(target)
|
279
|
+
target_dir = File.join(base_dir, target_dir) unless target_dir[0] == '/'
|
280
|
+
unless target_dir_list.include?(target_dir)
|
281
|
+
prompt.say "-> Create directory '#{target_dir}'" unless @report
|
282
|
+
FileUtils.mkpath(target_dir) unless dummy_operation
|
283
|
+
target_dir_list << target_dir
|
284
|
+
end
|
285
|
+
target_path = File.join(target_dir, target_file)
|
286
|
+
remark = nil
|
287
|
+
action = false
|
288
|
+
if File.exist?(target_path)
|
289
|
+
if compare_entry(entry, target_path)
|
290
|
+
remark = 'Duplicate - skipped.'
|
291
|
+
count[:duplicate] += 1
|
292
|
+
prompt.error "Duplicate file entry: #{entry}." unless @report
|
293
|
+
else
|
294
|
+
# puts "source: #{File.mtime(entry)} #{'%11s' % Filesize.new(File.size(entry)).pretty} #{entry}"
|
295
|
+
# puts "target: #{File.mtime(target_path)} #{'%11s' % Filesize.new(File.size(target_path)).pretty} #{target_path}"
|
296
|
+
if interactive ? prompt.send((overwrite ? :yes : :no), 'Overwrite target?') : overwrite
|
297
|
+
remark = 'Duplicate - updated'
|
298
|
+
action = true
|
299
|
+
count[:update] += 1
|
300
|
+
else
|
301
|
+
remark = 'Duplicate - rejected.'
|
302
|
+
prompt.error "ERROR: #{entry} exists with different content." unless @report
|
303
|
+
count[:reject] += 1
|
304
|
+
end
|
305
|
+
end
|
306
|
+
else
|
307
|
+
action = true
|
308
|
+
count[:move] += 1
|
309
|
+
end
|
310
|
+
if action
|
311
|
+
prompt.say "-> #{file_operation} '#{file_name}' to '#{target}'" unless @report
|
312
|
+
case file_operation
|
313
|
+
when 'move'
|
314
|
+
FileUtils.move(entry, File.join(target_dir, target_file), force: true)
|
315
|
+
when 'copy'
|
316
|
+
FileUtils.copy(entry, File.join(target_dir, target_file))
|
317
|
+
when 'link'
|
318
|
+
FileUtils.symlink(entry, File.join(target_dir, target_file), force: true)
|
319
|
+
else
|
320
|
+
# Shouldn't happen
|
321
|
+
raise RuntimeError, "Bad file operation: '#{file_operation}'"
|
322
|
+
end unless dummy_operation
|
323
|
+
end
|
324
|
+
write_report(entry, target_dir, target_file, remark)
|
325
|
+
end
|
326
|
+
|
327
|
+
prompt.ok "#{'%8d' % count[:skipped_dir]} dir(s) found and skipped."
|
328
|
+
prompt.ok "#{'%8d' % count[:unmatched_file]} file(s) found that did not match and skipped."
|
329
|
+
prompt.ok "#{'%8d' % count[:move]} file(s) #{VALID_ACTIONS[file_operation]}."
|
330
|
+
prompt.ok "#{'%8d' % count[:duplicate]} duplicate(s) found and skipped."
|
331
|
+
prompt.ok "#{'%8d' % count[:update]} changed file(s) found and updated."
|
332
|
+
prompt.ok "#{'%8d' % count[:reject]} changed file(s) found and rejected."
|
333
|
+
|
334
|
+
close_report
|
335
|
+
|
336
|
+
prompt.ok 'Done!'
|
337
|
+
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
def open_report
|
342
|
+
if (report_file = config.fetch(:report))
|
343
|
+
# noinspection RubyStringKeysInHashInspection
|
344
|
+
@report_type = {'.csv' => :csv, '.tsv' => :tsv, '.xml' => :xml, '.yml' => :yml}[File.extname(report_file)]
|
345
|
+
unless @report_type
|
346
|
+
prompt.error "Unknown file type: #{File.extname(report_file)}"
|
347
|
+
exit
|
348
|
+
end
|
349
|
+
@report = File.open(report_file, 'w+')
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def for_tsv(string)
|
354
|
+
; string =~ /\t\n/ ? "\"#{string.gsub('"', '""')}\"" : string;
|
355
|
+
end
|
356
|
+
|
357
|
+
def for_csv(string)
|
358
|
+
; string =~ /,\n/ ? "\"#{string.gsub('"', '""')}\"" : string;
|
359
|
+
end
|
360
|
+
|
361
|
+
def for_xml(string, type = :attr)
|
362
|
+
; string.encode(xml: type);
|
363
|
+
end
|
364
|
+
|
365
|
+
def for_yml(string)
|
366
|
+
; string.inspect.to_yaml;
|
367
|
+
end
|
368
|
+
|
369
|
+
def write_report(old_name, new_folder, new_name, remark = nil)
|
370
|
+
return unless @report
|
371
|
+
case @report_type
|
372
|
+
when :tsv
|
373
|
+
@report.puts "old_name\tnew_folder\tnew_name\tremark" if @report.size == 0
|
374
|
+
@report.puts "#{for_tsv(old_name)}\t#{for_tsv(new_folder)}" +
|
375
|
+
"\t#{for_tsv(new_name)}\t#{for_tsv(remark)}"
|
376
|
+
when :csv
|
377
|
+
@report.puts 'old_name,new_folder,new_name' if @report.size == 0
|
378
|
+
@report.puts "#{for_csv(old_name)},#{for_csv(new_folder)}" +
|
379
|
+
",#{for_csv(new_name)},#{for_csv(remark)}"
|
380
|
+
when :xml
|
381
|
+
@report.puts '<?xml version="1.0" encoding="UTF-8"?>' if @report.size == 0
|
382
|
+
@report.puts '<report>' if @report.size == 1
|
383
|
+
@report.puts ' <file>'
|
384
|
+
@report.puts " <old_name>#{for_xml(old_name, :text)}</old_name>"
|
385
|
+
@report.puts " <new_folder>#{for_xml(new_folder, :text)}</new_folder>"
|
386
|
+
@report.puts " <new_name>#{for_xml(new_name, :text)}</new_name>"
|
387
|
+
@report.puts " <remark>#{for_xml(remark, :text)}</remark>" if remark
|
388
|
+
@report.puts ' </file>'
|
389
|
+
when :yml
|
390
|
+
@report.puts '# Reorganisation report' if @report.size == 0
|
391
|
+
@report.puts "- old_name: #{for_yml(old_name)}" +
|
392
|
+
"\n new_folder: #{for_yml(new_folder)}" +
|
393
|
+
"\n new_name: #{for_yml(new_name)}" +
|
394
|
+
(remark ? "\n remark: #{for_yml(remark)}" : '')
|
395
|
+
else
|
396
|
+
#nothing
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def close_report
|
401
|
+
return unless @report
|
402
|
+
if @report_type == :xml
|
403
|
+
@report.puts '</report>'
|
404
|
+
end
|
405
|
+
@report.close
|
406
|
+
end
|
407
|
+
|
408
|
+
def compare_entry(src, tgt)
|
409
|
+
hasher = Libis::Tools::Checksum.new(:SHA256)
|
410
|
+
hasher.digest(src) == hasher.digest(tgt)
|
411
|
+
end
|
412
|
+
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|