module EasyState
Attributes
states_location[W]
Public Instance Methods
changed_keys(previous_state_hash, new_state_hash)
click to toggle source
Description:
Checks to see if any values have changed or are missing for any of the keys in the state hashes.
Returns:
A hash containing the changed keys and their checksums.
Parameters:
previous_state_hash: A ruby hash containing all the checksums of the previous states. This should be the full hash at the root level, not the actual previous state of the specified state_path. default: If no hash is specified, it will assume filesystem storage is being used and will look for the file and read it. new_state_hash: A ruby hash containing all the checksums of the previous states. This should be the full hash at the root level, not the actual previous state of the specified state_path. default: If no hash is specified, it will assume filesystem storage is being used and will look for the file and read it.
# File lib/easy_state/state.rb, line 237 def changed_keys(previous_state_hash, new_state_hash) left_diff = Hashly.deep_diff(previous_state_hash, new_state_hash) right_diff = Hashly.deep_diff(new_state_hash, previous_state_hash) Hashly.deep_merge(left_diff, right_diff) end
config()
click to toggle source
# File lib/easy_state/config.rb, line 4 def config @config ||= EasyJSON.config(defaults: defaults) end
defaults()
click to toggle source
# File lib/easy_state/config.rb, line 8 def defaults { 'paths' => { 'cache' => Dir.tmpdir, }, } end
directory_state(path, filter: nil)
click to toggle source
Description:
Read the state of a directory.
Returns:
A hash with keys being the relative path from the directory and values being the checksum of each path.
Parameters:
path: The path to the directory filter: (optional) A wildcard filter for searching for specific files. default: '**/*'
# File lib/easy_state/state.rb, line 41 def directory_state(path, filter: nil) filter ||= '**/*' path = path.tr('\\', '/') Dir.glob("#{path}/#{filter}").each_with_object({}) do |entry_path, hash| relative_path = entry_path.sub(path, '') next hash[relative_path] = 'directory' if ::File.directory?(entry_path) hash[relative_path] = Digest::SHA256.file(entry_path).hexdigest end end
hash_state(hash, sort_if_sortable: true)
click to toggle source
Description:
Takes checksums of every leaf in a hash so that no sensitive values are present.
Returns:
A hash containing all the same keys, but the values that are not a hash are converted to checksums recursively.
Parameters:
hash: A ruby hash to be converted into a state hash sort_if_sortable: (optional) Sort objects that are sortable (like an Array) before taking their checksum. default: true
# File lib/easy_state/state.rb, line 59 def hash_state(hash, sort_if_sortable: true) return hash if hash.nil? || hash.empty? Hashly.stringify_all_keys(hash).each_with_object({}) do |(k, v), checksum_hash| next checksum_hash[k] = hash_state(v) if v.is_a?(::Hash) value = sort_if_sortable && v.respond_to?(:sort) ? v.sort : v checksum_hash[k] = Digest::SHA256.hexdigest(value.to_s) end end
object_state(obj, sort_if_sortable: true)
click to toggle source
Description:
Gets the state of an object. It takes a SHA256 checksum of the content of the object's to_s. For hashes, it traverses the entire hash recursively taking checksums of every non-hash value.
Parameters:
obj: An object to be converted into a state. It can be any type of object, but the to_s method must represent its state. sort_if_sortable: (optional) Sorts sortable objects passed such as Arrays before taking a checksum of the object for comparison. default: true
# File lib/easy_state/state.rb, line 22 def object_state(obj, sort_if_sortable: true) case obj when ::Hash processed_hash = sort_if_sortable ? Hashly.deep_sort(obj) : obj hash_state(processed_hash) else processed_obj = sort_if_sortable && obj.respond_to?(:sort) ? obj.sort : obj Digest::SHA256.hexdigest(processed_obj.to_s) end end
object_state_changed?(state_path, obj, previous_state_hash: nil, states_folder: nil, sort_if_sortable: true)
click to toggle source
Description:
Checks to see if the entry (obj) at the path specified matches what was last saved at that path in the previous state hash.
Returns:
true or false
Parameters:
state_path: A path separated by forward slashes specifying where to store the state, much like Hashicorp Vault paths. It is NOT a filesystem path - EG: 'myproject/environment/sql_credentials' obj: An object containing the value of the desired state (not a checksum). It can be any type of object. previous_state_hash: (optional) A ruby hash containing all the checksums of the previous states. This should be the full hash at the root level, not the actual previous state of the specified state_path. default: If no hash is specified, it will assume filesystem storage is being used and will look for the file and read it. states_folder: (optional) The location where the state files are saved if previous_state_hash is nil. default: The setting specified in the config which defaults to a subfolder under the user's temp directory. sort_if_sortable: (optional) Sorts sortable objects passed such as Arrays before taking a checksum of the object for comparison. default: true
# File lib/easy_state/state.rb, line 203 def object_state_changed?(state_path, obj, previous_state_hash: nil, states_folder: nil, sort_if_sortable: true) current_obj_state = object_state(obj, sort_if_sortable: sort_if_sortable) state_changed?(state_path, current_obj_state, previous_state_hash: previous_state_hash, states_folder: states_folder) end
read_state(state_path, previous_state_hash: nil, states_folder: nil, silent: false)
click to toggle source
Description:
Reads the state at the state_path specified.
Returns:
The checksum or hash of checksums at the state_path specified.
Parameters:
state_path: A path separated by forward slashes specifying where to store the state, much like Hashicorp Vault paths. It is NOT a filesystem path - EG: 'myproject/environment/sql_credentials' previous_state_hash: (optional) A ruby hash containing all the checksums of the previous states. This should be the full hash at the root level (the value of the root key), not the actual previous state of the specified state_path. default: If no hash is specified, it will assume filesystem storage is being used and will look for the file and read it if it exists. states_folder: (optional) The location where the state files are saved if previous_state_hash is nil. default: The setting specified in the config which defaults to a subfolder under the user's temp directory.
# File lib/easy_state/state.rb, line 151 def read_state(state_path, previous_state_hash: nil, states_folder: nil, silent: false) raise 'Failed to save state! state_path is a required argument!' if state_path.nil? || state_path.empty? state_keys = state_path.split('/') states_folder ||= states_location previous_state_hash ||= read_state_file(state_keys.first, states_folder: states_folder, silent: silent) return previous_state_hash if state_keys.count == 1 # The previous_state_hash is the entire content of the root level state current_path_hash = previous_state_hash state_keys.drop(1).each do |state_key| return nil if current_path_hash[state_key].nil? return current_path_hash[state_key] if state_key == state_keys.last # on the leaf (last key), return the checksum current_path_hash = current_path_hash[state_key] # move up one level end end
read_state_file(state_path_root, states_folder: nil, silent: false)
click to toggle source
Description:
Reads a state file from the filesystem.
Returns:
A hash containing the last known states for the state_path_root specified.
Parameters:
state_path_root: The first segment of a state_path (first entry before the first slash). states_folder: (optional) The location where the state files are saved if previous_state_hash is nil. default: The setting specified in the config which defaults to a subfolder under the user's temp directory.
# File lib/easy_state/state.rb, line 173 def read_state_file(state_path_root, states_folder: nil, silent: false) states_folder ||= states_location state_hash = {} return state_hash unless ::File.exist?("#{states_folder}/#{state_path_root}.state") EasyIO.logger.info 'Reading state file...' unless silent EasyIO::Disk.open_file_and_wait_for_exclusive_lock("#{states_folder}/#{state_path_root}.state") do |file| content = file.read state_hash = JSON.parse(content) end state_hash rescue JSON::ParserError EasyIO.logger.warn "#{states_folder}/#{state_path_root}.state did not contain valid json! Resetting state!" {} end
save_directory_state(path, filter: nil)
click to toggle source
Description:
Save the state of the directory structure at the path specified
Parameters:
path: A filesystem path for which the state is to be saved. This will take checksums of all files under this path recursively. filter: (optional) A glob-style filter can be provided in order to use wildcards. EG: (**/myfile*.config) default: '**/*' (all files and subfolders)
# File lib/easy_state/state.rb, line 89 def save_directory_state(path, filter: nil) checksum_hash = directory_state(path, filter: filter) checksum_hash['directory_state'] = obj_checksum(checksum_hash) save_object_state(path.tr(':', '_drive'), checksum_hash) end
save_object_state(state_path, obj, states_folder: nil, sort_if_sortable: true)
click to toggle source
Description:
Saves the state of the object at the state_path specified using filesystem storage.
Parameters:
state_path: A path separated by forward slashes specifying where to store the state, much like Hashicorp Vault paths. It is NOT a filesystem path - EG: 'myproject/environment/sql_credentials' obj: An object containing the value of the desired state (not a checksum). It can be any type of object. states_folder: (optional) The location where the state files are saved if previous_state_hash is nil. default: The setting specified in the config which defaults to a subfolder under the user's temp directory. sort_if_sortable: (optional) Sorts sortable objects passed such as Arrays before taking a checksum of the object for comparison. default: true
# File lib/easy_state/state.rb, line 78 def save_object_state(state_path, obj, states_folder: nil, sort_if_sortable: true) obj_checksum = obj_checksum(obj, sort_if_sortable: sort_if_sortable) save_state(state_path, obj_checksum, states_folder: states_folder) end
save_state(state_path, checksum_data, states_folder: nil, merge_with_existing_states: true, silent: false)
click to toggle source
Description:
Saves the state provided at the state_path specified, updating the appropriate state file in the filesystem.
Parameters:
state_path: A path separated by forward slashes specifying where to store the state, much like Hashicorp Vault paths. It is NOT a filesystem path - EG: 'myproject/environment/sql_credentials' checksum_data: a SHA256 checksum or hash of checksums to save at the path specified. states_folder: (optional) The location where the state files are saved. default: The setting specified in the config which defaults to a subfolder under the user's temp directory.
# File lib/easy_state/state.rb, line 103 def save_state(state_path, checksum_data, states_folder: nil, merge_with_existing_states: true, silent: false) raise 'Failed to save state! state_path is a required argument!' if state_path.nil? || state_path.empty? state_keys = state_path.split('/') states_folder ||= states_location state_hash = {} current_path_hash = state_hash if state_keys.count == 1 raise "When saving the entire state of a file, the checksum_data must be in the form of a hash! Type given: #{checksum_data.class}" unless checksum_data.is_a?(::Hash) state_hash = checksum_data else state_keys.drop(1).each do |state_key| if state_key == state_keys.last current_path_hash[state_key] = checksum_data # on the leaf (last key), save the object's checksum to it break end current_path_hash[state_key] = {} current_path_hash = current_path_hash[state_key] # move to the next level in the state_path end end FileUtils.mkdir_p(states_folder) unless ::File.directory?(states_folder) EasyIO::Disk.open_file_and_wait_for_exclusive_lock("#{states_folder}/#{state_keys.first}.state") do |file| file_content = file.read previous_state_hash = begin file_content.strip.empty? ? {} : JSON.parse(file_content) rescue JSON::ParserError {} end new_state_content = merge_with_existing_states ? Hashly.deep_merge(previous_state_hash, state_hash) : state_hash file.truncate(0) file.rewind file.write(JSON.pretty_generate(new_state_content)) EasyIO.logger.info 'State saved to file.' unless silent end end
state_changed?(state_path, current_obj_state, previous_state_hash: nil, states_folder: nil)
click to toggle source
Description:
Checks to see if the state at the path specified matches what was last saved at that path in the previous state hash.
Returns:
true or false
Parameters:
state_path: A path separated by forward slashes specifying where to store the state, much like Hashicorp Vault paths. It is NOT a filesystem path - EG: 'myproject/environment/sql_credentials' current_obj_state: An object containing the checksum or hash of checksums of the current state. previous_state_hash: (optional) A ruby hash containing all the checksums of the previous states. This should be the full hash at the root level, not the actual previous state of the specified state_path. default: If no hash is specified, it will assume filesystem storage is being used and will look for the file and read it. states_folder: (optional) The location where the state files are saved if previous_state_hash is nil. default: The setting specified in the config which defaults to a subfolder under the user's temp directory.
# File lib/easy_state/state.rb, line 221 def state_changed?(state_path, current_obj_state, previous_state_hash: nil, states_folder: nil) previous_state = read_state(state_path, previous_state_hash: previous_state_hash, states_folder: states_folder, silent: true) previous_state != current_obj_state end
states_location()
click to toggle source
Description:
Provides the location where filesystem states are saved.
Returns:
A string containing the filesystem path to the states folder where state files are saved.
# File lib/easy_state/state.rb, line 11 def states_location @states_location ||= "#{@cache_path}/state_checksums" end