module SugarUtils::File

@api

Public Class Methods

append(filename, data, options = {}) click to toggle source

Append to an existing file, or create the file if it does not exist.

@note Either option :mode or :perm can be used to specific the permissions on the file being written to. This aliasing is used because both these names are used in the standard library, File.open uses :perm and FileUtils uses :mode. The user can choose whichever alias makes their code most readable.

@param filename [String] @param data [#to_s] @param options [Hash] @option options [Integer] :timeout (10) @option options [Boolean] :flush (false) @option options [String, Integer] :owner @option options [String, Integer] :group @option options [Integer] :mode (0o644) @option options [Integer] :perm (0o644)

@raise [SugarUtils::File::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 305
def self.append(filename, data, options = {}) # rubocop:disable MethodLength, AbcSize
  write_options = WriteOptions.new(filename, options)

  FileUtils.mkdir_p(::File.dirname(filename))
  ::File.open(filename, 'a', write_options.perm) do |file|
    flock_exclusive(file, options)

    file.puts(data.to_s)

    # Flush and fsync to be 100% sure we write this data out now because we
    # are often reading it immediately and if the OS is buffering, it is
    # possible we might read it before it is been physically written to
    # disk. We are not worried about speed here, so this should be OKAY.
    if write_options.flush?
      file.flush
      file.fsync
    end
  end

  change_access(
    filename,
    write_options.owner,
    write_options.group,
    write_options.perm
  )
rescue Timeout::Error
  raise(Error, "Unable to write #{filename} because it is locked")
rescue SystemCallError, IOError => e
  raise(Error, "Unable to write #{filename} with #{e}")
end
atomic_write(filename, data, options = {}) click to toggle source

Atomically write to an existing file, overwriting it, or create the file if it does not exist.

@note Either option :mode or :perm can be used to specific the permissions on the file being written to. This aliasing is used because both these names are used in the standard library, File.open uses :perm and FileUtils uses :mode. The user can choose whichever alias makes their code most readable.

@param filename [String] @param data [#to_s] @param options [Hash] @option options [Integer] :timeout (10) @option options [Boolean] :flush (false) @option options [String, Integer] :owner @option options [String, Integer] :group @option options [Integer] :mode (0o644) @option options [Integer] :perm (0o644)

@raise [SugarUtils::File::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 217
def self.atomic_write(filename, data, options = {}) # rubocop:disable MethodLength, AbcSize
  write_options = WriteOptions.new(filename, options)

  # @note This method is similar to the atomic_write which is implemented in
  # ActiveSupport. We re-implemented the method because of the following:
  # * we needed the method, but wanted to avoid pulling in the entire
  #   ActiveSupport gem.
  # * we wnated to keep the behaviour and interface consistent with the other
  #   SugarUtils write methods
  #
  # @see https://apidock.com/rails/File/atomic_write/class
  FileUtils.mkdir_p(::File.dirname(filename))
  Tempfile.open(::File.basename(filename, '.*'), ::File.dirname(filename)) do |temp_file|
    temp_file.puts(data.to_s)
    # Flush and fsync to be 100% sure we write this data out now because we
    # are often reading it immediately and if the OS is buffering, it is
    # possible we might read it before it is been physically written to
    # disk. We are not worried about speed here, so this should be OKAY.
    if write_options.flush?
      temp_file.flush
      temp_file.fsync
    end
    temp_file.close

    ::File.open(filename, 'w+', write_options.perm) do |file|
      flock_exclusive(file, options)
      FileUtils.move(temp_file.path, filename)
    end
  end

  change_access(
    filename,
    write_options.owner,
    write_options.group,
    write_options.perm
  )
rescue Timeout::Error
  raise(Error, "Unable to write #{filename} because it is locked")
rescue SystemCallError, IOError => e
  raise(Error, "Unable to write #{filename} with #{e}")
end
change_access(filename, owner, group, permission) click to toggle source

Change all of the access values for the specified file including:

  • owner

  • group

  • permissions

@note Although the are all required, nil can be passed to any of them and those nils will be skipped. Hopefully, this will avoid conditions in the calling code because the optional parameters will just be passed in and skipped when they are missing.

@param filename [String] @param owner [nil, Integer, String] @param group [nil, Integer, String] @param permission [nil, Integer]

@raise [SugarUtils::File::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 58
def self.change_access(filename, owner, group, permission)
  FileUtils.chown(owner, group, filename)
  FileUtils.chmod(permission, filename) if permission
  nil
rescue SystemCallError, IOError
  raise(Error, "Unable to change access on #{filename}")
end
flock_exclusive(file, options = {}) click to toggle source

@param file [File] @param options [Hash] @option options [Integer] :timeout (10)

@raise [Timeout::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 35
def self.flock_exclusive(file, options = {})
  timeout = options[:timeout] || 10
  Timeout.timeout(timeout) { file.flock(::File::LOCK_EX) }
end
flock_shared(file, options = {}) click to toggle source

@param file [File] @param options [Hash] @option options [Integer] :timeout (10)

@raise [Timeout::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 23
def self.flock_shared(file, options = {})
  timeout = options[:timeout] || 10
  Timeout.timeout(timeout) { file.flock(::File::LOCK_SH) }
end
read(filename, options = {}) click to toggle source

@param filename [String] @param options [Hash] @option options [Integer] :timeout (10) @option options [Boolean] :raise_on_missing (true) @option options [String] :value_on_missing ('') which specifies the

value to return if the file is missing and raise_on_missing is false

@option options [Boolean, String] :scrub_encoding scrub incorrectly

encoded characters with this value, or with '' if the value is true

@raise [SugarUtils::File::Error]

@return [String]

# File lib/sugar_utils/file.rb, line 78
def self.read(filename, options = {}) # rubocop:disable MethodLength
  options[:value_on_missing] ||= ''
  options[:raise_on_missing] = true if options[:raise_on_missing].nil?

  result =
    ::File.open(filename, ::File::RDONLY) do |file|
      flock_shared(file, options)
      file.read
    end

  return result unless options[:scrub_encoding]

  SugarUtils.scrub_encoding(result, options[:scrub_encoding])
rescue SystemCallError, IOError
  raise(Error, "Cannot read #{filename}") if options[:raise_on_missing]

  options[:value_on_missing]
rescue Timeout::Error
  raise(Error, "Cannot read #{filename} because it is locked")
end
read_json(filename, options = {}) click to toggle source

@param filename [String] @param options [Hash] @option options [Integer] :timeout (10) @option options [Boolean] :raise_on_missing (true)

@raise [SugarUtils::File::Error]

@return [Object]

# File lib/sugar_utils/file.rb, line 107
def self.read_json(filename, options = {})
  options[:value_on_missing] = :missing

  read_result = read(filename, options)
  return {} if read_result == :missing

  MultiJson.load(read_result)
rescue MultiJson::ParseError
  raise(Error, "Cannot parse #{filename}")
end
touch(filename, options = {}) click to toggle source

Touch the specified file.

@param filename [String] @param options [Hash] @option options [String, Integer] :owner @option options [String, Integer] :group @option options [Integer] :mode @option options [Integer] :perm @option options [Integer] :mtime

@return [void]

# File lib/sugar_utils/file.rb, line 129
def self.touch(filename, options = {})
  write_options = WriteOptions.new(filename, options)

  FileUtils.mkdir_p(::File.dirname(filename))
  FileUtils.touch(filename, write_options.slice(:mtime))
  change_access(
    filename,
    write_options.owner,
    write_options.group,
    write_options.perm(nil)
  )
end
write(filename, data, options = {}) click to toggle source

Write to an existing file, overwriting it, or create the file if it does not exist.

@note Either option :mode or :perm can be used to specific the permissions on the file being written to. This aliasing is used because both these names are used in the standard library, File.open uses :perm and FileUtils uses :mode. The user can choose whichever alias makes their code most readable.

@param filename [String] @param data [#to_s] @param options [Hash] @option options [Integer] :timeout (10) @option options [Boolean] :flush (false) @option options [String, Integer] :owner @option options [String, Integer] :group @option options [Integer] :mode (0o644) @option options [Integer] :perm (0o644)

@raise [SugarUtils::File::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 164
def self.write(filename, data, options = {}) # rubocop:disable MethodLength, AbcSize
  write_options = WriteOptions.new(filename, options)

  FileUtils.mkdir_p(::File.dirname(filename))
  ::File.open(filename, 'w+', write_options.perm) do |file|
    flock_exclusive(file, options)

    file.puts(data.to_s)

    # Flush and fsync to be 100% sure we write this data out now because we
    # are often reading it immediately and if the OS is buffering, it is
    # possible we might read it before it is been physically written to
    # disk. We are not worried about speed here, so this should be OKAY.
    if write_options.flush?
      file.flush
      file.fsync
    end
  end

  change_access(
    filename,
    write_options.owner,
    write_options.group,
    write_options.perm
  )
rescue Timeout::Error
  raise(Error, "Unable to write #{filename} because it is locked")
rescue SystemCallError, IOError => e
  raise(Error, "Unable to write #{filename} with #{e}")
end
write_json(filename, data, options = {}) click to toggle source

Write the data parameter as JSON to the filename path.

@note Either option :mode or :perm can be used to specific the permissions on the file being written to. This aliasing is used because both these names are used in the standard library, File.open uses :perm and FileUtils uses :mode. The user can choose whichever alias makes their code most readable.

@param filename [String] @param data [#to_json] @param options [Hash] @option options [Integer] :timeout (10) @option options [Boolean] :flush (false) @option options [String, Integer] :owner @option options [String, Integer] :group @option options [Integer] :mode (0o644) @option options [Integer] :perm (0o644)

@raise [SugarUtils::File::Error]

@return [void]

# File lib/sugar_utils/file.rb, line 280
def self.write_json(filename, data, options = {})
  atomic_write(filename, MultiJson.dump(data, pretty: true), options)
end

Private Class Methods

deprecate_option(_method, option_name, option_repl, year, month) click to toggle source

Following the same pattern as the existing stdlib method deprecation module. @see ruby-doc.org/stdlib-2.0.0/libdoc/rubygems/rdoc/Gem/Deprecate.html

# File lib/sugar_utils/file.rb, line 341
def self.deprecate_option(_method, option_name, option_repl, year, month) # rubocop:disable MethodLength
  return if Gem::Deprecate.skip

  klass  = is_a?(Module)
  target = klass ? "#{self}." : "#{self.class}#"

  # Determine the method
  method = caller_locations(1, 1).first.label

  # Determine the caller
  external_caller             = caller_locations(2, 1).first
  location_of_external_caller = "#{external_caller.absolute_path}:#{external_caller.lineno}"

  msg = [
    "NOTE: #{target}#{method} option :#{option_name} is deprecated",
    case option_repl
    when :none
      ' with no replacement'
    when String
      option_repl
    else
      "; use :#{option_repl} instead"
    end,
    format('. It will be removed on or after %4d-%02d-01.', year, month),
    "\n#{target}#{method} called from #{location_of_external_caller}"
  ]
  warn("#{msg.join}.")
end