module SugarUtils::File
@api
Public Class Methods
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
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 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
@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
@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
@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 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 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 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
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