Class: CookbookOmnifetch::StagingArea

Inherits:
Object
  • Object
show all
Defined in:
lib/cookbook-omnifetch/staging_area.rb

Overview

A staging area in which the caller can stage files and publish them to a local directory.

When performing long operations such as installing or updating a cookbook from the web, StagingArea allows you to minimize the risk that a process running in parallel might retrieve an incomplete cookbook from the local cache before it is completely installed. (See #publish! for details.)

StagingArea allocates temporary directories on the local file system. It is the caller’s responsibility to use #discard! when it is done to remove those directories. The StagingArea.stage method handles directory cleanup for the staging area it creates before returning.

Examples:

installing files using the StagingArea.stage helper

CookbookOmnifetch::StagingArea.stage(install_path) do |staging_path|
  # Copy files to staging_path
end

creating a staging area and publishing it manually

stage = CookbookOmnifetch::StagingArea.new
# Copy files to stage.path
stage.publish!(install_path)
stage.discard!

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.stage(target_path) {|staging_path| ... } ⇒ Object

Creates a staging area, calls a block to populate it, then publishes it.

stage creates a staging area and calls the provided block to populate it with files. If the staging area does not contain any changes for target_path (see #match?), it cleans up the staging area without modifying target_path. Otherwise, it publishes its contents to target_path and deletes the staging area. As a safety measure, stage will not publish an empty staging area.

Parameters:

  • target_path (Pathname)

    directory to which the staging area will publish its contents

Yield Parameters:

  • staging_path (Pathname)

    the directory in which the block should stage its files



42
43
44
45
46
47
48
49
50
# File 'lib/cookbook-omnifetch/staging_area.rb', line 42

def self.stage(target_path)
  sa = new
  begin
    yield(sa.path)
    sa.publish!(target_path) unless sa.empty? || sa.match?(target_path)
  ensure
    sa.discard!
  end
end

Instance Method Details

#discard!Object

Removes the staging area and its contents from the file system.

The staging area is no longer available once #discard! removes it from the file system. Future attempts to use it will raise CookbookOmnifetch::StagingAreaNotAvailable.



133
134
135
136
# File 'lib/cookbook-omnifetch/staging_area.rb', line 133

def discard!
  FileUtils.rm_rf(@stage_tmp) unless @stage_tmp.nil?
  @unavailable = true
end

#empty?Boolean

Returns true if the staging area is empty.

A staging area is considered empty when it has no files or directories in its path or the staging directory does not exist.

Returns:

  • (Boolean)

    whether the staging area is empty

Raises:



71
72
73
# File 'lib/cookbook-omnifetch/staging_area.rb', line 71

def empty?
  !path.exist? || path.empty?
end

#match?(compare_path) ⇒ Boolean

Returns true if the staging area’s contents match those of a given path.

#match? compares the contents of the staging area with the contents of the compare_path. It considers the staging area to match if it contains all of and nothing more than the files and directories present in compare_path and the content of each file is the same as that of its corresponding file in compare_path. #match? does not compare file metadata or the contents of special files.

Parameters:

  • compare_path (String)

    the directory to which the staging area will compare its contents

Returns:

  • (Boolean)

    whether the staging area matches compare_path

Raises:



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/cookbook-omnifetch/staging_area.rb', line 91

def match?(compare_path)
  raise StagingAreaNotAvailable if unavailable?

  target = Pathname(compare_path)
  return false unless target.exist?

  files = Dir.glob("**/*", File::FNM_DOTMATCH, base: path)
  target_files = Dir.glob("**/*", File::FNM_DOTMATCH, base: target)
  return false unless files.sort == target_files.sort

  files.each do |subpath|
    return false if files_different?(path, target, subpath)
  end

  true
end

#pathPathname

Path to the staging folder on the file system.

Returns:

  • (Pathname)

    path to the staging folder

Raises:



114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/cookbook-omnifetch/staging_area.rb', line 114

def path
  raise StagingAreaNotAvailable if unavailable?

  return @path unless @path.nil?

  # Dir.mktmpdir returns a directory with restrictive permissions that it
  # doesn't support modifying, so create a subdirectory under it with
  # regular permissions for staging.
  @stage_tmp = Dir.mktmpdir
  @path = Pathname.new(File.join(@stage_tmp, "staging"))
  FileUtils.mkdir(@path)
  @path
end

#publish!(install_path) ⇒ Object

Replaces install_path with the contents of the staging area.

#publish! removes the target and copies the new content into place using two atomic file system operations. This eliminates much of the risk associated with updating the target in a multiprocess environment by ensuring that another process does not see a partially removed or populated directory at the target_path while this operation is being performed.

Note that it is still possible for the #publish! to interrupt another process performing a long operation, such as creating a recursive copy of the target. In this situation, the other process may create a copy that consists of a combination of content from the old target directory and the newly staged files. The other process may also raise an exception should it try to access the target during a small window in the #publish! operation where the target directory does not exist, or tries to open a file that is no longer part of the target tree after #publish! completes. The other process can detect this situation by verifying that the content of its copy matches the content of target_path after its copy is complete.

Parameters:

  • install_path (String)

    directory to which the staging area will publish its contents

Raises:



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/cookbook-omnifetch/staging_area.rb', line 164

def publish!(install_path)
  target = Pathname(install_path)
  cache_dir = target.parent
  cache_dir.mkpath
  Dir.mktmpdir("_STAGING_TMP_", cache_dir) do |tmpdir|
    newtmp = File.join(tmpdir, "new_cookbook")
    oldtmp = File.join(tmpdir, "old_cookbook")
    FileUtils.cp_r(path, newtmp)

    # We could achieve an atomic replace using symbolic links, if they are
    # supported on all platforms.
    File.rename(target, oldtmp) if target.exist?
    File.rename(newtmp, target)
  end
end

#unavailable?Boolean

Returns true if the staging area is no longer available for use.

The staging area is no longer available once #discard! removes it from the file system.

Returns:

  • (Boolean)

    whether the staging area is unavailable



58
59
60
# File 'lib/cookbook-omnifetch/staging_area.rb', line 58

def unavailable?
  !!@unavailable
end