Module: SugarUtils::File

Defined in:
lib/sugar_utils/file.rb,
lib/sugar_utils/file/write_options.rb

Defined Under Namespace

Classes: Error, WriteOptions

Class Method Summary collapse

Class Method Details

.append(filename, data, options = {}) ⇒ void

Note:

Either option :mode or :perm can be used to specific the permissions

This method returns an undefined value.

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

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.

Parameters:

  • filename (String)
  • data (#to_s)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10
  • :flush (Boolean) — default: false
  • :owner (String, Integer)
  • :group (String, Integer)
  • :mode (Integer) — default: 0o644
  • :perm (Integer) — default: 0o644

Raises:


305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# 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 = {}) ⇒ void

Note:

Either option :mode or :perm can be used to specific the permissions

This method returns an undefined value.

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

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.

Parameters:

  • filename (String)
  • data (#to_s)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10
  • :flush (Boolean) — default: false
  • :owner (String, Integer)
  • :group (String, Integer)
  • :mode (Integer) — default: 0o644
  • :perm (Integer) — default: 0o644

Raises:


217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# 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) ⇒ void

Note:

Although the are all required, nil can be passed to any of them and

This method returns an undefined value.

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

  • owner

  • group

  • permissions

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.

Parameters:

  • filename (String)
  • owner (nil, Integer, String)
  • group (nil, Integer, String)
  • permission (nil, Integer)

Raises:


58
59
60
61
62
63
64
# 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 = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • file (File)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10

Raises:

  • (Timeout::Error)

35
36
37
38
# 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 = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • file (File)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10

Raises:

  • (Timeout::Error)

23
24
25
26
# 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 = {}) ⇒ String

Parameters:

  • filename (String)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10
  • :raise_on_missing (Boolean) — default: true
  • :value_on_missing (String) — default: ''

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

  • :scrub_encoding (Boolean, String)

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

Returns:

  • (String)

Raises:


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 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 = {}) ⇒ Object

Parameters:

  • filename (String)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10
  • :raise_on_missing (Boolean) — default: true

Returns:

  • (Object)

Raises:


107
108
109
110
111
112
113
114
115
116
# 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 = {}) ⇒ void

This method returns an undefined value.

Touch the specified file.

Parameters:

  • filename (String)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :owner (String, Integer)
  • :group (String, Integer)
  • :mode (Integer)
  • :perm (Integer)
  • :mtime (Integer)

129
130
131
132
133
134
135
136
137
138
139
140
# 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 = {}) ⇒ void

Note:

Either option :mode or :perm can be used to specific the permissions

This method returns an undefined value.

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

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.

Parameters:

  • filename (String)
  • data (#to_s)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10
  • :flush (Boolean) — default: false
  • :owner (String, Integer)
  • :group (String, Integer)
  • :mode (Integer) — default: 0o644
  • :perm (Integer) — default: 0o644

Raises:


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# 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 = {}) ⇒ void

Note:

Either option :mode or :perm can be used to specific the permissions

This method returns an undefined value.

Write the data parameter as JSON to the filename path.

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.

Parameters:

  • filename (String)
  • data (#to_json)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :timeout (Integer) — default: 10
  • :flush (Boolean) — default: false
  • :owner (String, Integer)
  • :group (String, Integer)
  • :mode (Integer) — default: 0o644
  • :perm (Integer) — default: 0o644

Raises:


280
281
282
# 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