javascript 89 lines · 2 tabs

Stream a Multipart Upload Through Gzip to Disk with stream.pipeline

Shared by codesnips Aug 2026
2 tabs
'use strict';

const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const crypto = require('crypto');
const { pipeline } = require('stream');
const { promisify } = require('util');

const pipelineAsync = promisify(pipeline);

function safeName(original) {
  const base = path.basename(original || 'upload').replace(/[^a-zA-Z0-9._-]/g, '_');
  const token = crypto.randomBytes(6).toString('hex');
  return `${token}-${base}.gz`;
}

async function storeCompressed(source, { dir, filename }) {
  const name = safeName(filename);
  const finalPath = path.join(dir, name);
  const tmpPath = `${finalPath}.part`;

  const gzip = zlib.createGzip({ level: zlib.constants.Z_BEST_SPEED });
  const out = fs.createWriteStream(tmpPath);

  try {
    await pipelineAsync(source, gzip, out);
    await fs.promises.rename(tmpPath, finalPath);
  } catch (err) {
    await fs.promises.unlink(tmpPath).catch(() => {});
    throw err;
  }

  const { size } = await fs.promises.stat(finalPath);
  return { name, path: finalPath, compressedBytes: size };
}

module.exports = { storeCompressed };
2 files · javascript Explain with highlit

This snippet shows the correct way to accept a large file upload and write it to disk as a gzip-compressed artifact without ever buffering the whole payload in memory. The core idea is a stream pipeline: the incoming request body is a Readable, gzip is a Transform, and the destination file is a Writable, and stream.pipeline wires them together while propagating backpressure and errors across every stage.

In uploadStore.js, the storeCompressed helper wraps pipeline in its promisified form via util.promisify. Using pipeline rather than a chain of .pipe() calls matters because .pipe() does not forward errors or destroy upstream sources when a downstream stage fails, which classically leaks file descriptors and leaves half-written files. pipeline guarantees that if any stage errors — the socket aborts, the disk fills, gzip throws — every stream in the chain is destroyed. The helper writes to a temporary .part path first and only renames into place on success, so consumers never observe a truncated file; on failure it unlinks the partial and rethrows. The rename is atomic on the same filesystem, which is what makes this crash-safe.

uploadStore.js also derives a safe on-disk name and returns the compressed byte count read from fs.stat, giving the caller something to persist.

In uploadRoutes.js, busboy parses the multipart stream and emits a file event whose value is itself a Readable. That readable is handed straight to storeCompressed, so bytes flow request → busboy → gzip → disk with backpressure intact and nothing accumulating in RAM. A crucial detail is draining unused file streams with file.resume() when the field name is unexpected; an unconsumed busboy stream stalls the whole parse. The route awaits the returned promise, responds with the stored metadata, and lets a rejected pipeline propagate to Express error handling.

The trade-off is that streaming precludes reading the full body before deciding to reject it, so size limits are enforced incrementally via busboy limits rather than after the fact. This pattern is the right reach whenever uploads may be large, memory is constrained, or partial writes must never survive a crash.


Related snips

Share this code

Here's the card — post it anywhere.

Stream a Multipart Upload Through Gzip to Disk with stream.pipeline — share card
Link copied