libis-tools 1.0.5-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|