def add(path, name, version, **opts)
      providers = opts[:providers]
      providers = Array(providers) if providers
      provider = nil

      # A helper to check if a box exists. We store this in a variable
      # since we call it multiple times.
      check_box_exists = lambda do |box_formats|
        box = find(name, box_formats, version)
        next if !box

        if !opts[:force]
          @logger.error(
            "Box already exists, can't add: #{name} v#{version} #{box_formats.join(", ")}")
          raise Errors::BoxAlreadyExists,
            name: name,
            provider: box_formats.join(", "),
            version: version
        end

        # We're forcing, so just delete the old box
        @logger.info(
          "Box already exists, but forcing so removing: " +
          "#{name} v#{version} #{box_formats.join(", ")}")
        box.destroy!
      end

      with_collection_lock do
        log_provider = providers ? providers.join(", ") : "any provider"
        @logger.debug("Adding box: #{name} (#{log_provider}) from #{path}")

        # Verify the box doesn't exist early if we're given a provider. This
        # can potentially speed things up considerably since we don't need
        # to unpack any files.
        check_box_exists.call(providers) if providers

        # Create a temporary directory since we're not sure at this point if
        # the box we're unpackaging already exists (if no provider was given)
        with_temp_dir do |temp_dir|
          # Extract the box into a temporary directory.
          @logger.debug("Unpacking box into temporary directory: #{temp_dir}")
          result = Util::Subprocess.execute(
            "bsdtar", "--no-same-owner", "--no-same-permissions", "-v", "-x", "-m", "-s", "|\\\\\|/|", "-C", temp_dir.to_s, "-f", path.to_s)
          if result.exit_code != 0
            raise Errors::BoxUnpackageFailure,
              output: result.stderr.to_s
          end

          # If we get a V1 box, we want to update it in place
          if v1_box?(temp_dir)
            @logger.debug("Added box is a V1 box. Upgrading in place.")
            temp_dir = v1_upgrade(temp_dir)
          end

          # We re-wrap ourselves in the safety net in case we upgraded.
          # If we didn't upgrade, then this is still safe because the
          # helper will only delete the directory if it exists
          with_temp_dir(temp_dir) do |final_temp_dir|
            # Get an instance of the box we just added before it is finalized
            # in the system so we can inspect and use its metadata.
            box = Box.new(name, nil, version, final_temp_dir)

            # Get the provider, since we'll need that to at the least add it
            # to the system or check that it matches what is given to us.
            box_provider = box.metadata["provider"]

            if providers
              found = providers.find { |p| p.to_sym == box_provider.to_sym }
              if !found
                @logger.error("Added box provider doesnt match expected: #{log_provider}")
                raise Errors::BoxProviderDoesntMatch,
                  expected: log_provider, actual: box_provider
              end
            else
              # Verify the box doesn't already exist
              check_box_exists.call([box_provider])
            end

            # We weren't given a provider, so store this one.
            provider = box_provider.to_sym

            # Create the directory for this box, not including the provider
            root_box_dir = @directory.join(dir_name(name))
            box_dir = root_box_dir.join(version)
            box_dir.mkpath
            @logger.debug("Box directory: #{box_dir}")

            # This is the final directory we'll move it to
            final_dir = box_dir.join(provider.to_s)
            if final_dir.exist?
              @logger.debug("Removing existing provider directory...")
              final_dir.rmtree
            end

            # Move to final destination
            final_dir.mkpath

            # Recursively move individual files from the temporary directory
            # to the final location. We do this instead of moving the entire
            # directory to avoid issues on Windows. [GH-1424]
            copy_pairs = [[final_temp_dir, final_dir]]
            while !copy_pairs.empty?
              from, to = copy_pairs.shift
              from.children(true).each do |f|
                dest = to.join(f.basename)

                # We don't copy entire directories, so create the
                # directory and then add to our list to copy.
                if f.directory?
                  dest.mkpath
                  copy_pairs << [f, dest]
                  next
                end

                # Copy the single file
                @logger.debug("Moving: #{f} => #{dest}")
                FileUtils.mv(f, dest)
              end
            end

            if opts[:metadata_url]
              root_box_dir.join("metadata_url").open("w") do |f|
                f.write(opts[:metadata_url])
              end
            end
          end
        end
      end

      # Return the box
      find(name, provider, version)
    end
def add(path, name, version, **opts)
      providers = opts[:providers]
      providers = Array(providers) if providers
      provider = nil

      # A helper to check if a box exists. We store this in a variable
      # since we call it multiple times.
      check_box_exists = lambda do |box_formats|
        box = find(name, box_formats, version)
        next if !box

        if !opts[:force]
          @logger.error(
            "Box already exists, can't add: #{name} v#{version} #{box_formats.join(", ")}")
          raise Errors::BoxAlreadyExists,
            name: name,
            provider: box_formats.join(", "),
            version: version
        end

        # We're forcing, so just delete the old box
        @logger.info(
          "Box already exists, but forcing so removing: " +
          "#{name} v#{version} #{box_formats.join(", ")}")
        box.destroy!
      end

      with_collection_lock do
        log_provider = providers ? providers.join(", ") : "any provider"
        @logger.debug("Adding box: #{name} (#{log_provider}) from #{path}")

        # Verify the box doesn't exist early if we're given a provider. This
        # can potentially speed things up considerably since we don't need
        # to unpack any files.
        check_box_exists.call(providers) if providers

        # Create a temporary directory since we're not sure at this point if
        # the box we're unpackaging already exists (if no provider was given)
        with_temp_dir do |temp_dir|
          # Extract the box into a temporary directory.
          @logger.debug("Unpacking box into temporary directory: #{temp_dir}")
          result = Util::Subprocess.execute(
            "bsdtar", "--no-same-owner", "--no-same-permissions", "-v", "-x", "-m", "-s", "|\\\\\|/|", "-C", temp_dir.to_s, "-f", path.to_s)
          if result.exit_code != 0
            raise Errors::BoxUnpackageFailure,
              output: result.stderr.to_s
          end

          # If we get a V1 box, we want to update it in place
          if v1_box?(temp_dir)
            @logger.debug("Added box is a V1 box. Upgrading in place.")
            temp_dir = v1_upgrade(temp_dir)
          end

          # We re-wrap ourselves in the safety net in case we upgraded.
          # If we didn't upgrade, then this is still safe because the
          # helper will only delete the directory if it exists
          with_temp_dir(temp_dir) do |final_temp_dir|
            # Get an instance of the box we just added before it is finalized
            # in the system so we can inspect and use its metadata.
            box = Box.new(name, nil, version, final_temp_dir)

            # Get the provider, since we'll need that to at the least add it
            # to the system or check that it matches what is given to us.
            box_provider = box.metadata["provider"]

            if providers
              found = providers.find { |p| p.to_sym == box_provider.to_sym }
              if !found
                @logger.error("Added box provider doesnt match expected: #{log_provider}")
                raise Errors::BoxProviderDoesntMatch,
                  expected: log_provider, actual: box_provider
              end
            else
              # Verify the box doesn't already exist
              check_box_exists.call([box_provider])
            end

            # We weren't given a provider, so store this one.
            provider = box_provider.to_sym

            # Create the directory for this box, not including the provider
            root_box_dir = @directory.join(dir_name(name))
            box_dir = root_box_dir.join(version)
            box_dir.mkpath
            @logger.debug("Box directory: #{box_dir}")

            # This is the final directory we'll move it to
            final_dir = box_dir.join(provider.to_s)
            if final_dir.exist?
              @logger.debug("Removing existing provider directory...")
              final_dir.rmtree
            end

            # Move to final destination
            final_dir.mkpath

            # Recursively move individual files from the temporary directory
            # to the final location. We do this instead of moving the entire
            # directory to avoid issues on Windows. [GH-1424]
            copy_pairs = [[final_temp_dir, final_dir]]
            while !copy_pairs.empty?
              from, to = copy_pairs.shift
              from.children(true).each do |f|
                dest = to.join(f.basename)

                # We don't copy entire directories, so create the
                # directory and then add to our list to copy.
                if f.directory?
                  dest.mkpath
                  copy_pairs << [f, dest]
                  next
                end

                # Copy the single file
                @logger.debug("Moving: #{f} => #{dest}")
                FileUtils.mv(f, dest)
              end
            end

            if opts[:metadata_url]
              root_box_dir.join("metadata_url").open("w") do |f|
                f.write(opts[:metadata_url])
              end
            end
          end
        end
      end

      # Return the box
      find(name, provider, version)
    end
Initializes the collection.

 @param [Pathname] directory The directory that contains the collection
   of boxes.
 This adds a new box to the system.

 There are some exceptional cases:
 * BoxAlreadyExists - The box you're attempting to add already exists.
 * BoxProviderDoesntMatch - If the given box provider doesn't match the
   actual box provider in the untarred box.
 * BoxUnpackageFailure - An invalid tar file.

 Preconditions:
 * File given in `path` must exist.

 @param [Pathname] path Path to the box file on disk.
 @param [String] name Logical name for the box.
 @param [String] version The version of this box.
 @param [Array<String>] providers The providers that this box can
   be a part of. This will be verified with the `metadata.json` and is
   meant as a basic check. If this isn't given, then whatever provider
   the box represents will be added.
 @param [Boolean] force If true, any existing box with the same name
   and provider will be replaced.