sugar_utils 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +17 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +30 -3
- data/.ruby-version +1 -0
- data/CHANGELOG.md +23 -0
- data/README.md +47 -19
- data/Rakefile +36 -8
- data/doc/dependency_decisions.yml +25 -0
- data/features/append_file.feature +46 -0
- data/features/atomic_write_file.feature +46 -0
- data/features/change_file_access.feature +27 -0
- data/features/ensure_boolean.feature +34 -0
- data/features/ensure_integer.feature +19 -0
- data/features/lock_file.feature +26 -0
- data/features/read_file.feature +30 -0
- data/features/read_json_file.feature +43 -0
- data/features/scrub_encoding.feature +25 -0
- data/features/step_definitions/steps.rb +38 -0
- data/features/support/env.rb +34 -0
- data/features/touch_file.feature +28 -0
- data/features/write_file.feature +46 -0
- data/features/write_json_file.feature +52 -0
- data/lib/sugar_utils/file/write_options.rb +56 -0
- data/lib/sugar_utils/file.rb +219 -76
- data/lib/sugar_utils/version.rb +1 -2
- data/lib/sugar_utils.rb +34 -4
- data/spec/spec_helper.rb +8 -6
- data/spec/sugar_utils/file/write_options_spec.rb +77 -0
- data/spec/sugar_utils/file_spec.rb +392 -80
- data/spec/sugar_utils_spec.rb +41 -13
- data/sugar_utils.gemspec +21 -13
- metadata +125 -40
- data/.travis.yml +0 -22
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aruba/cucumber'
|
4
|
+
|
5
|
+
# @see spec/spec_helper.rb
|
6
|
+
RSpec::Matchers.define :have_owner do |expected|
|
7
|
+
match do |actual|
|
8
|
+
next false unless File.exist?(actual)
|
9
|
+
|
10
|
+
@actual = Etc.getpwuid(File.stat(filename).uid).name
|
11
|
+
@expected = expected
|
12
|
+
values_match?(@expected, @actual)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
RSpec::Matchers.define :have_group do |expected|
|
17
|
+
match do |actual|
|
18
|
+
next false unless File.exist?(actual)
|
19
|
+
|
20
|
+
@actual = Etc.getgrgid(File.stat(actual).gid).name
|
21
|
+
@expected = expected
|
22
|
+
values_match?(@expected, @actual)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
RSpec::Matchers.define :have_mtime do |expected|
|
27
|
+
match do |actual|
|
28
|
+
next false unless File.exist?(actual)
|
29
|
+
|
30
|
+
@actual = File.stat(actual).mtime.to_i
|
31
|
+
@expected = expected
|
32
|
+
values_match?(@expected, @actual)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Feature: Touch a file
|
2
|
+
|
3
|
+
Scenario: Touch a file
|
4
|
+
When I run the following Ruby code:
|
5
|
+
"""ruby
|
6
|
+
require 'sugar_utils'
|
7
|
+
puts SugarUtils::File.touch('dir/test')
|
8
|
+
"""
|
9
|
+
Then the file named "dir/test" should exist
|
10
|
+
|
11
|
+
# TODO: Fix the owner/group setting check
|
12
|
+
Scenario: Touch a file and reset its permissions and mtime
|
13
|
+
When I run the following Ruby code:
|
14
|
+
"""ruby
|
15
|
+
require 'sugar_utils'
|
16
|
+
puts SugarUtils::File.touch(
|
17
|
+
'dir/test',
|
18
|
+
# owner: 'nobody',
|
19
|
+
# group: 'nogroup',
|
20
|
+
mode: 0o777,
|
21
|
+
mtime: 0
|
22
|
+
)
|
23
|
+
"""
|
24
|
+
Then the file named "dir/test" should exist
|
25
|
+
And the file named "dir/test" should have permissions "777"
|
26
|
+
And the file named "dir/test" should have modification time "0"
|
27
|
+
# And the file named "dir/test" should have owner "nobody"
|
28
|
+
# And the file named "dir/test" should have group "nogroup"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
Feature: Write to a file
|
2
|
+
|
3
|
+
Scenario: Write a file
|
4
|
+
When I run the following Ruby code:
|
5
|
+
"""ruby
|
6
|
+
require 'sugar_utils'
|
7
|
+
puts SugarUtils::File.write('dir/test', 'foobar')
|
8
|
+
"""
|
9
|
+
Then the file named "dir/test" should contain exactly:
|
10
|
+
"""
|
11
|
+
foobar
|
12
|
+
"""
|
13
|
+
|
14
|
+
Scenario: Overwrite a file
|
15
|
+
Given a file named "dir/test" with "deadbeef"
|
16
|
+
When I run the following Ruby code:
|
17
|
+
"""ruby
|
18
|
+
require 'sugar_utils'
|
19
|
+
puts SugarUtils::File.write('dir/test', 'foobar')
|
20
|
+
"""
|
21
|
+
Then the file named "dir/test" should contain exactly:
|
22
|
+
"""
|
23
|
+
foobar
|
24
|
+
"""
|
25
|
+
|
26
|
+
# TODO: Fix the owner/group setting check
|
27
|
+
Scenario: Overwrite a file and reset its permissions
|
28
|
+
Given a file named "dir/test" with "deadbeef"
|
29
|
+
When I run the following Ruby code:
|
30
|
+
"""ruby
|
31
|
+
require 'sugar_utils'
|
32
|
+
puts SugarUtils::File.write(
|
33
|
+
'dir/test',
|
34
|
+
'foobar',
|
35
|
+
# owner: 'nobody',
|
36
|
+
# group: 'nogroup',
|
37
|
+
mode: 0o777
|
38
|
+
)
|
39
|
+
"""
|
40
|
+
Then the file named "dir/test" should contain exactly:
|
41
|
+
"""
|
42
|
+
foobar
|
43
|
+
"""
|
44
|
+
And the file named "dir/test" should have permissions "777"
|
45
|
+
# And the file named "dir/test" should have owner "nobody"
|
46
|
+
# And the file named "dir/test" should have group "nogroup"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
Feature: Write JSON to a file
|
2
|
+
|
3
|
+
Scenario: Write a file
|
4
|
+
When I run the following Ruby code:
|
5
|
+
"""ruby
|
6
|
+
require 'sugar_utils'
|
7
|
+
MultiJson.use(:ok_json)
|
8
|
+
|
9
|
+
puts SugarUtils::File.write_json('dir/test.json', { key: :value })
|
10
|
+
"""
|
11
|
+
Then the file named "dir/test.json" should contain exactly:
|
12
|
+
"""
|
13
|
+
{"key":"value"}
|
14
|
+
"""
|
15
|
+
|
16
|
+
Scenario: Overwrite a file
|
17
|
+
Given a file named "dir/test.json" with "deadbeef"
|
18
|
+
When I run the following Ruby code:
|
19
|
+
"""ruby
|
20
|
+
require 'sugar_utils'
|
21
|
+
MultiJson.use(:ok_json)
|
22
|
+
|
23
|
+
puts SugarUtils::File.write_json('dir/test.json', { key: :value })
|
24
|
+
"""
|
25
|
+
Then the file named "dir/test.json" should contain exactly:
|
26
|
+
"""
|
27
|
+
{"key":"value"}
|
28
|
+
"""
|
29
|
+
|
30
|
+
# TODO: Fix the owner/group setting check
|
31
|
+
Scenario: Overwrite a file and reset its permissions
|
32
|
+
Given a file named "dir/test.json" with "deadbeef"
|
33
|
+
When I run the following Ruby code:
|
34
|
+
"""ruby
|
35
|
+
require 'sugar_utils'
|
36
|
+
MultiJson.use(:ok_json)
|
37
|
+
|
38
|
+
puts SugarUtils::File.write_json(
|
39
|
+
'dir/test.json',
|
40
|
+
{ key: :value },
|
41
|
+
# owner: 'nobody',
|
42
|
+
# group: 'nogroup',
|
43
|
+
mode: 0o777
|
44
|
+
)
|
45
|
+
"""
|
46
|
+
Then the file named "dir/test.json" should contain exactly:
|
47
|
+
"""
|
48
|
+
{"key":"value"}
|
49
|
+
"""
|
50
|
+
And the file named "dir/test.json" should have permissions "777"
|
51
|
+
# And the file named "dir/test.json" should have owner "nobody"
|
52
|
+
# And the file named "dir/test.json" should have group "nogroup"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SugarUtils
|
4
|
+
module File
|
5
|
+
# @api private
|
6
|
+
class WriteOptions
|
7
|
+
# @parma filename [String]
|
8
|
+
# @param options [Hash]
|
9
|
+
def initialize(filename, options)
|
10
|
+
@filename = filename
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
return unless filename && ::File.exist?(filename)
|
14
|
+
|
15
|
+
file_stat = ::File::Stat.new(filename)
|
16
|
+
@existing_owner = file_stat.uid
|
17
|
+
@existing_group = file_stat.gid
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean]
|
21
|
+
def flush?
|
22
|
+
@options[:flush] || false
|
23
|
+
end
|
24
|
+
|
25
|
+
# @overload perm
|
26
|
+
# The default permission is 0o644
|
27
|
+
# @overload perm(default_value)
|
28
|
+
# @param default_value [nil, Integer]
|
29
|
+
# Override the default_value including allowing nil.
|
30
|
+
#
|
31
|
+
# @return [Integer]
|
32
|
+
def perm(default_value = 0o644)
|
33
|
+
# NOTE: We are using the variable name 'perm' because that is the name
|
34
|
+
# of the argument used by File.open.
|
35
|
+
@options[:mode] || @options[:perm] || default_value
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [String, Integer]
|
39
|
+
def owner
|
40
|
+
@options[:owner] || @existing_owner
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String, Intekuuger]
|
44
|
+
def group
|
45
|
+
@options[:group] || @existing_group
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param keys [Array]
|
49
|
+
# @return [Hash]
|
50
|
+
def slice(*args)
|
51
|
+
keys = args.flatten.compact
|
52
|
+
@options.select { |k| keys.include?(k) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/sugar_utils/file.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
-
# encoding : utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'solid_assert'
|
5
4
|
require 'fileutils'
|
6
5
|
require 'multi_json'
|
7
6
|
require 'timeout'
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
require 'sugar_utils/file/write_options'
|
8
10
|
|
9
11
|
module SugarUtils
|
10
|
-
|
12
|
+
# @api
|
13
|
+
module File # rubocop:disable Metrics/ModuleLength
|
11
14
|
class Error < StandardError; end
|
12
15
|
|
13
|
-
# @param [File]
|
14
|
-
# @param [Hash]
|
16
|
+
# @param file [File]
|
17
|
+
# @param options [Hash]
|
15
18
|
# @option options [Integer] :timeout (10)
|
16
19
|
#
|
17
20
|
# @raise [Timeout::Error]
|
@@ -22,8 +25,8 @@ module SugarUtils
|
|
22
25
|
Timeout.timeout(timeout) { file.flock(::File::LOCK_SH) }
|
23
26
|
end
|
24
27
|
|
25
|
-
# @param [File]
|
26
|
-
# @param [Hash]
|
28
|
+
# @param file [File]
|
29
|
+
# @param options [Hash]
|
27
30
|
# @option options [Integer] :timeout (10)
|
28
31
|
#
|
29
32
|
# @raise [Timeout::Error]
|
@@ -34,8 +37,34 @@ module SugarUtils
|
|
34
37
|
Timeout.timeout(timeout) { file.flock(::File::LOCK_EX) }
|
35
38
|
end
|
36
39
|
|
37
|
-
#
|
38
|
-
#
|
40
|
+
# Change all of the access values for the specified file including:
|
41
|
+
# * owner
|
42
|
+
# * group
|
43
|
+
# * permissions
|
44
|
+
#
|
45
|
+
# @note Although the are all required, nil can be passed to any of them and
|
46
|
+
# those nils will be skipped. Hopefully, this will avoid conditions in the
|
47
|
+
# calling code because the optional parameters will just be passed in and
|
48
|
+
# skipped when they are missing.
|
49
|
+
#
|
50
|
+
# @param filename [String]
|
51
|
+
# @param owner [nil, Integer, String]
|
52
|
+
# @param group [nil, Integer, String]
|
53
|
+
# @param permission [nil, Integer]
|
54
|
+
#
|
55
|
+
# @raise [SugarUtils::File::Error]
|
56
|
+
#
|
57
|
+
# @return [void]
|
58
|
+
def self.change_access(filename, owner, group, permission)
|
59
|
+
FileUtils.chown(owner, group, filename)
|
60
|
+
FileUtils.chmod(permission, filename) if permission
|
61
|
+
nil
|
62
|
+
rescue SystemCallError, IOError
|
63
|
+
raise(Error, "Unable to change access on #{filename}")
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param filename [String]
|
67
|
+
# @param options [Hash]
|
39
68
|
# @option options [Integer] :timeout (10)
|
40
69
|
# @option options [Boolean] :raise_on_missing (true)
|
41
70
|
# @option options [String] :value_on_missing ('') which specifies the
|
@@ -46,7 +75,7 @@ module SugarUtils
|
|
46
75
|
# @raise [SugarUtils::File::Error]
|
47
76
|
#
|
48
77
|
# @return [String]
|
49
|
-
def self.read(filename, options = {}) # rubocop:disable MethodLength
|
78
|
+
def self.read(filename, options = {}) # rubocop:disable Metrics/MethodLength
|
50
79
|
options[:value_on_missing] ||= ''
|
51
80
|
options[:raise_on_missing] = true if options[:raise_on_missing].nil?
|
52
81
|
|
@@ -58,32 +87,17 @@ module SugarUtils
|
|
58
87
|
|
59
88
|
return result unless options[:scrub_encoding]
|
60
89
|
|
61
|
-
|
62
|
-
if options[:scrub_encoding].is_a?(String)
|
63
|
-
options[:scrub_encoding]
|
64
|
-
else
|
65
|
-
''
|
66
|
-
end
|
67
|
-
if result.respond_to?(:scrub)
|
68
|
-
result.scrub(replacement_character)
|
69
|
-
else
|
70
|
-
result.encode(
|
71
|
-
result.encoding,
|
72
|
-
'binary',
|
73
|
-
invalid: :replace,
|
74
|
-
undef: :replace,
|
75
|
-
replace: replacement_character
|
76
|
-
)
|
77
|
-
end
|
90
|
+
SugarUtils.scrub_encoding(result, options[:scrub_encoding])
|
78
91
|
rescue SystemCallError, IOError
|
79
92
|
raise(Error, "Cannot read #{filename}") if options[:raise_on_missing]
|
93
|
+
|
80
94
|
options[:value_on_missing]
|
81
95
|
rescue Timeout::Error
|
82
96
|
raise(Error, "Cannot read #{filename} because it is locked")
|
83
97
|
end
|
84
98
|
|
85
|
-
# @param [String]
|
86
|
-
# @param [Hash]
|
99
|
+
# @param filename [String]
|
100
|
+
# @param options [Hash]
|
87
101
|
# @option options [Integer] :timeout (10)
|
88
102
|
# @option options [Boolean] :raise_on_missing (true)
|
89
103
|
#
|
@@ -101,62 +115,57 @@ module SugarUtils
|
|
101
115
|
raise(Error, "Cannot parse #{filename}")
|
102
116
|
end
|
103
117
|
|
104
|
-
#
|
105
|
-
#
|
118
|
+
# Touch the specified file.
|
119
|
+
#
|
120
|
+
# @param filename [String]
|
121
|
+
# @param options [Hash]
|
106
122
|
# @option options [String, Integer] :owner
|
107
123
|
# @option options [String, Integer] :group
|
108
|
-
# @option options [Integer] :mode
|
124
|
+
# @option options [Integer] :mode
|
109
125
|
# @option options [Integer] :perm
|
110
126
|
# @option options [Integer] :mtime
|
111
127
|
#
|
112
128
|
# @return [void]
|
113
129
|
def self.touch(filename, options = {})
|
114
|
-
|
115
|
-
group = options[:group]
|
116
|
-
perm = options[:perm]
|
117
|
-
touch_options = options.select { |k| %i[mtime].include?(k) }
|
118
|
-
|
119
|
-
if options[:mode].is_a?(Integer)
|
120
|
-
perm = options[:mode]
|
121
|
-
deprecate_option(:touch, :mode, :perm, 2018, 7)
|
122
|
-
end
|
130
|
+
write_options = WriteOptions.new(filename, options)
|
123
131
|
|
124
132
|
FileUtils.mkdir_p(::File.dirname(filename))
|
125
|
-
FileUtils.touch(filename,
|
126
|
-
|
127
|
-
|
133
|
+
FileUtils.touch(filename, **write_options.slice(:mtime))
|
134
|
+
change_access(
|
135
|
+
filename,
|
136
|
+
write_options.owner,
|
137
|
+
write_options.group,
|
138
|
+
write_options.perm(nil)
|
139
|
+
)
|
128
140
|
end
|
129
141
|
|
130
|
-
#
|
131
|
-
#
|
132
|
-
#
|
142
|
+
# Write to an existing file, overwriting it, or create the file if it does
|
143
|
+
# not exist.
|
144
|
+
#
|
145
|
+
# @note Either option :mode or :perm can be used to specific the permissions
|
146
|
+
# on the file being written to. This aliasing is used because both these
|
147
|
+
# names are used in the standard library, File.open uses :perm and FileUtils
|
148
|
+
# uses :mode. The user can choose whichever alias makes their code most
|
149
|
+
# readable.
|
150
|
+
#
|
151
|
+
# @param filename [String]
|
152
|
+
# @param data [#to_s]
|
153
|
+
# @param options [Hash]
|
133
154
|
# @option options [Integer] :timeout (10)
|
134
155
|
# @option options [Boolean] :flush (false)
|
135
156
|
# @option options [String, Integer] :owner
|
136
157
|
# @option options [String, Integer] :group
|
137
|
-
# @option options [
|
158
|
+
# @option options [Integer] :mode (0o644)
|
138
159
|
# @option options [Integer] :perm (0o644)
|
139
160
|
#
|
140
161
|
# @raise [SugarUtils::File::Error]
|
141
162
|
#
|
142
163
|
# @return [void]
|
143
|
-
def self.write(filename, data, options = {}) # rubocop:disable MethodLength, AbcSize
|
144
|
-
|
145
|
-
owner = options[:owner]
|
146
|
-
group = options[:group]
|
147
|
-
perm = options[:perm] || 0o644
|
148
|
-
mode = 'w+'
|
149
|
-
|
150
|
-
if options[:mode].is_a?(Integer)
|
151
|
-
perm = options[:mode]
|
152
|
-
|
153
|
-
deprecate_option(:write, :mode, ' with an integer value; use perm instead', 2018, 7)
|
154
|
-
elsif !options[:mode].nil?
|
155
|
-
mode = options[:mode]
|
156
|
-
end
|
164
|
+
def self.write(filename, data, options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
165
|
+
write_options = WriteOptions.new(filename, options)
|
157
166
|
|
158
167
|
FileUtils.mkdir_p(::File.dirname(filename))
|
159
|
-
::File.open(filename,
|
168
|
+
::File.open(filename, 'w+', write_options.perm) do |file|
|
160
169
|
flock_exclusive(file, options)
|
161
170
|
|
162
171
|
file.puts(data.to_s)
|
@@ -165,33 +174,163 @@ module SugarUtils
|
|
165
174
|
# are often reading it immediately and if the OS is buffering, it is
|
166
175
|
# possible we might read it before it is been physically written to
|
167
176
|
# disk. We are not worried about speed here, so this should be OKAY.
|
168
|
-
if flush
|
177
|
+
if write_options.flush?
|
169
178
|
file.flush
|
170
179
|
file.fsync
|
171
180
|
end
|
181
|
+
end
|
182
|
+
|
183
|
+
change_access(
|
184
|
+
filename,
|
185
|
+
write_options.owner,
|
186
|
+
write_options.group,
|
187
|
+
write_options.perm
|
188
|
+
)
|
189
|
+
rescue Timeout::Error
|
190
|
+
raise(Error, "Unable to write #{filename} because it is locked")
|
191
|
+
rescue SystemCallError, IOError => e
|
192
|
+
raise(Error, "Unable to write #{filename} with #{e}")
|
193
|
+
end
|
172
194
|
|
173
|
-
|
174
|
-
|
195
|
+
# Atomically write to an existing file, overwriting it, or create the file
|
196
|
+
# if it does not exist.
|
197
|
+
#
|
198
|
+
# @note Either option :mode or :perm can be used to specific the permissions
|
199
|
+
# on the file being written to. This aliasing is used because both these
|
200
|
+
# names are used in the standard library, File.open uses :perm and FileUtils
|
201
|
+
# uses :mode. The user can choose whichever alias makes their code most
|
202
|
+
# readable.
|
203
|
+
#
|
204
|
+
# @param filename [String]
|
205
|
+
# @param data [#to_s]
|
206
|
+
# @param options [Hash]
|
207
|
+
# @option options [Integer] :timeout (10)
|
208
|
+
# @option options [Boolean] :flush (false)
|
209
|
+
# @option options [String, Integer] :owner
|
210
|
+
# @option options [String, Integer] :group
|
211
|
+
# @option options [Integer] :mode (0o644)
|
212
|
+
# @option options [Integer] :perm (0o644)
|
213
|
+
#
|
214
|
+
# @raise [SugarUtils::File::Error]
|
215
|
+
#
|
216
|
+
# @return [void]
|
217
|
+
def self.atomic_write(filename, data, options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
218
|
+
write_options = WriteOptions.new(filename, options)
|
219
|
+
|
220
|
+
# @note This method is similar to the atomic_write which is implemented in
|
221
|
+
# ActiveSupport. We re-implemented the method because of the following:
|
222
|
+
# * we needed the method, but wanted to avoid pulling in the entire
|
223
|
+
# ActiveSupport gem.
|
224
|
+
# * we wnated to keep the behaviour and interface consistent with the other
|
225
|
+
# SugarUtils write methods
|
226
|
+
#
|
227
|
+
# @see https://apidock.com/rails/File/atomic_write/class
|
228
|
+
FileUtils.mkdir_p(::File.dirname(filename))
|
229
|
+
Tempfile.open(::File.basename(filename, '.*'), ::File.dirname(filename)) do |temp_file|
|
230
|
+
temp_file.puts(data.to_s)
|
231
|
+
# Flush and fsync to be 100% sure we write this data out now because we
|
232
|
+
# are often reading it immediately and if the OS is buffering, it is
|
233
|
+
# possible we might read it before it is been physically written to
|
234
|
+
# disk. We are not worried about speed here, so this should be OKAY.
|
235
|
+
if write_options.flush?
|
236
|
+
temp_file.flush
|
237
|
+
temp_file.fsync
|
238
|
+
end
|
239
|
+
temp_file.close
|
240
|
+
|
241
|
+
::File.open(filename, 'w+', write_options.perm) do |file|
|
242
|
+
flock_exclusive(file, options)
|
243
|
+
FileUtils.move(temp_file.path, filename)
|
244
|
+
end
|
175
245
|
end
|
176
|
-
|
246
|
+
|
247
|
+
change_access(
|
248
|
+
filename,
|
249
|
+
write_options.owner,
|
250
|
+
write_options.group,
|
251
|
+
write_options.perm
|
252
|
+
)
|
177
253
|
rescue Timeout::Error
|
178
254
|
raise(Error, "Unable to write #{filename} because it is locked")
|
179
|
-
rescue SystemCallError, IOError =>
|
180
|
-
raise(Error, "Unable to write #{filename} with #{
|
255
|
+
rescue SystemCallError, IOError => e
|
256
|
+
raise(Error, "Unable to write #{filename} with #{e}")
|
181
257
|
end
|
182
258
|
|
183
|
-
#
|
184
|
-
#
|
185
|
-
# @
|
259
|
+
# Write the data parameter as JSON to the filename path.
|
260
|
+
#
|
261
|
+
# @note Either option :mode or :perm can be used to specific the permissions
|
262
|
+
# on the file being written to. This aliasing is used because both these
|
263
|
+
# names are used in the standard library, File.open uses :perm and FileUtils
|
264
|
+
# uses :mode. The user can choose whichever alias makes their code most
|
265
|
+
# readable.
|
266
|
+
#
|
267
|
+
# @param filename [String]
|
268
|
+
# @param data [#to_json]
|
269
|
+
# @param options [Hash]
|
186
270
|
# @option options [Integer] :timeout (10)
|
187
271
|
# @option options [Boolean] :flush (false)
|
188
|
-
# @option options [Integer] :
|
272
|
+
# @option options [String, Integer] :owner
|
273
|
+
# @option options [String, Integer] :group
|
274
|
+
# @option options [Integer] :mode (0o644)
|
275
|
+
# @option options [Integer] :perm (0o644)
|
189
276
|
#
|
190
277
|
# @raise [SugarUtils::File::Error]
|
191
278
|
#
|
192
279
|
# @return [void]
|
193
280
|
def self.write_json(filename, data, options = {})
|
194
|
-
|
281
|
+
atomic_write(filename, MultiJson.dump(data, pretty: true), options)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Append to an existing file, or create the file if it does not exist.
|
285
|
+
#
|
286
|
+
# @note Either option :mode or :perm can be used to specific the permissions
|
287
|
+
# on the file being written to. This aliasing is used because both these
|
288
|
+
# names are used in the standard library, File.open uses :perm and FileUtils
|
289
|
+
# uses :mode. The user can choose whichever alias makes their code most
|
290
|
+
# readable.
|
291
|
+
#
|
292
|
+
# @param filename [String]
|
293
|
+
# @param data [#to_s]
|
294
|
+
# @param options [Hash]
|
295
|
+
# @option options [Integer] :timeout (10)
|
296
|
+
# @option options [Boolean] :flush (false)
|
297
|
+
# @option options [String, Integer] :owner
|
298
|
+
# @option options [String, Integer] :group
|
299
|
+
# @option options [Integer] :mode (0o644)
|
300
|
+
# @option options [Integer] :perm (0o644)
|
301
|
+
#
|
302
|
+
# @raise [SugarUtils::File::Error]
|
303
|
+
#
|
304
|
+
# @return [void]
|
305
|
+
def self.append(filename, data, options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
306
|
+
write_options = WriteOptions.new(filename, options)
|
307
|
+
|
308
|
+
FileUtils.mkdir_p(::File.dirname(filename))
|
309
|
+
::File.open(filename, 'a', write_options.perm) do |file|
|
310
|
+
flock_exclusive(file, options)
|
311
|
+
|
312
|
+
file.puts(data.to_s)
|
313
|
+
|
314
|
+
# Flush and fsync to be 100% sure we write this data out now because we
|
315
|
+
# are often reading it immediately and if the OS is buffering, it is
|
316
|
+
# possible we might read it before it is been physically written to
|
317
|
+
# disk. We are not worried about speed here, so this should be OKAY.
|
318
|
+
if write_options.flush?
|
319
|
+
file.flush
|
320
|
+
file.fsync
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
change_access(
|
325
|
+
filename,
|
326
|
+
write_options.owner,
|
327
|
+
write_options.group,
|
328
|
+
write_options.perm
|
329
|
+
)
|
330
|
+
rescue Timeout::Error
|
331
|
+
raise(Error, "Unable to write #{filename} because it is locked")
|
332
|
+
rescue SystemCallError, IOError => e
|
333
|
+
raise(Error, "Unable to write #{filename} with #{e}")
|
195
334
|
end
|
196
335
|
|
197
336
|
############################################################################
|
@@ -199,7 +338,7 @@ module SugarUtils
|
|
199
338
|
# Following the same pattern as the existing stdlib method deprecation
|
200
339
|
# module.
|
201
340
|
# @see http://ruby-doc.org/stdlib-2.0.0/libdoc/rubygems/rdoc/Gem/Deprecate.html
|
202
|
-
def self.deprecate_option(_method, option_name, option_repl, year, month) # rubocop:disable MethodLength
|
341
|
+
def self.deprecate_option(_method, option_name, option_repl, year, month) # rubocop:disable Metrics/MethodLength
|
203
342
|
return if Gem::Deprecate.skip
|
204
343
|
|
205
344
|
klass = is_a?(Module)
|
@@ -216,13 +355,17 @@ module SugarUtils
|
|
216
355
|
"NOTE: #{target}#{method} option :#{option_name} is deprecated",
|
217
356
|
case option_repl
|
218
357
|
when :none
|
219
|
-
' with no replacement'
|
358
|
+
' with no replacement'
|
220
359
|
when String
|
221
360
|
option_repl
|
222
361
|
else
|
223
362
|
"; use :#{option_repl} instead"
|
224
363
|
end,
|
225
|
-
format(
|
364
|
+
format(
|
365
|
+
'. It will be removed on or after %<year>4d-%<month>02d-01.',
|
366
|
+
year: year,
|
367
|
+
month: month
|
368
|
+
),
|
226
369
|
"\n#{target}#{method} called from #{location_of_external_caller}"
|
227
370
|
]
|
228
371
|
warn("#{msg.join}.")
|