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.
- checksums.yaml +7 -0
- data/bin/dirmangle +337 -0
- 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: []
|