@typecad/jlcpcb-parts
Version:
Intelligent fuzzy search for JLCPCB electrical components with CLI interface
135 lines • 5.91 kB
JavaScript
import { promises as fs } from 'fs';
import { CacheUtils } from '../utils/CacheUtils.js';
/**
* Handles downloading CSV files from remote URLs with retry logic.
* Supports both shared cache directory and custom path modes.
*/
export class CsvDownloader {
maxRetries;
initialBackoffMs;
useSharedCache;
customCacheDir;
/**
* Creates a new CSV downloader
* @param maxRetries Maximum number of retry attempts (default: 3)
* @param initialBackoffMs Initial backoff time in milliseconds (default: 1000)
* @param useSharedCache Whether to use shared cache directory (default: true)
* @param customCacheDir Custom cache directory (used when useSharedCache is false)
*/
constructor(maxRetries = 3, initialBackoffMs = 1000, useSharedCache = true, customCacheDir = '.') {
this.maxRetries = maxRetries;
this.initialBackoffMs = initialBackoffMs;
this.useSharedCache = useSharedCache;
this.customCacheDir = customCacheDir;
}
/**
* Downloads a CSV file from a URL with retry logic
* @param url URL to download the CSV from
* @param fileName Name of the file to save (will be resolved to appropriate cache directory)
* @returns Promise that resolves when download is complete
* @throws NetworkError if download fails after all retries
*/
async downloadCsv(url, fileName) {
let attempt = 0;
let lastError = null;
// Resolve the actual file path based on cache configuration
const outputPath = await CacheUtils.resolveCacheFilePath(fileName, this.useSharedCache, this.customCacheDir);
while (attempt < this.maxRetries) {
try {
// Attempt to download the file
await this.performDownload(url, outputPath);
// If download succeeds, validate the file
await this.validateCsvFile(outputPath);
// If we get here, both download and validation succeeded
return;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// If it's a validation error, don't retry
if (errorMessage.includes('CSV validation failed')) {
throw error;
}
lastError = error instanceof Error ? error : new Error(String(error));
attempt++;
if (attempt < this.maxRetries) {
// Calculate exponential backoff time
const backoffTime = this.initialBackoffMs * Math.pow(2, attempt - 1);
console.warn(`Download attempt ${attempt} failed. Retrying in ${backoffTime}ms...`);
await this.delay(backoffTime);
}
}
}
// If we get here, all attempts failed
const networkError = new Error(`Failed to download CSV after ${this.maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
networkError.code = lastError instanceof Error && 'code' in lastError ?
String(lastError.code) : 'DOWNLOAD_FAILED';
throw networkError;
}
/**
* Performs the actual download operation
* @param url URL to download from
* @param outputPath Path to save the file
* @private
*/
async performDownload(url, outputPath) {
// Use native fetch API for Node.js
const response = await fetch(url);
if (!response.ok) {
const networkError = new Error(`HTTP error: ${response.status} ${response.statusText}`);
networkError.statusCode = response.status;
throw networkError;
}
// Get response as text
const text = await response.text();
// Write to file
await fs.writeFile(outputPath, text, 'utf-8');
}
/**
* Validates that the downloaded CSV file is complete and valid
* @param filePath Path to the CSV file
* @private
*/
async validateCsvFile(filePath) {
try {
// Check if file exists and has content
const stats = await fs.stat(filePath);
if (stats.size === 0) {
throw new Error('Downloaded CSV file is empty');
}
// Read the first few bytes to check if it starts with valid CSV content
const fileHandle = await fs.open(filePath, 'r');
const buffer = Buffer.alloc(1024); // Read first 1KB
const { bytesRead } = await fileHandle.read(buffer, 0, 1024, 0);
await fileHandle.close();
if (bytesRead === 0) {
throw new Error('Could not read from CSV file');
}
const fileStart = buffer.toString('utf8', 0, bytesRead);
// Basic validation: Check if the file starts with expected CSV header format
// This is a simple check - we're looking for comma-separated values in the first line
if (!fileStart.includes(',')) {
throw new Error('File does not appear to be a valid CSV (no commas found in header)');
}
// Additional validation could be added here, such as checking for specific column headers
}
catch (error) {
// If validation fails, delete the invalid file
try {
await fs.unlink(filePath);
}
catch {
// Ignore errors when trying to delete the file
}
throw new Error(`CSV validation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Simple delay function for implementing backoff
* @param ms Milliseconds to delay
* @private
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
//# sourceMappingURL=CsvDownloader.js.map