Finished feature

This commit is contained in:
Tristan Ancelet 2024-05-24 19:13:55 -05:00 committed by Tristan Ancelet
parent 1d546c93b1
commit 58b6de55a2

View File

@ -1,5 +1,10 @@
#!/usr/bin/ruby #!/usr/bin/ruby
# BEGIN: Requires
require 'digest'
# END: Requires
# BEGIN: Helper Functions # BEGIN: Helper Functions
@ -14,17 +19,25 @@ Description:
search the filebucket and restore from it. search the filebucket and restore from it.
Actions: Actions:
search <term> : Search for bucket entries matching a portion of the filepath search <term> : Search for bucket entries matching a portion of the filepath
list : List all Bucket entries list : List all Bucket entries
list-files : List all files/paths that have been backed up to the bucket list-files : List all files/paths that have been backed up to the bucket
get <entry-hash> : Get the content of a specific entry (by hash) get <entry-hash> : Get the content of a specific entry (by hash)
restore-hash <entry-hash> : Restore previous state of file stored in bucket (by-hash) restore <value> : Restore previous state of file stored in bucket. Value can be hash or filename/filepath
restore-file <filepath> : Restore previous state of file stored in bucket (by-filepath/filename)
Global Flags: Global Flags:
-d | --debug : Set debug flag -d | --debug : Set debug flag
-h | --help : This help message -h | --help : This help message
Info Format Flags:
-i | --inline : Set the info format to inline (MTIME : HASH : FILENAME)
-l | --long : Set the info format to long
: Entry [HASH]:
: Paths: path1,path2,...,pathn
: MTIME: YYYY-MM-DD HH:MM:SS -####
:
-c | --csv : Set the info format to csv ( MTIME,HASH,FILENAME1[;FILENAMEn] )
Author: Author:
Name: Tristan Ancelet Name: Tristan Ancelet
Email: tristan.ancelet@acumera.com Email: tristan.ancelet@acumera.com
@ -63,6 +76,18 @@ def log (message, level = 0)
end end
end end
def which(cmd)
#https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
exts.each do |ext|
exe = File.join(path, "#{cmd}#{ext}")
return exe if File.executable?(exe) && !File.directory?(exe)
end
end
nil
end
def get_verification (prompt = "Do you want to continue?") def get_verification (prompt = "Do you want to continue?")
while true while true
puts "#{prompt} (y/n): " puts "#{prompt} (y/n): "
@ -79,6 +104,54 @@ def get_verification (prompt = "Do you want to continue?")
end end
end end
def get_selections (reference_array, prompt = "Which of the following do you want to select? ", options = { :multiple => false, }, &procedure)
## Making clone of array since the selections were passed by reference
selections = reference_array.clone
def put_prompt (selections, prompt)
puts prompt
selections.each_with_index do |value,index|
puts "#{index} : #{value}"
end
end
if options[:multiple] == true
output = Array.new
else
output = ""
end
put_prompt selections, prompt
while true
choice = Integer(STDIN.gets.strip())
if choice.is_a? Integer
if choice >= 0 and choice < selections.count
if options[:multiple] == true
output.push(selections[choice])
selections.delete_at(choice)
if get_verification "Are you done selecting?"
break
end
put_prompt selections, prompt
else
output = selections[choice]
break
end
else
puts "#{choice} is not between the values of 0 and #{selections.count}. Please try again."
end
else
puts "#{choice} is not a valid option. Please try again."
end
end
if procedure.respond_to? "call"
output = procedure.call(output)
end
output
end
# END: Helper Functions # END: Helper Functions
@ -98,11 +171,19 @@ end
$DEBUG=false $DEBUG=false
puppet_exe = which "puppet"
if puppet_exe == nil
puts "The puppet utility was not found in $PATH. This utility will not be able to function"
exit
end
$CONFIG = Hash.new $CONFIG = Hash.new
$CONFIG[:bucket_dir]=` puppet agent --configprint clientbucketdir `.strip() $CONFIG[:bucket_dir]=` #{puppet_exe} agent --configprint clientbucketdir `.strip()
$CONFIG[:action]="" $CONFIG[:action]=""
$CONFIG[:search_term]="" $CONFIG[:search_term]=""
$CONFIG[:log_file]="" $CONFIG[:log_file]=""
$CONFIG[:info_format]="inline"
File.open('/etc/hostname') do |file| File.open('/etc/hostname') do |file|
$HOSTNAME=file.read().strip() $HOSTNAME=file.read().strip()
end end
@ -151,6 +232,25 @@ case ARGV[i]
exit exit
end end
while i < ARGV.count
case ARGV[i]
when '-c', '--csv'
$CONFIG[:info_format]='csv'
log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}"
i+=1
when '-l', '--long'
$CONFIG[:info_format]='long'
log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}"
i+=1
when '-i', '--inline'
$CONFIG[:info_format]='inline'
log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}"
i+=1
else
i+=1
end
end
## BEGIN: Checks ## BEGIN: Checks
if $CONFIG[:action] == "" if $CONFIG[:action] == ""
@ -164,6 +264,7 @@ case $CONFIG[:action]
usage usage
exit exit
end end
end end
## END: Checks ## END: Checks
@ -178,12 +279,8 @@ class BucketEntry
def initialize (entry_dir) def initialize (entry_dir)
@entry_dir = entry_dir @entry_dir = entry_dir
@hash = File.basename(entry_dir) @hash = File.basename(entry_dir)
@filepaths = Array.new
File.open("#{entry_dir}/paths") do |file| File.open("#{entry_dir}/paths") do |file|
file.read().split(/\n/).each do |path| @filepaths = file.read.split(/\n/)
log "BucketEntry[#{@hash}] adding #{path} to @filepaths"
@filepaths.push(path)
end
end end
@mtime = File.mtime(entry_dir) @mtime = File.mtime(entry_dir)
log "BucketEntry was created from #{entry_dir}" log "BucketEntry was created from #{entry_dir}"
@ -191,10 +288,10 @@ class BucketEntry
def path_include? (path_string) def path_include? (path_string)
log "BucketEntry[#{hash}] was called with #{path_string}" log "BucketEntry[#{hash}] was called with #{path_string}"
@filepaths.each.any? {|path| path.include? path_string} @filepaths.any?{|path| path.include? path_string}
end end
def infostring def long_info
"Entry [#{@hash}]: "Entry [#{@hash}]:
Paths: #{@filepaths.join(',')} Paths: #{@filepaths.join(',')}
MTIME: #{@mtime} MTIME: #{@mtime}
@ -202,10 +299,25 @@ class BucketEntry
" "
end end
def csv_info
[@mtime,@hash,@filepaths.join(';')].join(',')
end
def inline_info def inline_info
"#{@mtime} : #{@hash} : #{@filepaths.join(',')}" "#{@mtime} : #{@hash} : #{@filepaths.join(',')}"
end end
def info
case $CONFIG[:info_format]
when 'long'
long_info
when 'inline'
inline_info
when 'csv'
csv_info
end
end
def content def content
log "BucketEntry[#{@hash}] getting contents" log "BucketEntry[#{@hash}] getting contents"
File.open("#{@entry_dir}/contents",'r') do |file| File.open("#{@entry_dir}/contents",'r') do |file|
@ -224,6 +336,15 @@ class Bucket
load_bucket load_bucket
end end
def select(&proc)
@entries.each_value.select &proc
end
def any?(&proc)
@entries.each_value.any? &proc
end
def load_bucket def load_bucket
log "Bucket[#{@bucketdir}] is loading entries" log "Bucket[#{@bucketdir}] is loading entries"
Dir["#{@bucketdir}/**/paths"].each.map{|path| File.dirname(path)}.each do |directory| Dir["#{@bucketdir}/**/paths"].each.map{|path| File.dirname(path)}.each do |directory|
@ -234,6 +355,18 @@ class Bucket
end end
log "Bucket[#{@bucketdir}] was loaded" log "Bucket[#{@bucketdir}] was loaded"
end end
def filenames
filenames = Array.new
@entries.each_value do |entry|
entry.filepaths.each do |path|
if not filenames.include? path
filenames.push(path)
end
end
end
filenames
end
end end
# END: Classes # END: Classes
@ -243,12 +376,10 @@ end
# BEGIN: Work Functions # BEGIN: Work Functions
def search_entries_paths (bucket) def search_entries_paths (bucket)
log "user entered" puts bucket.select{|entry| entry.path_include? $CONFIG[:search_term]}.sort_by(&:mtime).map(&:info)
puts bucket.entries.each_value.select {|entry| entry.path_include? $CONFIG[:search_term]}.map{|entry| entry.inline_info}.sort.join("\n")
end end
def get_content_of_entry_hash (bucket) def get_content_of_entry_hash (bucket)
log "user entered"
if bucket.entries.has_key? $CONFIG[:search_term] if bucket.entries.has_key? $CONFIG[:search_term]
puts bucket.entries[$CONFIG[:search_term]].content puts bucket.entries[$CONFIG[:search_term]].content
else else
@ -258,40 +389,84 @@ def get_content_of_entry_hash (bucket)
end end
def list_all_entries (bucket) def list_all_entries (bucket)
puts bucket.entries.each_value.each.map{|entry| entry.inline_info}.sort.join("\n") puts bucket.filenames.map{|filename| bucket.select{|entry| entry.path_include? filename}.sort_by(&:mtime).map(&:info)}
end end
def list_entry_files (bucket) def list_entry_files (bucket)
filenames = Array.new puts bucket.filenames.sort.join("\n")
bucket.entries.each_value do |entry| end
entry.filepaths.each do |path|
if not filenames.include? path def get_entry_by_file (bucket, filenames)
filenames.push(path) entry = nil
if filenames.count == 1
filename = filenames[0]
else
filename = get_selections filenames, "Your filename matched multiple files. Please select one to restore"
end
entries = bucket.select{|entry| entry.path_include? filename}
if entries.count == 1
entry = entries.first
else
while true
mtimes = entries.map{|entry| entry.mtime}
entry_mtime = get_selections(mtimes , "Which timestamp to you want to revert the file to?")
entry = entries.lazy.select{|entry| entry.mtime == entry_mtime}.first
if get_verification "Do you want to see the contents of #{filename} at this time?"
puts entry.content
if get_verification "Is this the entry you want to overwrite #{filename} with?"
break
end
else
break
end end
end end
end end
puts filenames.sort.join("\n")
if filename[0] != '/'
filename = "/#{filename}"
end
return entry, filename
end
def get_entry_by_hash (bucket)
if bucket.entries.has_key? $CONFIG[:search_term]
entry = bucket.entries[$CONFIG[:search_term]]
filepath = ""
if entry.filepaths.count == 1
filepath = entry.filepaths[0]
else
filepath = get_selections entry.filepaths, "What filepath do you wish to restore to?"
end
if filepath[0] != '/'
filepath = "/#{filepath}"
end
return entry, filepath
else
puts "There were no entries corresponding to #{$CONFIG[:search_term]}"
end
end end
def restore_entry (bucket) def restore_entry (bucket)
if bucket.entries.has_key? $CONFIG[:search_term] entry = nil
entry = bucket.entries[$CONFIG[:search_term]] if bucket.any?{ |entry| entry.path_include? $CONFIG[:search_term] }
if entry.filepaths.count == 1 filenames = bucket.filenames.select {|filename| filename.include? $CONFIG[:search_term]}
filepath = entry.filepaths[0] entry, filepath = get_entry_by_file bucket, filenames
if filepath[0] != '/' else
filepath = "/#{filepath}" entry, filepath = get_entry_by_hash bucket
end end
if get_verification "Are you sure you want to overwrite #{filepath}?"
File.open(filepath,'w') do |file| if get_verification "Are you sure you want to overwrite #{filepath}?"
file.write(entry.content) File.open(filepath,'w') do |file|
end file.write(entry.content)
else
puts "Ok not overwriting."
end
else
end end
else else
puts "There were no entries corresponding to #{$CONFIG[:search_term]}" puts "Ok not overwriting."
end end
end end