cookbook-omnifetch 0.11.1 → 0.12.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6b7baff1c908f3ee02466f86ff7225a6a567006ed325313514d429eb07bddaa
4
- data.tar.gz: a30c239d1ff4f032738cf9755226aff95e6f7df330c5119b367fa520306a9372
3
+ metadata.gz: a84aa2f4551b271e3c13a1260d6070ed4a637ca777ce81343dc3087bf73d9122
4
+ data.tar.gz: b93c4da8a8ab2a64f7c62e4b9a677ca0276f8a7a9ae6c82f73e21a644f03e07b
5
5
  SHA512:
6
- metadata.gz: c7f11e46181d038e79d55e527ebc743b977ed012ad0630dbfe8f687bdc36c9638a0a7e70eb24feacc1fc2ef960cdd68f4cdbff55eea8d5bdd98c5f29f2b25962
7
- data.tar.gz: 5ad4d570c3fe10fa6f30c055d8bb0d73dd24fc1a90c15fe804007bda3b9738780d0e87128ecfae2cb569e5bdba949ad3d2f3e48999e58c27b99dd970f4cae297
6
+ metadata.gz: e5043f1653e630012f06582d1917ddb72a7904e0a6b283ad5db6f2020a7b26859280bcf50e8ee19298dd00798d1738f644aa5348aafc5571c003addc3fe69fbb
7
+ data.tar.gz: 7568ed165159ef9800da6211ad8d3bb53ffedde3cd6923a37dd7506dd425f53f0994f49c05cdff8858fbb63ceacf271b4e4f7c54add4ebeb646e6597dc4f1daa
@@ -34,8 +34,7 @@ module CookbookOmnifetch
34
34
  #
35
35
  # @return [Boolean]
36
36
  def installed?
37
- # Always force a refresh of cache
38
- false
37
+ install_path.exist?
39
38
  end
40
39
 
41
40
  def http_client
@@ -45,8 +45,7 @@ module CookbookOmnifetch
45
45
  #
46
46
  # @return [Boolean]
47
47
  def installed?
48
- # Always force a refresh of cache
49
- false
48
+ install_path.exist?
50
49
  end
51
50
 
52
51
  def http_client
@@ -106,4 +106,9 @@ module CookbookOmnifetch
106
106
  end
107
107
  end
108
108
 
109
+ class StagingAreaNotAvailable < OmnifetchError
110
+ def initialize
111
+ super "failed to access a StagingArea that is no longer available"
112
+ end
113
+ end
109
114
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "threaded_job_queue"
2
- require "digest/md5"
2
+ require_relative "staging_area"
3
+ require "digest/md5" unless defined?(Digest::MD5)
3
4
 
4
5
  module CookbookOmnifetch
5
6
 
@@ -47,17 +48,20 @@ module CookbookOmnifetch
47
48
  end
48
49
 
49
50
  def install
50
- metadata = http_client.get(url_path)
51
- clean_cache(metadata)
52
- sync_cache(metadata)
51
+ StagingArea.stage(install_path) do |staging_path|
52
+ FileUtils.cp_r("#{install_path}/.", staging_path) if Dir.exist?(install_path)
53
+ metadata = http_client.get(url_path)
54
+ clean_cache(staging_path, metadata)
55
+ sync_cache(staging_path, metadata)
56
+ end
53
57
  end
54
58
 
55
59
  # Removes files from cache that are not supposed to be there, based on
56
60
  # files in metadata.
57
- def clean_cache(metadata)
58
- actual_file_list = Dir.glob(File.join(install_path, "**/*"))
61
+ def clean_cache(staging_path, metadata)
62
+ actual_file_list = Dir.glob(File.join(staging_path, "**/*"))
59
63
  expected_file_list = []
60
- CookbookMetadata.new(metadata).files { |_, path, _| expected_file_list << File.join(install_path, path) }
64
+ CookbookMetadata.new(metadata).files { |_, path, _| expected_file_list << File.join(staging_path, path) }
61
65
 
62
66
  extra_files = actual_file_list - expected_file_list
63
67
  extra_files.each do |path|
@@ -69,10 +73,10 @@ module CookbookOmnifetch
69
73
 
70
74
  # Downloads any out-of-date files into installer cache, overwriting
71
75
  # those that don't match the checksum provided the metadata @ url_path
72
- def sync_cache(metadata)
76
+ def sync_cache(staging_path, metadata)
73
77
  queue = ThreadedJobQueue.new
74
78
  CookbookMetadata.new(metadata).files do |url, path, checksum|
75
- dest_path = File.join(install_path, path)
79
+ dest_path = File.join(staging_path, path)
76
80
  FileUtils.mkdir_p(File.dirname(dest_path))
77
81
  if file_outdated?(dest_path, checksum)
78
82
  queue << lambda do |_lock|
@@ -0,0 +1,192 @@
1
+ require_relative "exceptions"
2
+
3
+ module CookbookOmnifetch
4
+ # A staging area in which the caller can stage files and publish them to a
5
+ # local directory.
6
+ #
7
+ # When performing long operations such as installing or updating a cookbook
8
+ # from the web, {StagingArea} allows you to minimize the risk that a process
9
+ # running in parallel might retrieve an incomplete cookbook from the local
10
+ # cache before it is completely installed. (See {publish!} for details.)
11
+ #
12
+ # {StagingArea} allocates temporary directories on the local file system. It
13
+ # is the caller's responsibility to use {discard!} when it is done to remove
14
+ # those directories. The {.stage} method handles directory cleanup for the
15
+ # staging area it creates before returning.
16
+ #
17
+ # @example installing files using the {.stage} helper
18
+ # CookbookOmnifetch::StagingArea.stage(install_path) do |staging_path|
19
+ # # Copy files to staging_path
20
+ # end
21
+ #
22
+ # @example creating a staging area and publishing it manually
23
+ # stage = CookbookOmnifetch::StagingArea.new
24
+ # # Copy files to stage.path
25
+ # stage.publish!(install_path)
26
+ # stage.discard!
27
+ class StagingArea
28
+ # Creates a staging area, calls a block to populate it, then publishes it.
29
+ #
30
+ # {stage} creates a staging area and calls the provided block to populate it
31
+ # with files. If the staging area does not contain any changes for
32
+ # +target_path+ (see {#match?}), it cleans up the staging area without
33
+ # modifying +target_path+. Otherwise, it publishes its contents to
34
+ # +target_path+ and deletes the staging area. As a safety measure, {stage}
35
+ # will not publish an empty staging area.
36
+ #
37
+ # @param [Pathname] target_path
38
+ # directory to which the staging area will publish its contents
39
+ #
40
+ # @yieldparam staging_path [Pathname]
41
+ # the directory in which the block should stage its files
42
+ def self.stage(target_path)
43
+ sa = new
44
+ begin
45
+ yield(sa.path)
46
+ sa.publish!(target_path) unless sa.empty? || sa.match?(target_path)
47
+ ensure
48
+ sa.discard!
49
+ end
50
+ end
51
+
52
+ # Returns true if the staging area is no longer available for use.
53
+ #
54
+ # The staging area is no longer available once {discard!} removes it from
55
+ # the file system.
56
+ #
57
+ # @return [Boolean] whether the staging area is unavailable
58
+ def unavailable?
59
+ !!@unavailable
60
+ end
61
+
62
+ # Returns true if the staging area is empty.
63
+ #
64
+ # A staging area is considered empty when it has no files or directories in
65
+ # its path or the staging directory does not exist.
66
+ #
67
+ # @raise [StagingAreaNotAvailable]
68
+ # when called after the staging area destroyed with {discard!}
69
+ #
70
+ # @return [Boolean] whether the staging area is empty
71
+ def empty?
72
+ !path.exist? || path.empty?
73
+ end
74
+
75
+ # Returns true if the staging area's contents match those of a given path.
76
+ #
77
+ # {#match?} compares the contents of the staging area with the contents of
78
+ # the +compare_path+. It considers the staging area to match if it contains
79
+ # all of and nothing more than the files and directories present in
80
+ # +compare_path+ and the content of each file is the same as that of its
81
+ # corresponding file in +compare_path+. {match?} does not compare file
82
+ # metadata or the contents of special files.
83
+ #
84
+ # @param [String] compare_path
85
+ # the directory to which the staging area will compare its contents
86
+ #
87
+ # @raise [StagingAreaNotAvailable]
88
+ # when called after the staging area destroyed with {discard!}
89
+ #
90
+ # @return [Boolean] whether the staging area matches +compare_path+
91
+ def match?(compare_path)
92
+ raise StagingAreaNotAvailable if unavailable?
93
+
94
+ target = Pathname(compare_path)
95
+ return false unless target.exist?
96
+
97
+ files = Dir.glob("**/*", File::FNM_DOTMATCH, base: path)
98
+ target_files = Dir.glob("**/*", File::FNM_DOTMATCH, base: target)
99
+ return false unless files.sort == target_files.sort
100
+
101
+ files.each do |subpath|
102
+ return false if files_different?(path, target, subpath)
103
+ end
104
+
105
+ true
106
+ end
107
+
108
+ # Path to the staging folder on the file system.
109
+ #
110
+ # @raise [StagingAreaNotAvailable]
111
+ # when called after the staging area destroyed with {discard!}
112
+ #
113
+ # @return [Pathname] path to the staging folder
114
+ def path
115
+ raise StagingAreaNotAvailable if unavailable?
116
+
117
+ return @path unless @path.nil?
118
+
119
+ # Dir.mktmpdir returns a directory with restrictive permissions that it
120
+ # doesn't support modifying, so create a subdirectory under it with
121
+ # regular permissions for staging.
122
+ @stage_tmp = Dir.mktmpdir
123
+ @path = Pathname.new(File.join(@stage_tmp, "staging"))
124
+ FileUtils.mkdir(@path)
125
+ @path
126
+ end
127
+
128
+ # Removes the staging area and its contents from the file system.
129
+ #
130
+ # The staging area is no longer available once {discard!} removes it from
131
+ # the file system. Future attempts to use it will raise
132
+ # {StagingAreaNotAvailable}.
133
+ def discard!
134
+ FileUtils.rm_rf(@stage_tmp) unless @stage_tmp.nil?
135
+ @unavailable = true
136
+ end
137
+
138
+ # Replaces +install_path+ with the contents of the staging area.
139
+ #
140
+ # {publish!} removes the target and copies the new content into place using
141
+ # two atomic file system operations. This eliminates much of the risk
142
+ # associated with updating the target in a multiprocess environment by
143
+ # ensuring that another process does not see a partially removed or
144
+ # populated directory at the +target_path+ while this operation is being
145
+ # performed.
146
+ #
147
+ # Note that it is still possible for the {publish!} to interrupt another
148
+ # process performing a long operation, such as creating a recursive copy of
149
+ # the target. In this situation, the other process may create a copy that
150
+ # consists of a combination of content from the old target directory and the
151
+ # newly staged files. The other process may also raise an exception should
152
+ # it try to access the target during a small window in the {publish!}
153
+ # operation where the target directory does not exist, or tries to open a
154
+ # file that is no longer part of the target tree after {publish!} completes.
155
+ # The other process can detect this situation by verifying that the content
156
+ # of its copy matches the content of +target_path+ after its copy is
157
+ # complete.
158
+ #
159
+ # @param [String] install_path
160
+ # directory to which the staging area will publish its contents
161
+ #
162
+ # @raise [StagingAreaNotAvailable]
163
+ # when called after the staging area destroyed with {discard!}
164
+ def publish!(install_path)
165
+ target = Pathname(install_path)
166
+ cache_dir = target.parent
167
+ cache_dir.mkpath
168
+ Dir.mktmpdir("_STAGING_TMP_", cache_dir) do |tmpdir|
169
+ newtmp = File.join(tmpdir, "new_cookbook")
170
+ oldtmp = File.join(tmpdir, "old_cookbook")
171
+ FileUtils.cp_r(path, newtmp)
172
+
173
+ # We could achieve an atomic replace using symbolic links, if they are
174
+ # supported on all platforms.
175
+ File.rename(target, oldtmp) if target.exist?
176
+ File.rename(newtmp, target)
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ # compares two files
183
+ def files_different?(base1, base2, subpath)
184
+ file1 = File.join(base1, subpath)
185
+ file2 = File.join(base2, subpath)
186
+ return true unless File.ftype(file1) == File.ftype(file2)
187
+ return true if File.file?(file1) && !FileUtils.cmp(file1, file2)
188
+
189
+ false
190
+ end
191
+ end
192
+ end
@@ -1,3 +1,3 @@
1
1
  module CookbookOmnifetch
2
- VERSION = "0.11.1".freeze
2
+ VERSION = "0.12.2".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cookbook-omnifetch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.12.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamie Winsor
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2020-08-31 00:00:00.000000000 Z
16
+ date: 2022-03-16 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: mixlib-archive
@@ -60,6 +60,7 @@ files:
60
60
  - lib/cookbook-omnifetch/integration.rb
61
61
  - lib/cookbook-omnifetch/metadata_based_installer.rb
62
62
  - lib/cookbook-omnifetch/path.rb
63
+ - lib/cookbook-omnifetch/staging_area.rb
63
64
  - lib/cookbook-omnifetch/threaded_job_queue.rb
64
65
  - lib/cookbook-omnifetch/version.rb
65
66
  homepage: https://github.com/chef/cookbook-omnifetch
@@ -81,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
82
  - !ruby/object:Gem::Version
82
83
  version: '0'
83
84
  requirements: []
84
- rubygems_version: 3.0.3
85
+ rubygems_version: 3.1.4
85
86
  signing_key:
86
87
  specification_version: 4
87
88
  summary: Library code to fetch Chef cookbooks from a variety of sources to a local