cookbook-omnifetch 0.11.1 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
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