dirmangle 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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/bin/dirmangle +337 -0
  3. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e785caf27f388cd1f1bf8ed22afae13d55646a6
4
+ data.tar.gz: 391573317c143d119ade15d0e9ee57e795ac8ff9
5
+ SHA512:
6
+ metadata.gz: d362686353e2d04b8493adad07a3cdd77306c79135d8beff514695d45a7819287f4341a11aa667451503484a6cda61d09b563a4733c6c6e754490d7c0673682e
7
+ data.tar.gz: 7a635bd09cd365676fc5e2687cb14fb58e49dcfa3c1731d9f3a63a57e1542937979d86389b673a0048b846e6cf7ef95f1b47dd82e1388a59c18f169058baf78f
data/bin/dirmangle ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # dirmangle - mirror and mangle path components of a directory tree
4
+ #
5
+ # DESCRIPTION
6
+ # This script creates a mirror of a source directory, manipulating
7
+ # path components based on rules defined in dirmangle.yaml. The
8
+ # purpose of this is to provide links to (or copies of) data using
9
+ # an alternative directory tree.
10
+ #
11
+ # -C, --config FILE
12
+ # The location of the configuration YAML to load
13
+ #
14
+ # -n, --noop
15
+ # Do everything but actually modify the filesystem
16
+ #
17
+ # -r, --rules RULES
18
+ # Only execute the comma separated list of rules
19
+ #
20
+ # -v, --verbosity LEVEL
21
+ # Set the log verbosity level [debug, info (default), warn, error, fatal]
22
+ #
23
+ # AUTHOR
24
+ # Gary Hetzel <garyhetzel@gmail.com>
25
+ #
26
+ require 'yaml'
27
+ require 'logger'
28
+ require 'fileutils'
29
+
30
+ DEFAULT_CONF=['~/.config/dirmangle/dirmangle.yaml', '/etc/dirmangle.yaml']
31
+
32
+ class String
33
+ def to_bool
34
+ return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
35
+ return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
36
+ raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
37
+ end
38
+
39
+ def titleize
40
+ downcase.split.map{|i| exceptions.include?(i) ? i : i.capitalize }.join(' ').sentencecase
41
+ end
42
+
43
+ def sentencecase
44
+ self[0,1].upcase + self[1,length-1]
45
+ end
46
+
47
+ private
48
+ def exceptions
49
+ %w{a an and at but by for from if in nor of on or out over the to}
50
+ end
51
+ end
52
+
53
+ class Dirmangle
54
+ def initialize(options={})
55
+ @options = options
56
+ @writecount = 0
57
+ @paths = []
58
+
59
+ # initialize logging
60
+ setup_logger()
61
+
62
+ # alert on no-op
63
+ Dirmangle.log('No-op mode: nothing will actually be modified') if @options[:noop]
64
+
65
+ # load and process config
66
+ process_config()
67
+
68
+ # make changes
69
+ create_paths()
70
+
71
+ # yay we're done!
72
+ Dirmangle.log("Operation complete. #{@writecount} entries written.")
73
+ Dirmangle.log('No-op mode: nothing actually happened') if @options[:noop]
74
+ end
75
+
76
+ private
77
+ def setup_logger()
78
+ @@logger = Logger.new($stderr)
79
+
80
+ @@logger.formatter = proc do |severity, datetime, progname, msg|
81
+ "[%s] %5s: %s\n" % [datetime, severity, msg]
82
+ end
83
+
84
+ # normalize loglevel option
85
+ @options[:loglevel] ||= :info
86
+
87
+ case @options[:loglevel].to_s.to_sym
88
+ when :debug
89
+ @@logger.level = Logger::DEBUG
90
+ when :warn
91
+ @@logger.level = Logger::WARN
92
+ when :error
93
+ @@logger.level = Logger::ERROR
94
+ when :fatal
95
+ @@logger.level = Logger::FATAL
96
+ else
97
+ @@logger.level = Logger::INFO
98
+ end
99
+ end
100
+
101
+
102
+ def process_config()
103
+ confFile = (@options[:confPath] || DEFAULT_CONF)
104
+
105
+ [*confFile].each do |c|
106
+ c = File.expand_path(c)
107
+ if File.exists?(c)
108
+ @config = YAML.load_file(c)
109
+ break
110
+ end
111
+ end
112
+
113
+ raise "Could not find configuration file" if not @config
114
+
115
+ if @config['mangle']
116
+ @config['mangle']['rules'].each do |title, config|
117
+ if not (@options[:rules].empty? || @options[:rules].include?(title))
118
+ Dirmangle.log("Rule '#{title}' not being processed", :debug)
119
+ next
120
+ end
121
+
122
+ if not (config.has_key?('source') and config.has_key?('destination') and config.has_key?('action'))
123
+ Dirmangle.log("Rule configuration for '#{title}' requires a source, destination, and action; skipping", :debug)
124
+ end
125
+
126
+ Dirmangle.log("Processing rule '#{title}'...")
127
+ @source_root = File.expand_path(config['root'] || @config['mangle']['root'] || File.dirname(__FILE__)).sub(/\/$/, '')
128
+ @destination_root = File.expand_path(config['destination_root'] || @source_root)
129
+ @source = config['source']
130
+
131
+ # change to root directory
132
+ begin
133
+ Dir.chdir(@source_root)
134
+ rescue
135
+ Dirmangle.log("No such directory #{@source_root}, skipping rule #{title}")
136
+ next
137
+ end
138
+
139
+ source_rx = %r|#{@source}|
140
+ parts = @source.split(%r|(?!\\)/|).reject{|i| i.empty? }
141
+ source_glob = File.join("", parts.collect{|i| (i =~ /\W+/ ? '*' : i) })
142
+
143
+ # recursively list all files and mangle their paths
144
+ Dir[File.join(@source_root, source_glob)].each do |path|
145
+ if path =~ source_rx
146
+ source_match = $~
147
+
148
+ # for one or more destination paths
149
+ [*config['destination']].uniq.each do |d|
150
+ destination = d.gsub(%r|%\((?<name>\w+)(?:\.(?<eval>[^\)]+))?(?:\:(?<format>[^\)]+))?\)|) do |match|
151
+ eval_stack = ($~[:eval].split('.').reject{|i| i.empty? } rescue [])
152
+ rv = source_match[$~[:name]]
153
+
154
+ # evaluate methods
155
+ eval_stack.each do |e|
156
+ rv = rv.send(e)
157
+ end rescue nil
158
+
159
+ ($~[:format] ? "%#{$~[:format]}" % rv : rv)
160
+ end
161
+
162
+ source = File.expand_path(path)
163
+ destination = File.join(@destination_root, destination)
164
+
165
+ # add this path translation rule to the processing stack
166
+ add_path(title, source, destination, config)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+
175
+ def create_paths()
176
+ # step through paths array in triples
177
+ @paths.each do |path|
178
+ tdir = File.dirname(path[:destination])
179
+
180
+ if not File.directory?(tdir)
181
+ Dirmangle.log("Creating directory #{tdir}", :debug)
182
+ FileUtils.mkdir_p(tdir, {
183
+ :noop => @options[:noop]
184
+ })
185
+ end
186
+
187
+ # remove trailing slash from destination (if present) so link gets created at
188
+ # that path, not *in* it
189
+ path[:destination] = path[:destination].sub(/\/$/,'')
190
+
191
+ begin
192
+ print_source = path[:source].sub(@source_root,'').sub(/^\//,'')
193
+ print_destination = path[:destination].sub(@destination_root,'').sub(/^\//,'')
194
+
195
+ # perform the specified operations on processed files
196
+ case path[:operation]
197
+ when :copy
198
+ Dirmangle.log("Copying #{print_source} -> #{print_destination}")
199
+ FileUtils.cp(path[:source], path[:destination], {
200
+ :noop => @options[:noop],
201
+ :preserve => true
202
+ })
203
+
204
+ when :move
205
+ Dirmangle.log("Moving #{print_source} -> #{print_destination}")
206
+ FileUtils.mv(path[:source], path[:destination], {
207
+ :noop => @options[:noop],
208
+ :preserve => true
209
+ })
210
+
211
+ when :hardlink
212
+ Dirmangle.log("Hard linking #{print_source} -> #{print_destination}", :debug)
213
+ FileUtils.ln(path[:source], path[:destination], {
214
+ :noop => @options[:noop]
215
+ })
216
+
217
+ when :link
218
+ Dirmangle.log("Linking #{print_source} -> #{print_destination}", :debug)
219
+ FileUtils.ln_s(path[:source], path[:destination], {
220
+ :noop => @options[:noop]
221
+ })
222
+
223
+ else
224
+ Dirmangle.log("Unknown operation #{path[:operation]} for rule #{path[:rule]}, skipping execution")
225
+ break
226
+ end
227
+
228
+ @writecount += 1
229
+
230
+ rescue Errno::EEXIST
231
+ #Dirmangle.log("Destination path #{print_destination} already exists, skipping", :error)
232
+ next
233
+ end
234
+ end
235
+
236
+ # prune empty directories from target
237
+ prune_empty_directories(@destination_root)
238
+ end
239
+
240
+
241
+ def prune_empty_directories(root, options={})
242
+ root = File.expand_path(root)
243
+
244
+ # proc for removing an empty directory
245
+ remover = Proc.new do |dir|
246
+ if File.directory?(dir)
247
+ if Dir.entries(dir).empty?
248
+ Dirmangle.log("Pruning empty directory #{dir}", :debug)
249
+ FileUtils.rmdir(dir, {
250
+ :noop => @options[:noop]
251
+ })
252
+ end
253
+ end
254
+ end
255
+
256
+ # remove all empty subdirectories
257
+ Dir[File.join(root, '**', '*')].sort.reverse.each do |entry|
258
+ remover.call(entry)
259
+ end
260
+
261
+ # remove an empty root directory (if specified)
262
+ if options[:remove_root]
263
+ remover.call(root)
264
+ end
265
+ end
266
+
267
+
268
+ def add_path(rule, source, destination, options={})
269
+ # push a new path pair to have some operation performed upon it
270
+ if source && destination
271
+ options[:operation] = :link if not options[:operation]
272
+
273
+ Dirmangle.log("Adding #{options[:operation]} operation from #{source} -> #{destination}", :debug)
274
+
275
+ @paths << {
276
+ :rule => rule,
277
+ :source => source,
278
+ :destination => destination,
279
+ :operation => options[:operation].to_s.strip.to_sym,
280
+ :force => (options[:force] ? true : false)
281
+ }
282
+ end
283
+ end
284
+
285
+
286
+ def Dirmangle.log(message, severity = :info)
287
+ severity = :info unless [:debug, :info, :warn, :error, :fatal].include?(severity.to_sym)
288
+ @@logger.send(severity, message)
289
+ end
290
+ end
291
+
292
+
293
+ require 'optparse'
294
+
295
+ options = {}
296
+
297
+ parser = OptionParser.new do |opts|
298
+ opts.banner = "Usage: dirmangle [options]"
299
+
300
+ # -----------------------------------------------------------------------------
301
+ opts.on('-C', '--config FILE', 'Configuration YAML location') do |file|
302
+ options[:confPath] = file
303
+ end
304
+
305
+ # -----------------------------------------------------------------------------
306
+ options[:noop] = false
307
+ opts.on('-n', '--noop', 'Dry run (do not modify filesystem)') do
308
+ options[:noop] = true
309
+ end
310
+
311
+ # -----------------------------------------------------------------------------
312
+ opts.on('-v', '--verbosity LEVEL', 'Logging verbosity') do |level|
313
+ level = level.to_s.downcase
314
+
315
+ if ['debug', 'info', 'warn', 'error', 'fatal'].include?(level)
316
+ options[:loglevel] = level.to_sym
317
+ end
318
+ end
319
+
320
+ # -----------------------------------------------------------------------------
321
+ options[:rules] = []
322
+ opts.on('-r', '--rules RULES', 'Only execute the comma separated list of rules') do |rules|
323
+ options[:rules] = rules.split(',')
324
+ end
325
+ end
326
+
327
+ parser.parse!
328
+
329
+ # begin
330
+ Dirmangle.new(options)
331
+ # rescue Exception => e
332
+ # Dirmangle.log(e.message, :fatal)
333
+
334
+ # e.backtrace.each do |m|
335
+ # Dirmangle.log(m, :debug)
336
+ # end
337
+ # end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dirmangle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Gary Hetzel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-03-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: dirmangle is a tool for copying, moving, or creating symlinks to sets
14
+ of files using regular expressions
15
+ email: garyhetzel@gmail.com
16
+ executables:
17
+ - dirmangle
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/dirmangle
22
+ homepage: https://github.com/ghetzel/dirmangle
23
+ licenses: []
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.0.0
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Batch directory clone, move, and link tool
45
+ test_files: []