sfn 0.0.1 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +107 -0
- data/LICENSE +13 -0
- data/README.md +142 -61
- data/bin/sfn +43 -0
- data/lib/chef/knife/knife_plugin_seed.rb +117 -0
- data/lib/sfn.rb +17 -0
- data/lib/sfn/cache.rb +385 -0
- data/lib/sfn/command.rb +45 -0
- data/lib/sfn/command/create.rb +87 -0
- data/lib/sfn/command/describe.rb +87 -0
- data/lib/sfn/command/destroy.rb +74 -0
- data/lib/sfn/command/events.rb +98 -0
- data/lib/sfn/command/export.rb +103 -0
- data/lib/sfn/command/import.rb +117 -0
- data/lib/sfn/command/inspect.rb +160 -0
- data/lib/sfn/command/list.rb +59 -0
- data/lib/sfn/command/promote.rb +17 -0
- data/lib/sfn/command/update.rb +95 -0
- data/lib/sfn/command/validate.rb +34 -0
- data/lib/sfn/command_module.rb +9 -0
- data/lib/sfn/command_module/base.rb +150 -0
- data/lib/sfn/command_module/stack.rb +166 -0
- data/lib/sfn/command_module/template.rb +147 -0
- data/lib/sfn/config.rb +106 -0
- data/lib/sfn/config/create.rb +35 -0
- data/lib/sfn/config/describe.rb +19 -0
- data/lib/sfn/config/destroy.rb +9 -0
- data/lib/sfn/config/events.rb +25 -0
- data/lib/sfn/config/export.rb +29 -0
- data/lib/sfn/config/import.rb +24 -0
- data/lib/sfn/config/inspect.rb +37 -0
- data/lib/sfn/config/list.rb +25 -0
- data/lib/sfn/config/promote.rb +23 -0
- data/lib/sfn/config/update.rb +20 -0
- data/lib/sfn/config/validate.rb +49 -0
- data/lib/sfn/monkey_patch.rb +8 -0
- data/lib/sfn/monkey_patch/stack.rb +200 -0
- data/lib/sfn/provider.rb +224 -0
- data/lib/sfn/utils.rb +23 -0
- data/lib/sfn/utils/debug.rb +31 -0
- data/lib/sfn/utils/json.rb +37 -0
- data/lib/sfn/utils/object_storage.rb +28 -0
- data/lib/sfn/utils/output.rb +79 -0
- data/lib/sfn/utils/path_selector.rb +99 -0
- data/lib/sfn/utils/ssher.rb +29 -0
- data/lib/sfn/utils/stack_exporter.rb +275 -0
- data/lib/sfn/utils/stack_parameter_scrubber.rb +37 -0
- data/lib/sfn/utils/stack_parameter_validator.rb +124 -0
- data/lib/sfn/version.rb +4 -0
- data/sfn.gemspec +19 -0
- metadata +110 -4
data/lib/sfn/provider.rb
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'sfn'
|
3
|
+
|
4
|
+
module Sfn
|
5
|
+
# Remote provider interface
|
6
|
+
class Provider
|
7
|
+
|
8
|
+
include Bogo::AnimalStrings
|
9
|
+
|
10
|
+
# Minimum number of seconds to wait before re-expanding in
|
11
|
+
# progress stack
|
12
|
+
STACK_EXPAND_INTERVAL = 45
|
13
|
+
|
14
|
+
# Default interval for refreshing stack list in cache
|
15
|
+
STACK_LIST_INTERVAL = 120
|
16
|
+
|
17
|
+
# @return [Miasma::Models::Orchestration]
|
18
|
+
attr_reader :connection
|
19
|
+
# @return [Cache]
|
20
|
+
attr_reader :cache
|
21
|
+
# @return [Thread, NilClass] stack list updater
|
22
|
+
attr_accessor :updater
|
23
|
+
# @return [TrueClass, FalseClass] async updates
|
24
|
+
attr_reader :async
|
25
|
+
# @return [Logger, NilClass] logger in use
|
26
|
+
attr_reader :logger
|
27
|
+
# @return [Numeric] interval between stack expansions
|
28
|
+
attr_reader :stack_expansion_interval
|
29
|
+
# @return [Numeric] interval between stack list updates
|
30
|
+
attr_reader :stack_list_interval
|
31
|
+
|
32
|
+
# Create new instance
|
33
|
+
#
|
34
|
+
# @param args [Hash]
|
35
|
+
# @option args [Hash] :miasma miasma connection hash
|
36
|
+
# @option args [Cache] :cache
|
37
|
+
# @option args [TrueClass, FalseClass] :async fetch stacks async (defaults true)
|
38
|
+
# @option args [Logger] :logger use custom logger
|
39
|
+
# @option args [Numeric] :stack_expansion_interval interval to wait between stack data expands
|
40
|
+
# @option args [Numeric] :stack_list_interval interval to wait between stack list refresh
|
41
|
+
def initialize(args={})
|
42
|
+
args = args.to_smash
|
43
|
+
unless(args[:miasma][:provider])
|
44
|
+
best_guess = args[:miasma].keys.group_by do |key|
|
45
|
+
key.to_s.split('_').first
|
46
|
+
end.sort do |x, y|
|
47
|
+
y.size <=> x.size
|
48
|
+
end.first
|
49
|
+
if(best_guess)
|
50
|
+
provider = best_guess.first.to_sym
|
51
|
+
else
|
52
|
+
raise ArgumentError.new 'Cannot auto determine :provider value for credentials'
|
53
|
+
end
|
54
|
+
else
|
55
|
+
provider = args[:miasma].delete(:provider).to_sym
|
56
|
+
end
|
57
|
+
if(provider == :aws)
|
58
|
+
if(args[:miasma][:region])
|
59
|
+
args[:miasma][:aws_region] = args[:miasma].delete(:region)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
if(ENV['DEBUG'].to_s.downcase == 'true')
|
63
|
+
log_to = STDOUT
|
64
|
+
else
|
65
|
+
if(Gem.win_platform?)
|
66
|
+
log_to = 'NUL'
|
67
|
+
else
|
68
|
+
log_to = '/dev/null'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@logger = args.fetch(:logger, Logger.new(log_to))
|
72
|
+
@stack_expansion_interval = args.fetch(:stack_expansion_interval, STACK_EXPAND_INTERVAL)
|
73
|
+
@stack_list_interval = args.fetch(:stack_list_interval, STACK_LIST_INTERVAL)
|
74
|
+
@connection = Miasma.api(
|
75
|
+
:provider => provider,
|
76
|
+
:type => :orchestration,
|
77
|
+
:credentials => args[:miasma]
|
78
|
+
)
|
79
|
+
@cache = args.fetch(:cache, Cache.new(:local))
|
80
|
+
@async = args.fetch(:async, true)
|
81
|
+
@miamsa_args = args[:miasma].dup
|
82
|
+
cache.init(:stacks_lock, :lock, :timeout => 0.1)
|
83
|
+
cache.init(:stacks, :stamped)
|
84
|
+
cache.init(:stack_expansion_lock, :lock, :timeout => 0.1)
|
85
|
+
if(args.fetch(:fetch, false))
|
86
|
+
async ? update_stack_list! : fetch_stacks
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Miasma::Orchestration::Stacks]
|
91
|
+
def stacks
|
92
|
+
connection.stacks.from_json(cached_stacks)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [String] json representation of cached stacks
|
96
|
+
def cached_stacks
|
97
|
+
fetch_stacks unless @initial_fetch_complete
|
98
|
+
value = cache[:stacks].value
|
99
|
+
value ? MultiJson.dump(MultiJson.load(value).values) : '[]'
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Miasma::Orchestration::Stack, NilClass]
|
103
|
+
def stack(stack_id)
|
104
|
+
stacks.get(stack_id)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Store stack attribute changes
|
108
|
+
#
|
109
|
+
# @param stack_id [String]
|
110
|
+
# @param stack_attributes [Hash]
|
111
|
+
# @return [TrueClass]
|
112
|
+
def save_expanded_stack(stack_id, stack_attributes)
|
113
|
+
current_stacks = MultiJson.load(cached_stacks)
|
114
|
+
cache.locked_action(:stacks_lock) do
|
115
|
+
logger.info "Saving expanded stack attributes in cache (#{stack_id})"
|
116
|
+
current_stacks[stack_id] = stack_attributes.merge('Cached' => Time.now.to_i)
|
117
|
+
cache[:stacks].value = MultiJson.dump(current_stacks)
|
118
|
+
end
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
# Remove stack from the cache
|
123
|
+
#
|
124
|
+
# @param stack_id [String]
|
125
|
+
# @return [TrueClass, FalseClass]
|
126
|
+
def remove_stack(stack_id)
|
127
|
+
current_stacks = MultiJson.load(cached_stacks)
|
128
|
+
logger.info "Attempting to remove stack from internal cache (#{stack_id})"
|
129
|
+
cache.locked_action(:stacks_lock) do
|
130
|
+
val = current_stacks.delete(stack_id)
|
131
|
+
logger.info "Successfully removed stack from internal cache (#{stack_id})"
|
132
|
+
cache[:stacks].value = MultiJson.dump(current_stacks)
|
133
|
+
!!val
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Expand all lazy loaded attributes within stack
|
138
|
+
#
|
139
|
+
# @param stack [Miasma::Models::Orchestration::Stack]
|
140
|
+
def expand_stack(stack)
|
141
|
+
logger.info "Stack expansion requested (#{stack.id})"
|
142
|
+
if((stack.in_progress? && Time.now.to_i - stack.attributes['Cached'].to_i > stack_expansion_interval) ||
|
143
|
+
!stack.attributes['Cached'])
|
144
|
+
begin
|
145
|
+
expanded = false
|
146
|
+
cache.locked_action(:stack_expansion_lock) do
|
147
|
+
expanded = true
|
148
|
+
stack.reload
|
149
|
+
stack.data['Cached'] = Time.now.to_i
|
150
|
+
end
|
151
|
+
if(expanded)
|
152
|
+
save_expanded_stack(stack.id, stack.to_json)
|
153
|
+
end
|
154
|
+
rescue => e
|
155
|
+
logger.error "Stack expansion failed (#{stack.id}) - #{e.class}: #{e}"
|
156
|
+
end
|
157
|
+
else
|
158
|
+
logger.info "Stack has been cached within expand interval. Expansion prevented. (#{stack.id})"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Request stack information and store in cache
|
163
|
+
#
|
164
|
+
# @return [TrueClass]
|
165
|
+
def fetch_stacks
|
166
|
+
cache.locked_action(:stacks_lock) do
|
167
|
+
logger.info "Lock aquired for stack update. Requesting stacks from upstream. (#{Thread.current})"
|
168
|
+
stacks = Hash[
|
169
|
+
connection.stacks.reload.all.map do |stack|
|
170
|
+
[stack.id, stack.attributes]
|
171
|
+
end
|
172
|
+
]
|
173
|
+
if(cache[:stacks].value)
|
174
|
+
existing_stacks = MultiJson.load(cache[:stacks].value)
|
175
|
+
# Force common types
|
176
|
+
stacks = MultiJson.load(MultiJson.dump(stacks))
|
177
|
+
# Remove stacks that have been deleted
|
178
|
+
stale_ids = existing_stacks.keys - stacks.keys
|
179
|
+
stacks = existing_stacks.to_smash.deep_merge(stacks)
|
180
|
+
stale_ids.each do |stale_id|
|
181
|
+
stacks.delete(stale_id)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
cache[:stacks].value = stacks.to_json
|
185
|
+
logger.info 'Stack list has been updated from upstream and cached locally'
|
186
|
+
end
|
187
|
+
@initial_fetch_complete = true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Start async stack list update. Creates thread that loops every
|
191
|
+
# `self.stack_list_interval` seconds and refreshes stack list in cache
|
192
|
+
#
|
193
|
+
# @return [TrueClass, FalseClass]
|
194
|
+
def update_stack_list!
|
195
|
+
if(updater.nil? || !updater.alive?)
|
196
|
+
self.updater = Thread.new{
|
197
|
+
loop do
|
198
|
+
begin
|
199
|
+
fetch_stacks
|
200
|
+
sleep(stack_list_interval)
|
201
|
+
rescue => e
|
202
|
+
logger.error "Failure encountered on stack fetch: #{e.class} - #{e}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
}
|
206
|
+
true
|
207
|
+
else
|
208
|
+
false
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Build API connection for service type
|
213
|
+
#
|
214
|
+
# @param service [String, Symbol]
|
215
|
+
# @return [Miasma::Model]
|
216
|
+
def service_for(service)
|
217
|
+
connection.api_for(service)
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Release the monkeys!
|
224
|
+
Sfn::MonkeyPatch::Stack
|
data/lib/sfn/utils.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
# Utility classes and modules
|
5
|
+
module Utils
|
6
|
+
|
7
|
+
autoload :Output, 'sfn/utils/output'
|
8
|
+
autoload :StackParameterValidator, 'sfn/utils/stack_parameter_validator'
|
9
|
+
autoload :StackParameterScrubber, 'sfn/utils/stack_parameter_scrubber'
|
10
|
+
autoload :StackExporter, 'sfn/utils/stack_exporter'
|
11
|
+
autoload :Debug, 'sfn/utils/debug'
|
12
|
+
autoload :JSON, 'sfn/utils/json'
|
13
|
+
autoload :Ssher, 'sfn/utils/ssher'
|
14
|
+
autoload :ObjectStorage, 'sfn/utils/object_storage'
|
15
|
+
autoload :PathSelector, 'sfn/utils/path_selector'
|
16
|
+
|
17
|
+
# Provide methods directly from module for previous version compatibility
|
18
|
+
extend JSON
|
19
|
+
extend ObjectStorage
|
20
|
+
extend Bogo::AnimalStrings
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
module Utils
|
5
|
+
# Debug helpers
|
6
|
+
module Debug
|
7
|
+
# Output helpers
|
8
|
+
module Output
|
9
|
+
# Write debug message
|
10
|
+
#
|
11
|
+
# @param msg [String]
|
12
|
+
def debug(msg)
|
13
|
+
puts "<SparkleFormation>: #{msg}" if ENV['DEBUG']
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Load module into class
|
19
|
+
#
|
20
|
+
# @param klass [Class]
|
21
|
+
def included(klass)
|
22
|
+
klass.class_eval do
|
23
|
+
include Output
|
24
|
+
extend Output
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
# JSON helper methods
|
7
|
+
module JSON
|
8
|
+
|
9
|
+
# Convert to JSON
|
10
|
+
#
|
11
|
+
# @param thing [Object]
|
12
|
+
# @return [String]
|
13
|
+
def _to_json(thing)
|
14
|
+
MultiJson.dump(thing)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Load JSON data
|
18
|
+
#
|
19
|
+
# @param thing [String]
|
20
|
+
# @return [Object]
|
21
|
+
def _from_json(thing)
|
22
|
+
MultiJson.load(thing)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Format object into pretty JSON
|
26
|
+
#
|
27
|
+
# @param thing [Object]
|
28
|
+
# @return [String]
|
29
|
+
def _format_json(thing)
|
30
|
+
thing = _from_json(thing) if thing.is_a?(String)
|
31
|
+
MultiJson.dump(thing, :pretty => true)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
# Storage helpers
|
7
|
+
module ObjectStorage
|
8
|
+
|
9
|
+
# Write to file
|
10
|
+
#
|
11
|
+
# @param object [Object]
|
12
|
+
# @param path [String] path to write object
|
13
|
+
# @param directory [Miasma::Models::Storage::Directory]
|
14
|
+
# @return [String] file path
|
15
|
+
def file_store(object, path, directory)
|
16
|
+
raise NotImplementedError.new 'Internal updated required! :('
|
17
|
+
content = object.is_a?(String) ? object : Utils._format_json(object)
|
18
|
+
directory.files.create(
|
19
|
+
:identity => path,
|
20
|
+
:body => content
|
21
|
+
)
|
22
|
+
loc = directory.service.service.name.split('::').last.downcase
|
23
|
+
"#{loc}://#{directory.identity}/#{path}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
module Utils
|
5
|
+
# Output Helpers
|
6
|
+
module Output
|
7
|
+
|
8
|
+
# Process things and return items
|
9
|
+
#
|
10
|
+
# @param things [Array] items to process
|
11
|
+
# @param args [Hash] options
|
12
|
+
# @option args [TrueClass, FalseClass] :flat flatten result array
|
13
|
+
# @option args [Array] :attributes attributes to extract
|
14
|
+
# @todo this was extracted from events and needs to be cleaned up
|
15
|
+
def process(things, args={})
|
16
|
+
@event_ids ||= []
|
17
|
+
processed = things.reverse.map do |thing|
|
18
|
+
next if @event_ids.include?(thing['id'])
|
19
|
+
@event_ids.push(thing['id']).compact!
|
20
|
+
if(args[:attributes])
|
21
|
+
args[:attributes].map do |key|
|
22
|
+
thing[key].to_s
|
23
|
+
end
|
24
|
+
else
|
25
|
+
thing.values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
args[:flat] ? processed.flatten : processed
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generate formatted titles
|
32
|
+
#
|
33
|
+
# @param thing [Object] thing being processed
|
34
|
+
# @param args [Hash]
|
35
|
+
# @option args [Array] :attributes
|
36
|
+
# @return [Array<String>] formatted titles
|
37
|
+
def get_titles(thing, args={})
|
38
|
+
attrs = args[:attributes] || []
|
39
|
+
if(attrs.empty?)
|
40
|
+
hash = thing.is_a?(Array) ? thing.first : thing
|
41
|
+
hash ||= {}
|
42
|
+
attrs = hash.keys
|
43
|
+
end
|
44
|
+
titles = attrs.map do |key|
|
45
|
+
camel(key).gsub(/([a-z])([A-Z])/, '\1 \2')
|
46
|
+
end.compact
|
47
|
+
if(args[:format])
|
48
|
+
titles.map{|s| @ui.color(s, :bold)}
|
49
|
+
else
|
50
|
+
titles
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Output stack related things in nice format
|
55
|
+
#
|
56
|
+
# @param stack [String] name of stack
|
57
|
+
# @param things [Array] things to display
|
58
|
+
# @param what [String] description of things for output
|
59
|
+
# @param args [Symbol] options (:ignore_empty_output)
|
60
|
+
def things_output(stack, things, what, *args)
|
61
|
+
unless(args.include?(:no_title))
|
62
|
+
output = get_titles(things, :format => true, :attributes => allowed_attributes)
|
63
|
+
else
|
64
|
+
output = []
|
65
|
+
end
|
66
|
+
columns = allowed_attributes.size
|
67
|
+
output += process(things, :flat => true, :attributes => allowed_attributes)
|
68
|
+
output.compact!
|
69
|
+
if(output.empty?)
|
70
|
+
ui.warn 'No information found' unless args.include?(:ignore_empty_output)
|
71
|
+
else
|
72
|
+
ui.info "#{what.to_s.capitalize} for stack: #{ui.color(stack, :bold)}" if stack
|
73
|
+
ui.info "#{ui.list(output, :uneven_columns_across, columns)}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'sfn'
|
2
|
+
|
3
|
+
module Sfn
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
# Helper methods for path selection
|
7
|
+
module PathSelector
|
8
|
+
|
9
|
+
# Humanize the base name of path
|
10
|
+
#
|
11
|
+
# @param path [String]
|
12
|
+
# @return [String]
|
13
|
+
def humanize_path_basename(path)
|
14
|
+
File.basename(path).sub(
|
15
|
+
File.extname(path), ''
|
16
|
+
).split(/[-_]/).map(&:capitalize).join(' ')
|
17
|
+
end
|
18
|
+
|
19
|
+
# Prompt user for file selection
|
20
|
+
#
|
21
|
+
# @param directory [String] path to directory
|
22
|
+
# @param opts [Hash] options
|
23
|
+
# @option opts [Array<String>] :ignore_directories directory names
|
24
|
+
# @option opts [String] :directories_name title for directories
|
25
|
+
# @option opts [String] :files_name title for files
|
26
|
+
# @option opts [String] :filter_prefix only return results matching filter
|
27
|
+
# @return [String] file path
|
28
|
+
def prompt_for_file(directory, opts={})
|
29
|
+
file_list = Dir.glob(File.join(directory, '**', '**', '*')).find_all do |file|
|
30
|
+
File.file?(file)
|
31
|
+
end
|
32
|
+
if(opts[:filter_prefix])
|
33
|
+
file_list = file_list.find_all do |file|
|
34
|
+
file.start_with?(options[:filter_prefix])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
directories = file_list.map do |file|
|
38
|
+
File.dirname(file)
|
39
|
+
end.uniq
|
40
|
+
files = file_list.find_all do |path|
|
41
|
+
path.sub(directory, '').split('/').size == 2
|
42
|
+
end
|
43
|
+
if(opts[:ignore_directories])
|
44
|
+
directories.delete_if do |dir|
|
45
|
+
opts[:ignore_directories].include?(File.basename(dir))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
if(directories.empty? && files.empty?)
|
49
|
+
ui.fatal 'No formation paths discoverable!'
|
50
|
+
else
|
51
|
+
output = ['Please select an entry']
|
52
|
+
output << '(or directory to list):' unless directories.empty?
|
53
|
+
ui.info output.join(' ')
|
54
|
+
output.clear
|
55
|
+
idx = 1
|
56
|
+
valid = {}
|
57
|
+
unless(directories.empty?)
|
58
|
+
output << ui.color("#{opts.fetch(:directories_name, 'Directories')}:", :bold)
|
59
|
+
directories.each do |dir|
|
60
|
+
valid[idx] = {:path => dir, :type => :directory}
|
61
|
+
output << [idx, humanize_path_basename(dir)]
|
62
|
+
idx += 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
unless(files.empty?)
|
66
|
+
output << ui.color("#{opts.fetch(:files_name, 'Files')}:", :bold)
|
67
|
+
files.each do |file|
|
68
|
+
valid[idx] = {:path => file, :type => :file}
|
69
|
+
output << [idx, humanize_path_basename(file)]
|
70
|
+
idx += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
max = idx.to_s.length
|
74
|
+
output.map! do |o|
|
75
|
+
if(o.is_a?(Array))
|
76
|
+
" #{o.first}.#{' ' * (max - o.first.to_s.length)} #{o.last}"
|
77
|
+
else
|
78
|
+
o
|
79
|
+
end
|
80
|
+
end
|
81
|
+
ui.info "#{output.join("\n")}\n"
|
82
|
+
response = ask_question('Enter selection: ').to_i
|
83
|
+
unless(valid[response])
|
84
|
+
ui.fatal 'How about using a real value'
|
85
|
+
exit 1
|
86
|
+
else
|
87
|
+
entry = valid[response.to_i]
|
88
|
+
if(entry[:type] == :directory)
|
89
|
+
prompt_for_file(entry[:path], opts)
|
90
|
+
else
|
91
|
+
"/#{entry[:path]}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|