Skip to content

Commit 91e5db5

Browse files
committed
Add logger support to httpDownload for automatic progress tracking
Enhance httpDownload with integrated logger support, allowing CLI tools to get automatic progress logging without writing custom callbacks. Changes: - Add logger?: Logger parameter to HttpDownloadOptions - Add progressInterval?: number parameter (default 10%) to control logging frequency - Auto-generate onProgress callback when logger is provided - Logger takes precedence over custom onProgress callback - Format: "Progress: XX% (Y.Y MB / Z.Z MB)" Example usage: import { httpDownload } from '@socketsecurity/lib/http-request' import { getDefaultLogger } from '@socketsecurity/lib/logger' await httpDownload(url, destPath, { logger: getDefaultLogger(), progressInterval: 10, // Log every 10% retries: 2, retryDelay: 5000 }) This eliminates boilerplate progress tracking code in CLI tools while maintaining backward compatibility with existing onProgress usage.
1 parent 96976d5 commit 91e5db5

File tree

1 file changed

+69
-1
lines changed

1 file changed

+69
-1
lines changed

src/http-request.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { createWriteStream } from 'fs'
1818

1919
import type { IncomingMessage } from 'http'
2020

21+
import type { Logger } from './logger.js'
22+
2123
let _http: typeof import('http') | undefined
2224
let _https: typeof import('https') | undefined
2325
/**
@@ -318,9 +320,31 @@ export interface HttpDownloadOptions {
318320
* ```
319321
*/
320322
headers?: Record<string, string> | undefined
323+
/**
324+
* Logger instance for automatic progress logging.
325+
* When provided with `progressInterval`, will automatically log download progress.
326+
* Mutually exclusive with `onProgress` - if both are provided, `logger` takes precedence.
327+
*
328+
* @example
329+
* ```ts
330+
* import { getDefaultLogger } from '@socketsecurity/lib/logger'
331+
*
332+
* const logger = getDefaultLogger()
333+
* await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {
334+
* logger,
335+
* progressInterval: 10 // Log every 10%
336+
* })
337+
* // Output:
338+
* // Progress: 10% (5.2 MB / 52.0 MB)
339+
* // Progress: 20% (10.4 MB / 52.0 MB)
340+
* // ...
341+
* ```
342+
*/
343+
logger?: Logger | undefined
321344
/**
322345
* Callback for tracking download progress.
323346
* Called periodically as data is received.
347+
* If `logger` is provided, this callback is ignored.
324348
*
325349
* @param downloaded - Number of bytes downloaded so far
326350
* @param total - Total file size in bytes (from Content-Length header)
@@ -336,6 +360,29 @@ export interface HttpDownloadOptions {
336360
* ```
337361
*/
338362
onProgress?: ((downloaded: number, total: number) => void) | undefined
363+
/**
364+
* Progress reporting interval as a percentage (0-100).
365+
* Only used when `logger` is provided.
366+
* Progress will be logged each time the download advances by this percentage.
367+
*
368+
* @default 10
369+
*
370+
* @example
371+
* ```ts
372+
* // Log every 10%
373+
* await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {
374+
* logger: getDefaultLogger(),
375+
* progressInterval: 10
376+
* })
377+
*
378+
* // Log every 25%
379+
* await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {
380+
* logger: getDefaultLogger(),
381+
* progressInterval: 25
382+
* })
383+
* ```
384+
*/
385+
progressInterval?: number | undefined
339386
/**
340387
* Number of retry attempts for failed downloads.
341388
* Uses exponential backoff: delay = `retryDelay` * 2^attempt.
@@ -694,20 +741,41 @@ export async function httpDownload(
694741
): Promise<HttpDownloadResult> {
695742
const {
696743
headers = {},
744+
logger,
697745
onProgress,
746+
progressInterval = 10,
698747
retries = 0,
699748
retryDelay = 1000,
700749
timeout = 120_000,
701750
} = { __proto__: null, ...options } as HttpDownloadOptions
702751

752+
// Create progress callback from logger if provided
753+
let progressCallback:
754+
| ((downloaded: number, total: number) => void)
755+
| undefined
756+
if (logger) {
757+
let lastPercent = 0
758+
progressCallback = (downloaded: number, total: number) => {
759+
const percent = Math.floor((downloaded / total) * 100)
760+
if (percent >= lastPercent + progressInterval) {
761+
logger.log(
762+
` Progress: ${percent}% (${(downloaded / 1024 / 1024).toFixed(1)} MB / ${(total / 1024 / 1024).toFixed(1)} MB)`,
763+
)
764+
lastPercent = percent
765+
}
766+
}
767+
} else {
768+
progressCallback = onProgress
769+
}
770+
703771
// Retry logic with exponential backoff
704772
let lastError: Error | undefined
705773
for (let attempt = 0; attempt <= retries; attempt++) {
706774
try {
707775
// eslint-disable-next-line no-await-in-loop
708776
return await httpDownloadAttempt(url, destPath, {
709777
headers,
710-
onProgress,
778+
onProgress: progressCallback,
711779
timeout,
712780
})
713781
} catch (e) {

0 commit comments

Comments
 (0)