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