nli_pipeline 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5803338257515506b32dc63259908d23d660e701
4
- data.tar.gz: b5a12fbcf9b2e604464bd3c99c6b9f3101e34539
3
+ metadata.gz: 0b403349bdb310782fa8b2dae71361b5ee0874e1
4
+ data.tar.gz: 837d3ca13607c39c8aa1d451abb80ab9a1066196
5
5
  SHA512:
6
- metadata.gz: b547fd4c73d957517ca8b8cc0b30f189d042188e025d37f9ad42238bdbeae976efede708a6229f9b64a39c4fe353d9e611cdd983393830f45025ef0525b4d77a
7
- data.tar.gz: 6ec520a89825c94223381bff4169f807a24d29988c7207f27d0fdbef385c5c581542a751f2ccd49b2888da274f4614e5810e13301e5ebaa544466ec1d6160baf
6
+ metadata.gz: de55b1cddd338648732b811fc616a2e4d9484c1f585c3413e0532e1726d3b7b3748c7f8f78bece0b81120dcfcb665b37438082911baf788652ba583734d32eb1
7
+ data.tar.gz: f5cf0e8b41936beb85fbd4967ef45d8d56ab2c4edcc4fb474fe322759989b880e0a2a401c11c36199948383cbe225357c82c57cf0991ed4435cdaa67e3435aec
data/bin/setup_pipeline CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'nli_pipeline'
3
3
 
4
- SetupPipeline.new.main()
4
+ NliPipeline::SetupPipeline.new.main
data/lib/nli_pipeline.rb CHANGED
@@ -1,11 +1,8 @@
1
+ # import all ruby files in nli_pipeline
2
+ Dir["#{__dir__}/nli_pipeline/**/*.rb"].each do |file|
3
+ require file
4
+ end
5
+
1
6
  # used to defined constants and include requirements for /bin/setup_pipeline
2
7
  module NliPipeline
3
-
4
- # constant use in nli_pipeline.gemspec and bin/setup_pipeline
5
- # to determine current version of gem
6
- # should also match tag on bitbucket
7
- VERSION = "0.1.2"
8
-
9
- require_relative 'nli_pipeline/file_manager'
10
- require_relative 'nli_pipeline/setup_pipeline'
11
8
  end
@@ -0,0 +1,110 @@
1
+ module NliPipeline
2
+ # TODO: on v1.0.0 release, it may be possible to replace this with open struct
3
+ # if path is no longer a required argument
4
+
5
+ # Abstract Utilities for handling class instantiation
6
+ # similar to open struct, but enforces some arguments
7
+ module AbstractUtil
8
+ # override include to automatically extend classmethods
9
+ # i.e. functions in the module ClassMethods becom class methods
10
+ # when the AbstractUtil is included
11
+ def self.included(parent)
12
+ parent.extend(ClassMethods)
13
+ end
14
+
15
+ # @raise [ArgumentError] unless all values in kwargs are truthy
16
+ # @return [Boolean]
17
+ def raise_unless_all(**kwargs)
18
+ message = "All #{kwargs} must be truthy to use"\
19
+ " #{self.class}.#{caller_locations(1, 1)[0].label}"
20
+ raise ArgumentError, message unless kwargs.values.all?
21
+ true
22
+ end
23
+
24
+ # keys become instance variable names
25
+ # value is set using corresponding value in kwargs
26
+ # all instance variables have getters, but not setters
27
+ # add attributes to class
28
+ def add_attrs(**kwargs)
29
+ kwargs.each do |k, v|
30
+ instance_variable_set("@#{k}", v)
31
+ self.class.class_eval { attr_reader k.to_sym }
32
+ end
33
+ puts(to_s.yellow) if @debug
34
+ end
35
+
36
+ # support method for init_with_attrs to warn user about unused kwargs
37
+ def drop_forbidden_args_message(**kwargs)
38
+ forbidden_args = self.class.get_forbidden_kwargs(**kwargs)
39
+ show_warning = !forbidden_args.empty? && kwargs[:debug]
40
+ puts("Warning, dropping args #{forbidden_args} to #{self.class}".red) if show_warning
41
+ end
42
+
43
+ # similar to openstruct initialize
44
+ # assigns all keyword arguments as instance variables
45
+ # but uses attr_reader, not attr_accessor
46
+ def init_with_attrs(**kwargs)
47
+ sanitized_args = self.class.get_allowed_kwargs(**kwargs)
48
+ drop_forbidden_args_message(**kwargs)
49
+ unless self.class.required_args?(**kwargs)
50
+ # raise SystemWrapper::ConfigError.new()
51
+ msg = "#{self.class}.#{caller_locations(1, 1)[0].label}"\
52
+ " requires arguments: #{self.class.required_args}"
53
+ raise ArgumentError, msg
54
+ end
55
+ # merge passed args with default values for args
56
+ merged_args = self.class.supported_args.merge(sanitized_args)
57
+ add_attrs(**merged_args)
58
+ end
59
+
60
+ # override default to_s for nice formatting of instance vars
61
+ def to_s
62
+ format_instance_vars = lambda do |x, y|
63
+ x + format("\n%<key>-30s: %<value>s", key: y, value: instance_variable_get(y.to_sym))
64
+ end
65
+ vars = instance_variables.reduce('', &format_instance_vars)
66
+ "\n#{self.class} config#{vars}\n"
67
+ end
68
+
69
+ # methods to be loaded as static/class methods in class that extends AbstractUtil
70
+ module ClassMethods
71
+ # :nocov:
72
+ # use method_missing to force classes that use AbstractUtil to implement
73
+ # supported_args and required_args.
74
+ # @param method [String]
75
+ # @param args [Array]
76
+ def method_missing(method, *args)
77
+ # cast methods to array of symbols, compare to current missing method
78
+ if %i[supported_args required_args].include?(method.to_sym)
79
+ message = "Method #{method} must be implemented in #{self.class} "\
80
+ 'in order to use AbstractUtil'
81
+ raise ArgumentError, message
82
+ end
83
+ super
84
+ end
85
+ # can't test call to super, so wrap in nocov tag
86
+ # :nocov:
87
+
88
+ # support for method_missing
89
+ # https://robots.thoughtbot.com/always-define-respond-to-missing-when-overriding
90
+ def respond_to_missing?(method, *args)
91
+ %i[supported_args required_args].include?(method.to_sym) || super
92
+ end
93
+
94
+ # @return [Hash] args that can be converted into instance vars for class
95
+ def get_allowed_kwargs(**kwargs)
96
+ kwargs.select { |k, _v| supported_args.key?(k) }
97
+ end
98
+
99
+ # @return [Hash] args that can't be converted into instance vars for class
100
+ def get_forbidden_kwargs(**kwargs)
101
+ kwargs.reject { |k, _v| supported_args.key?(k) }
102
+ end
103
+
104
+ # @return [Boolean] have all required args been passed?
105
+ def required_args?(**kwargs)
106
+ required_args.all? { |key| kwargs.key?(key) }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,243 @@
1
+ require 'colorize'
2
+ require 'nli_pipeline/system_wrapper/call_wrapper'
3
+ require 'nli_pipeline/abstract_util/init_attrs'
4
+
5
+ module NliPipeline
6
+ # handle docker build, save and run for nli-pipelines
7
+ # this DOES NOT handle docker login
8
+ # if you want to push to dockerhub or pull private images
9
+ # you'll need to log in in advance
10
+ class DockerManager < SystemWrapper::CallWrapper
11
+ include AbstractUtil
12
+
13
+ # static methods required by NliPipeline::AbstractUtil::init_attrs
14
+ # @see NliPipeline::AbstractUtil#init_with_attrs
15
+ # @see NliPipeline::AbstractUtil#get_allowed_args
16
+ # @return [Hash]
17
+ def self.supported_args
18
+ {
19
+ path: '', debug: false, fail_on_error: false,
20
+ image_name: false, upstream_image_name: false, tar_name: false,
21
+ git_branch: false, proxy: false, container_name: false, commit: true,
22
+ mount: false, bash_commands: [], ports: []
23
+ }
24
+ end
25
+
26
+ # @see NliPipeline::AbstractUtil#init_with_attrs
27
+ # @see NliPipeline::AbstractUtil::ClassMethods#required_args?
28
+ # @return [Array[Symbol]]
29
+ def self.required_args
30
+ [:path]
31
+ end
32
+
33
+ # creates new DockerManager and assigns each keywoard argument as instance var with attr_reader
34
+ # uses image_name.tar as tar_name unless it's explicitly set
35
+ def initialize(**kwargs)
36
+ # add nlireland/ prefix to imagename if no upstream image name is set
37
+ can_guess_upstream = kwargs[:image_name] && !kwargs[:upstream_image_name]
38
+ kwargs[:upstream_image_name] = "nlireland/#{kwargs[:image_name]}" if can_guess_upstream
39
+
40
+ # set tar name as image name if image name is set but tar name isn't
41
+ can_make_tar = kwargs[:image_name] && !kwargs[:tar_name]
42
+ kwargs[:tar_name] = "#{kwargs[:image_name]}.tar" if can_make_tar
43
+
44
+ # set container name manually, should not be able to change it
45
+ kwargs[:container_name] = 'pipeline-container'
46
+
47
+ # set git branch if it's not defined
48
+ # pass debug flags down to GitManager (will show warning about extra args though)
49
+ kwargs[:git_branch] = GitManager.new(**kwargs).branch unless kwargs[:git_branch]
50
+
51
+ init_with_attrs(**kwargs)
52
+ end
53
+
54
+ # check if last commit contained build command
55
+ # @return Boolean
56
+ def build_commit?
57
+ # neeed \\ since command will be passed to the shell as \[.*\]
58
+ # escape chars all the way down
59
+ custom_error_message = "[docker build] or variant not in last commit.\nFailing Build."
60
+ custom_message = 'can build docker'
61
+ # will match any combination of 'docker' and 'build' inside square brackets
62
+ # TODO: refator to use GitManager and stub git log -1 call
63
+ last_commit_message = NliPipeline::GitManager.new.last_commit_message
64
+ command = "echo '#{last_commit_message}' | grep -o '\\[.*\\]' "\
65
+ "| grep -i 'docker' | grep -i 'build'"
66
+
67
+ pretty_print(custom_message: custom_message) do
68
+ call_system(command, custom_error_message: custom_error_message)
69
+ end
70
+ end
71
+
72
+ # @return [String] build arguments
73
+ def build_args
74
+ args = []
75
+ args.push("proxy=#{@proxy}") if @proxy
76
+ args.push("commit=#{NliPipeline::GitManager.new.last_commit_url}") if @commit
77
+ args.reduce('', &->(x, y) { x + " --build-arg #{y}" })
78
+ end
79
+
80
+ # build docker image
81
+ # @return [Boolean] success/failure
82
+ def build
83
+ can_build = build_commit?
84
+ custom_message = 'built docker image'
85
+
86
+ # if we can't build, fail early
87
+ unless can_build
88
+ # if --fail-on-error is passed, this will throw and exception
89
+ # and "Skipping build" will not be output
90
+ ret = pretty_print(custom_message: custom_message) { false }
91
+ puts('Skipping build'.yellow)
92
+ return ret
93
+ end
94
+
95
+ command = 'docker build'
96
+ # command += " --build-arg proxy=#{@proxy}" if @proxy
97
+ command += build_args
98
+ command += " -t #{@image_name}" if @image_name
99
+ pretty_print(custom_message: custom_message) { call_system("#{command} #{@path}") }
100
+ end
101
+
102
+ # save docker image as tarfile
103
+ # @return [Boolean] success/failure
104
+ def save
105
+ # throw an error is image_name is not set
106
+ raise_unless_all(image_name: @image_name)
107
+ save_command = "docker save #{@image_name} -o #{@path}/#{@tar_name}"
108
+ puts 'saving docker image'.yellow
109
+ pretty_print(custom_message: 'saved docker image') { call_system(save_command) }
110
+ end
111
+
112
+ # save docker image as tarfile
113
+ # @return [Boolean] success/failure
114
+ def build_and_save
115
+ built = build
116
+ # if the build fails quit
117
+ return false unless built
118
+ save
119
+ end
120
+
121
+ # load saved docker image
122
+ # @return [Boolean] success/failure
123
+ def load
124
+ load_command = "docker load -i #{@path}/#{@tar_name}"
125
+ pretty_print(custom_message: "loading #{@tar_name}") { call_system(load_command) }
126
+ end
127
+
128
+ # def tar_exist?
129
+ # File.exist? "#{@path}/#{@tar_name}"
130
+ # end
131
+
132
+ # if the last commit was a build commit
133
+ # try to load and run the image
134
+ # otherwise pull and run the upstream image
135
+ #
136
+ # in prepare_run_func
137
+ # always:
138
+ # 1. run in detached mode with stdin open (-id)
139
+ # 2. pass CI=true to as a build argument
140
+ # 3. set the container name (pipeline-container)
141
+ #
142
+ # @raise [SystemWrapper::ConfigError] if the local image failed to be loaded /run,
143
+ # or the upstream image failed to run
144
+ # @return [Boolean] success/failure
145
+ def run
146
+ run_func = prepare_run_func
147
+ # if the last commit had a build instruction, load the built image
148
+ if build_commit? && @image_name
149
+ load
150
+ run_func.call(@image_name)
151
+ # otherwise try to pull the upstream image from dockerhub
152
+ elsif @upstream_image_name
153
+ run_func.call(@upstream_image_name)
154
+ # if neither image nor upstream image were set, throw config error
155
+ # can't use raise_unless_all since some, but not all, must be set
156
+ else
157
+ error_message = 'you must set image_name or upstream_image_name to run a docker image'
158
+ config_error = "Config Error: #{error_message}"
159
+ config = {
160
+ image_name: @image_name, upstream_image_name: @upstream_image_name,
161
+ build_commit: build_commit?
162
+ }
163
+ raise SystemWrapper::ConfigError.new(config: config, msg: config_error)
164
+ end
165
+ end
166
+
167
+ # create function to bind args to on docker run
168
+ # @return [Boolean] success/failure
169
+ def prepare_run_func
170
+ base_command = 'docker run -id'
171
+ base_command += " -v #{@path}:#{@mount}" if @mount
172
+ base_command = @ports.reduce(base_command, &->(x, y) { x + " -p #{y}:#{y}" })
173
+ base_command += " -e CI=TRUE --name #{@container_name}"
174
+ proc do |name|
175
+ pretty_print(custom_message: "running #{name}") { call_system("#{base_command} #{name}") }
176
+ end
177
+ end
178
+
179
+ # send bash command to running docker container
180
+ # avoid clash with exec!
181
+ # @return [Boolean] success/failure
182
+ def docker_exec
183
+ @bash_commands.each do |cmd|
184
+ pretty_print { call_system("docker exec #{@container_name} bash -c '#{cmd}'") }
185
+ end
186
+ end
187
+
188
+ # relies on tag to ensure image_name and upstream_image_name are set
189
+ # setup_pipeline sets fail-on-error to true for all deploys
190
+ # so build_commit? will raise an exception and stop the build
191
+ # if the last commit was not a build commit
192
+ # @return [Boolean] success/failure
193
+ def deploy_master
194
+ build_commit?
195
+ master_tags = ['latest', "latest-#{Time.now.strftime('%Y-%m-%d')}"]
196
+ master_tags.map do |t|
197
+ tag(t)
198
+ message = "pushing tag: #{t} to #{@upstream_image_name}"
199
+ push_command = "docker push #{@upstream_image_name}:#{t}"
200
+ # deploy each tag individually on the off change
201
+ # that another tag for the image that wasn't created by setup_pipeline
202
+ # exists and should not be pushed
203
+ pretty_print(custom_message: message) { call_system(push_command) }
204
+ end
205
+ end
206
+
207
+ # setup_pipeline sets fail-on-error to true for all deploys
208
+ # so build_commit? will raise an exception and stop the build
209
+ # if the last commit was not a build commit
210
+ # @return [Boolean] success/failure
211
+ def deploy_branch
212
+ build_commit?
213
+ # if gitbranch is not set, raise an error
214
+ # todo: this may be unnecessary now with git branch default
215
+ raise_unless_all(git_branch: @git_branch)
216
+ message = "pushing tag: #{@git_branch} to #{@upstream_image_name}"
217
+ tag(@git_branch)
218
+ push_command = "docker push #{@upstream_image_name}:#{@git_branch}"
219
+ pretty_print(custom_message: message) { call_system(push_command) }
220
+ end
221
+
222
+ private
223
+
224
+ # @param tag [String] what to tag the docker image as
225
+ # @return [Boolean] success/failure
226
+ def tag(tag)
227
+ # docker tag requires a local and upstream image
228
+ # raise an error if either of these are missing
229
+ raise_unless_all(image_name: @image_name, upstream_image_name: @upstream_image_name)
230
+ imgs_and_tag = "#{@image_name} #{@upstream_image_name}:#{tag}"
231
+ print_arg = "tagging #{imgs_and_tag}"
232
+ call_system_arg = "docker tag #{imgs_and_tag}"
233
+ # ret = pretty_print(custom_message: print_arg) { call_system(call_system_arg) }
234
+ pretty_print(custom_message: print_arg) { call_system(call_system_arg) }
235
+
236
+ # # raise an error if fail on error is set
237
+ # error_messsage = "Failed to tag #{@upstream_image_name}:#{tag}"
238
+ # err = NliPipeline::SystemWrapper::CallWrapperError.new(error_messsage)
239
+ # raise err if !ret && @fail_on_error
240
+ # ret
241
+ end
242
+ end
243
+ end
@@ -1,144 +1,177 @@
1
- require 'rubygems'
2
1
  require 'colorize'
2
+ require 'nli_pipeline/system_wrapper/call_wrapper'
3
+ require 'nli_pipeline/abstract_util/init_attrs'
3
4
 
4
- # Manage example files
5
- #
6
- # EXAMPLE:
7
- # >> FileManager.new.copy_example_files()
8
- # => Moving 1 .EXAMPLE files
9
- class FileManager
10
-
11
- # String: must be valid file path
12
- attr_reader :path
13
- # String: file extension default: .EXAMPLE
14
- attr_accessor :extension
15
- # Bool: show all system commands run by ruby
16
- attr_accessor :debug
17
-
18
- # Arguments:
19
- # path: (string: path to directory)
20
- # extension: (string)
21
- # debug: (boolean)
22
- # no_output: (boolean: used for suppressing output during tests)
23
- def initialize(path, extension: ".EXAMPLE", debug: false, no_output: false)
24
- puts("config = {path: '#{path}', extension: '#{extension}', debug: #{debug}, no_output: #{no_output}}".yellow()) if debug
25
- @path = path
26
- @extension = extension
27
- @debug = debug
28
- @no_output = no_output
29
- @log_dir = "#{path}/.pipeline_logs"
30
- @created_log_name = "created_files_"
31
-
32
- unless File.directory?(@log_dir)
33
- Dir.mkdir(@log_dir)
34
- end
35
- end
5
+ module NliPipeline
6
+ # TODO: enforce that command must be either cp or mv
36
7
 
37
- # get backup dates in order from newest to oldest
38
- def get_all_backups()
39
- backup_files = Dir.glob("#{@path}/**/*.backup*")
40
- backup_numbers = backup_files.map {|x| x.split(".backup").last}
41
- return backup_numbers.uniq.sort
42
- end
8
+ # Manage example files
9
+ #
10
+ # EXAMPLE:
11
+ # >> NliPipeline::FileManager.new.copy_example_files()
12
+ # => Moving 1 .EXAMPLE files
13
+ class FileManager < SystemWrapper::CallWrapper
14
+ include AbstractUtil
15
+
16
+ # static methods required by NliPipeline::AbstractUtil::init_attrs
17
+ # @see NliPipeline::AbstractUtil#init_with_attrs
18
+ # @see NliPipeline::AbstractUtil#get_allowed_args
19
+ # @return [Hash]
20
+ def self.supported_args
21
+ {
22
+ path: '', extension: '.EXAMPLE', debug: false,
23
+ fail_on_error: false, log_dir: '.pipeline_logs',
24
+ created_get_log_name: 'created_files_', backup_path: ''
25
+ }
26
+ end
43
27
 
44
- # find all example files under @path
45
- def get_all_example_files()
46
- example_file_path = "#{@path}/**/*#{extension}"
47
- example_files = Dir.glob(example_file_path)
48
- if example_files.empty?
49
- raise ArgumentError.new("No #{extension} Files found at #{example_file_path}")
50
- else
51
- puts("Moving #{example_files.count} #{extension} files".green()) unless @no_output
28
+ # @see NliPipeline::AbstractUtil#init_with_attrs
29
+ # @see NliPipeline::AbstractUtil::ClassMethods#required_args?
30
+ # @return [Array[Symbol]]
31
+ def self.required_args
32
+ [:path]
52
33
  end
53
- return example_files
54
- end
55
34
 
56
- # read latest log in .pipeline_logs
57
- # @return [String] files created by last setup_pipeline
58
- def get_last_created_files()
59
- logs = Dir.glob("#{@log_dir}/*")
60
- if logs.empty?
61
- raise ArgumentError.new("No logs found in #{@log_dir}")
35
+ # automatically set backup path
36
+ # init_with_attrs handles the rest
37
+ # @param kwargs [Hash] all keyword (only supported_args) will be added as vars
38
+ def initialize(**kwargs)
39
+ # backup_path should not be configurable
40
+ # set it directly regardless of whether it's passed in
41
+ kwargs[:backup_path] = "#{kwargs[:path]}/**/*.backup*"
42
+ init_with_attrs(**kwargs)
62
43
  end
63
- log_numbers = logs.map {|x| x.split("#{@created_log_name}").last}
64
- latest_log_number = log_numbers.uniq.sort.first
65
- return File.readlines("#{@log_dir}/#{@created_log_name}#{latest_log_number}")
66
- end
67
44
 
68
- # Arguments:
69
- # command: (string: system command)
70
- def copy_example_files(command="cp")
71
- time_stamp = Time.now.strftime("%Y%m%d%H%M")
72
- puts("Setting up pipeline in #{@path}".yellow()) unless @no_output
73
- example_files = get_all_example_files
74
- files_to_backup = example_files.map {|f| f.gsub(@extension, "")}.select {|file| File.exists?(file) }
75
- # raise ArgumentError.new("#{files_to_backup.to_s} #{example_files.to_s}")
45
+ # @return [Array[String]] all example files under @path
46
+ def all_example_files
47
+ example_file_path = "#{@path}/**/*#{@extension}"
48
+ example_files = Dir.glob(example_file_path)
49
+ if example_files.empty?
50
+ raise ArgumentError, "No #{@extension} Files found at #{example_file_path}"
51
+ end
52
+ puts("Moving #{example_files.count} #{@extension} files".green)
53
+ example_files
54
+ end
76
55
 
77
- if !files_to_backup.empty?
78
- # preset backup time so all backups fomr same batch have the same timestamp
79
- puts("Backing up #{files_to_backup.count} files".green()) unless @no_output
80
- files_to_backup.each do |file_to_backup|
81
- full_command = "#{command} #{file_to_backup} #{file_to_backup}.backup#{time_stamp}"
82
- puts("\t#{full_command}") if @debug
83
- system(full_command)
56
+ # read latest log in .pipeline_logs
57
+ # @return [String] files created by last setup_pipeline
58
+ def last_created_files
59
+ logs = Dir.glob("#{@path}/#{@log_dir}/*")
60
+ if logs.empty?
61
+ puts("No logs found in #{@path}/#{@log_dir}".red)
62
+ return false
84
63
  end
64
+ log_numbers = logs.map { |x| x.split(@created_get_log_name.to_s).last }
65
+ latest_log_number = log_numbers.uniq.min
66
+ File.readlines(get_log_name(latest_log_number))
85
67
  end
86
68
 
87
- example_files.each do |example_file|
88
- target_file = example_file.gsub(@extension, "")
89
- # convert the example file to real config
90
- full_command = "#{command} #{example_file} #{target_file}"
91
- puts("\t#{full_command}") if @debug
92
- system(full_command)
69
+ # copy all files ending in @extension under @path.
70
+ # if a non-exmaple version of the file exists, back it up.
71
+ # @param command [String] command to run (cp/mv)
72
+ def copy_example_files(command = 'cp')
73
+ time_stamp = Time.now.strftime('%Y%m%d%H%M')
74
+ puts("Setting up pipeline in #{@path}".yellow)
75
+ example_files = all_example_files
76
+
77
+ backup_non_example_files(example_files, time_stamp)
93
78
 
94
- if File.exists?(target_file)
95
- add_to_log(target_file, time_stamp)
96
- puts("\t add #{target_file} to log #{get_log_name(time_stamp)}") if @debug
79
+ example_files.each do |example_file|
80
+ target_file = example_file.gsub(@extension, '')
81
+ call_system("#{command} #{example_file} #{target_file}")
82
+
83
+ if File.exist?(target_file)
84
+ add_to_log(target_file, time_stamp)
85
+ puts("\t add #{target_file} to log #{get_log_name(time_stamp)}") if @debug
86
+ end
97
87
  end
98
88
  end
99
89
 
100
- end
90
+ # get backup dates in order from newest to oldest
91
+ def all_backups
92
+ backup_files = Dir.glob(@backup_path)
93
+ backup_numbers = backup_files.map { |x| x.split('.backup').last }
94
+ backup_numbers.uniq.sort
95
+ end
101
96
 
102
- # Arguments:
103
- # command: (string: system command)
104
- #
105
- # to remove backup, use mv
106
- # to keep backup, use cp
107
- def load_from_backup(command="cp")
108
- puts("loading backup in #{@path}".yellow())
109
- backups = get_all_backups()
110
- if backups.empty?
111
- # TODO: better exception class here?
112
- raise "\n No backups found \n"
97
+ # for each example file,
98
+ # if a non-example version of that file exists,
99
+ # back it up.
100
+ #
101
+ # @param example_files [Array[string]] list of example files
102
+ # @param time_stamp [String] time of backup
103
+ # @param command [String] command to run (cp/mv)
104
+ def backup_non_example_files(example_files, time_stamp, command = 'cp')
105
+ possible_files = example_files.map { |f| f.gsub(@extension, '') }
106
+ files_to_backup = possible_files.select { |file| File.exist?(file) }
107
+ # raise ArgumentError.new("#{files_to_backup.to_s} #{example_files.to_s}")
108
+ return false if files_to_backup.empty?
109
+
110
+ # preset backup time so all backups fomr same batch have the same timestamp
111
+ puts("Backing up #{files_to_backup.count} files".green)
112
+ files_to_backup.each do |file_to_backup|
113
+ call_system("#{command} #{file_to_backup} #{file_to_backup}.backup#{time_stamp}")
114
+ end
113
115
  end
114
116
 
115
- puts "which backup would you like to load?\n#{backups}".yellow()
116
- latest_backup_date = STDIN.gets().chomp()
117
+ # @param command [String] system command.
118
+ # use mv to remove backup,
119
+ # use cp to keep backup.
120
+ # @return Boolean
121
+ def load_from_backup(command = 'cp')
122
+ puts("loading backup in #{@path}".yellow)
123
+ backups = all_backups
124
+ if backups.empty?
125
+ puts("No backups found in #{@backup_path}".red)
126
+ return false
127
+ end
128
+
129
+ backup_path, backup_date = handle_backup_user_input(backups)
130
+ files_to_load = Dir.glob(backup_path)
131
+ puts "loading #{files_to_load.count} files from #{backup_path}".green
117
132
 
118
- while !backups.include?(latest_backup_date)
119
- puts "#{latest_backup_date} is not in backups".red()
120
- puts "please choose from: #{backups}".yellow()
121
- latest_backup_date = STDIN.gets().chomp()
133
+ files_to_load.each do |backup_file|
134
+ target_file = backup_file.gsub("\.backup#{backup_date}", '')
135
+ call_system("#{command} #{backup_file} #{target_file}")
136
+ end
137
+ end
138
+
139
+ # keeps asking the use to choose a backup
140
+ # until they choose a valid one
141
+ # @param backups [Array[String]]
142
+ # @return [Array[String]]
143
+ def handle_backup_user_input(backups)
144
+ puts "which backup would you like to load?\n#{backups}".yellow
145
+ backup_date = STDIN.gets.chomp
146
+
147
+ until backups.include?(backup_date)
148
+ puts "#{backup_date} is not in backups".red
149
+ puts "please choose from: #{backups}".yellow
150
+ backup_date = STDIN.gets.chomp
151
+ end
152
+ ["#{@path}/**/*.backup#{backup_date}", backup_date]
122
153
  end
123
154
 
124
- files_to_load = Dir.glob("#{@path}/**/*.backup#{latest_backup_date}")
125
- puts "loading #{files_to_load.count} files from #{@path}/**/*.backup#{latest_backup_date}".green()
155
+ private
126
156
 
127
- files_to_load.each do |backup_file|
128
- target_file = backup_file.gsub("\.backup#{latest_backup_date}", "")
129
- full_command = "#{command} #{backup_file} #{target_file}"
130
- puts("\t#{full_command}") if @debug
131
- system(full_command)
157
+ # create dir for pipeline logs
158
+ def create_log_dir
159
+ full_path_to_log = "#{@path}/#{@log_dir}"
160
+ Dir.mkdir(full_path_to_log) unless File.directory?(full_path_to_log)
132
161
  end
133
- end
134
162
 
135
- private
163
+ # @param date [String]
164
+ # @return [String] full path to log
136
165
  def get_log_name(date)
137
- return "#{@log_dir}/#{@created_log_name}#{date}"
166
+ "#{@path}/#{@log_dir}/#{@created_get_log_name}#{date}"
138
167
  end
139
168
 
169
+ # appends to log file
170
+ # @param line [String] line to append to log file
171
+ # @param date [String]
140
172
  def add_to_log(line, date)
141
- File.open(get_log_name(date), "a") {|file| file.write(line)}
173
+ create_log_dir # if log doesn't exist
174
+ File.open(get_log_name(date), 'a') { |file| file.write(line) }
142
175
  end
143
-
176
+ end
144
177
  end