diff --git a/src/utils/compose_ts.ts b/src/utils/compose_ts.ts index 37653cf3b..4ff3b3598 100644 --- a/src/utils/compose_ts.ts +++ b/src/utils/compose_ts.ts @@ -789,6 +789,30 @@ export async function tarDirectory( return pack; } +/** + * Calculates the total size of a readable stream. + * + * @param stream A readable stream, such as the one returned by tarDirectory. + * @returns A promise that resolves to the total size of the stream in bytes. + */ +export async function getTarPackSize(stream: Readable): Promise { + return new Promise((resolve, reject) => { + let totalSize = 0; + + stream.on('data', (chunk: Buffer) => { + totalSize += chunk.length; + }); + + stream.on('end', () => { + resolve(totalSize); + }); + + stream.on('error', (err) => { + reject(err); + }); + }); +} + /** * Print warning messages for unused .dockerignore files, and info messages if * the --multi-dockerignore (-m) option is used in certain circumstances. diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index de14837b6..32c577def 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -512,3 +512,25 @@ export function pickAndRename>( }); return _.mapKeys(_.pick(obj, fields), (_val, key) => rename[key]); } + +/** + * Converts a size in bytes to a human-readable string. + * + * @param {number} bytes The size in bytes. + * @returns {string} A formatted string with the appropriate unit (e.g., "1.4 MB"). + */ +export function humanizeSize(bytes: number): string { + if (bytes == null || bytes === 0) { + return '0 Bytes'; + } + + const units = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + if (i === 0) { + return `${bytes} ${units[i]}`; + } + + const value = bytes / Math.pow(1024, i); + return `${parseFloat(value.toFixed(1))} ${units[i]}`; +} diff --git a/src/utils/remote-build.ts b/src/utils/remote-build.ts index d783b1fd3..1c1001cb6 100644 --- a/src/utils/remote-build.ts +++ b/src/utils/remote-build.ts @@ -20,11 +20,11 @@ import type { PlainResponse } from 'got'; import type got from 'got'; import type { RegistrySecrets } from '@balena/compose/dist/multibuild'; import type * as Stream from 'stream'; +import { PassThrough } from 'stream'; import streamToPromise = require('stream-to-promise'); import type { Pack } from 'tar-stream'; - import { ExpectedError, SIGINTError } from '../errors'; -import { tarDirectory } from './compose_ts'; +import { tarDirectory, getTarPackSize } from './compose_ts'; import { getVisuals, stripIndent } from './lazy'; import Logger = require('./logger'); @@ -326,10 +326,17 @@ async function getTarStream(build: RemoteBuild): Promise { convertEol: build.opts.convertEol, multiDockerignore: build.opts.multiDockerignore, }); + const passthrough = new PassThrough(); + tarStream.pipe(passthrough); + const tarPackSize = await getTarPackSize(tarStream); globalLogger.logDebug( `Tarring complete in ${Date.now() - tarStartTime} ms`, ); - return tarStream; + const { humanizeSize } = await import('./helpers'); + globalLogger.logDebug( + `The total size of the upload is ${humanizeSize(tarPackSize)}.`, + ); + return passthrough; } finally { tarSpinner.stop(); }