Ctrl K
ComunidadGuías…

app/services/backup_repository/sidecar.rb

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
require 'open3'

class BackupRepository
  # Sidecar IO helpers shared by the External and S3 adapters. Both reach
  # their destination only through a short-lived docker sidecar; this
  # module holds the run/capture/stream wrappers that are otherwise
  # identical.
  #
  # Including adapters must provide: `sidecar_command(*cmd)`.
  module Sidecar
    include Loggable

    def sidecar_run(*cmd)
      Open3.capture2e(*sidecar_command(*cmd))
    end

    # Like sidecar_run, but raises BackupRepository::Error on a non-zero
    # exit — mutating operations must not update the DB after the actual
    # rm/cp silently failed.
    def sidecar_run!(*cmd)
      output, status = sidecar_run(*cmd)
      return output if status.success?

      message = output.to_s.strip.presence || "exited with status #{status.exitstatus}"
      logger.error("sidecar failed: #{cmd.inspect} — #{message}")
      raise BackupRepository::Error, message
    end

    def sidecar_capture(*cmd)
      Open3.capture2(*sidecar_command(*cmd))
    end

    # Runs a sidecar whose stdout is a tar stream and parses it into an
    # ArchiveContents. Uses `popen2`, not `popen2e`, on purpose: the stdout
    # is a binary tar — merging stderr into it would corrupt the archive.
    # With `popen2` the sidecar's stderr passes straight through to HELIOS's
    # own stderr/log instead.
    #
    # Every failure mode is logged and yields an empty archive: a non-zero
    # exit, an unreadable tar, a sidecar that could not launch, and — the
    # subtle one — a clean exit that nonetheless produced no entries (an S3
    # object not yet readable in the moments right after upload).
    def stream_sidecar_archive(*cmd)
      Open3.popen2(*sidecar_command(*cmd)) do |_in, out, wait|
        out.binmode
        archive = BackupRepository.parse_tar_stream(BackupRepository::PipeIo.new(out))
        # Drain anything left over so the wait status reflects a clean exit
        # — TarReader stops as soon as it sees the zero-block sentinel.
        out.read
        verified_archive(archive, wait.value, cmd)
      end
    rescue Gem::Package::TarInvalidError => e
      warn_archive("produced an unreadable tar (#{e.message})", cmd)
    rescue Errno::ENOENT => e
      warn_archive("could not be launched (#{e.message})", cmd)
    end

    # Runs a sidecar whose stdout is a tar stream and yields it to the block
    # in 64 KB chunks (HTTP download). `popen2` keeps stderr off the stdout
    # pipe so a docker warning can never end up inside the download;
    # a non-zero exit raises BackupRepository::Error.
    def stream_sidecar_download(*cmd)
      Open3.popen2(*sidecar_command(*cmd)) do |_in, out, wait|
        out.binmode
        while (chunk = out.read(64 * 1024))
          yield chunk
        end
        status = wait.value
        next if status.success?

        raise BackupRepository::Error, "sidecar exited with status #{status.exitstatus}"
      end
    end

    private

    def verified_archive(archive, status, cmd)
      return warn_archive("exited #{status.exitstatus}", cmd) unless status.success?

      warn_archive('produced no tar entries', cmd) if archive.entries.empty?
      archive
    end

    def warn_archive(reason, cmd)
      logger.warn("archive sidecar #{reason} — #{cmd.inspect}")
      BackupRepository::EMPTY_ARCHIVE
    end
  end
end