sfn 0.0.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|