Module: ReactOnRailsPro::RollingDeploy::Tarball
- Defined in:
- lib/react_on_rails_pro/rolling_deploy/tarball.rb
Overview
Pure-Ruby tar.gz compose/extract used by the built-in HTTP rolling-deploy adapter. Stdlib only (Gem::Package::TarWriter / TarReader + Zlib) so the gem stays free of native or third-party tarball dependencies.
Wire format (one tarball per bundle hash):
./bundle.js
./loadable-stats.json (optional)
./react-client-manifest.json (when RSC enabled)
./react-server-client-manifest.json (when RSC enabled)
All entries are flat — the adapter renames ‘bundle.js` to `<hash>.js` during staging. The compose side only writes regular files keyed by their basename; the extract side rejects anything that isn’t a basename so a malicious server can’t write outside ‘dest_dir`.
Constant Summary collapse
- DEFAULT_MAX_SIZE =
Default uncompressed size cap. Bundles are mostly text and gzip well, but a malicious or misconfigured server could still send a multi-GB payload; cap the uncompressed total at 200 MB unless the caller overrides. Used by ‘extract` as a guard against zip-bomb-style payloads.
200 * 1024 * 1024
- EXTRACT_CHUNK_SIZE =
Per-entry read chunk during extract. Small enough that an inner large file doesn’t blow heap before the running total trips the size cap.
64 * 1024
- ENTRY_NAME_PATTERN =
Path-safety regex for tar entries. We require entries to be flat basenames (no slashes, no ‘..`, no NUL bytes, no leading dot, no leading hyphen) so an attacker can never write outside the target directory or hide files from `ls`. The `./` prefix permitted by tar is normalised away before this match runs.
Functionally identical to ‘ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN` — kept as a separate constant because tarball entry names and rolling-deploy hash strings are conceptually distinct: a future protocol change might tighten one without the other.
/\A[A-Za-z0-9_][A-Za-z0-9_.\-]*\z/
Class Method Summary collapse
-
.compose_to_io(entries, io) ⇒ Object
Compose a gzipped tarball into the given IO.
-
.compose_to_tempfile(entries) ⇒ Object
Compose a gzipped tarball from the given entries and yield the resulting Tempfile.
-
.extract(source, dest_dir, max_size: DEFAULT_MAX_SIZE) ⇒ Object
Extract a gzipped tarball from ‘source` (an IO-like or a String) into `dest_dir`.
Class Method Details
.compose_to_io(entries, io) ⇒ Object
Compose a gzipped tarball into the given IO. The IO must accept binary writes. See ‘compose_to_tempfile` for the `entries` shape.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/react_on_rails_pro/rolling_deploy/tarball.rb', line 74 def compose_to_io(entries, io) validate_compose_entries!(entries) gz = Zlib::GzipWriter.new(io) begin Gem::Package::TarWriter.new(gz) do |tar| entries.each do |name, path| add_file_to_tar(tar, name, path) end end gz.finish rescue StandardError # On the happy path `gz.finish` flushes and closes the writer; on the # exceptional path the TarWriter's `ensure` already wrote the tar EOF # record into `gz`, so we just need to release the GzipWriter's IO # reference before re-raising. Swallow any close error so the # original exception still surfaces. begin gz.close rescue StandardError nil end raise end io end |
.compose_to_tempfile(entries) ⇒ Object
Compose a gzipped tarball from the given entries and yield the resulting Tempfile. The temp file is removed when the block returns.
‘entries` is a Hash mapping the archive entry name (a flat basename, e.g. `“bundle.js”`) to the source file path on disk. Hash form lets callers rename the file in the archive — on the server side, the server bundle lives at `<hash>.js` on disk but ships as `bundle.js` in the tarball so the client adapter can stage it without needing to know the source filename.
62 63 64 65 66 67 68 69 70 |
# File 'lib/react_on_rails_pro/rolling_deploy/tarball.rb', line 62 def compose_to_tempfile(entries) Tempfile.create(["rolling-deploy-tarball-", ".tar.gz"]) do |tempfile| tempfile.binmode compose_to_io(entries, tempfile) tempfile.flush tempfile.rewind yield tempfile end end |
.extract(source, dest_dir, max_size: DEFAULT_MAX_SIZE) ⇒ Object
Extract a gzipped tarball from ‘source` (an IO-like or a String) into `dest_dir`. Enforces:
* Each entry name must be a safe basename (ENTRY_NAME_PATTERN).
* Each entry must be a regular file (no dirs, symlinks, hardlinks).
* Cumulative uncompressed bytes must not exceed `max_size`.
Returns the list of basenames extracted, in the order seen in the archive. Raises ReactOnRailsPro::Error on any safety or size violation.
Cleanup contract: each entry is written to a Tempfile inside ‘dest_dir` and atomically renamed into place only after the size cap check passes for that entry. On raise, no partial file is left at the entry’s final path, but earlier entries in the archive may have been written successfully. Callers must ‘rm_rf` `dest_dir` when extract raises so partial archives don’t leak into the cache.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/react_on_rails_pro/rolling_deploy/tarball.rb', line 116 def extract(source, dest_dir, max_size: DEFAULT_MAX_SIZE) FileUtils.mkdir_p(dest_dir) io = source.is_a?(String) ? StringIO.new(source) : source extracted = [] total_size = 0 Zlib::GzipReader.wrap(io) do |gz| Gem::Package::TarReader.new(gz) do |tar| tar.each do |entry| name = safe_entry_name!(entry) raise_unless_regular_file!(entry, name) total_size = write_entry!(entry, dest_dir, name, total_size, max_size) extracted << name end end end extracted rescue Zlib::GzipFile::Error, Zlib::DataError => e raise ReactOnRailsPro::Error, "Rolling-deploy tarball is not a valid gzip stream: #{e.class}: #{e.}" rescue Gem::Package::Error => e # `Gem::Package::TarReader` raises subclasses of Gem::Package::Error # (e.g. TarInvalidError) on malformed tar headers inside an # otherwise-valid gzip stream. Wrap them in our error type so the HTTP # adapter's StandardError rescue produces a "tarball is malformed" # warning instead of leaking the underlying RubyGems class. raise ReactOnRailsPro::Error, "Rolling-deploy tarball has malformed tar entries: #{e.class}: #{e.}" end |