180 lines
6.1 KiB
JavaScript
180 lines
6.1 KiB
JavaScript
|
|
const fs = require('fs'); // 👈 Usa il modulo promises
|
||
|
|
const axios = require('axios');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scarica un'immagine da una URL e la salva in una directory locale
|
||
|
|
* @param {string} url - L'URL dell'immagine da scaricare
|
||
|
|
* @param {string} filepath - Il percorso dove salvare l'immagine scaricata
|
||
|
|
*/
|
||
|
|
class ImageDownloader {
|
||
|
|
/**
|
||
|
|
* Scarica un'immagine da una URL e la salva in una directory locale.
|
||
|
|
* Tenta di scaricare fino a 3 volte in caso di errore, con un ritardo tra i tentativi.
|
||
|
|
*
|
||
|
|
* @param {string} url - L'URL dell'immagine da scaricare
|
||
|
|
* @param {string} filepath - Il percorso dove salvare l'immagine scaricata
|
||
|
|
* @param {number} maxRetries - Numero massimo di tentativi in caso di fallimento (default: 3)
|
||
|
|
* @param {number} delay - Ritardo in millisecondi tra i tentativi (default: 1000)
|
||
|
|
* @returns {Promise<boolean>}
|
||
|
|
*/
|
||
|
|
async downloadImage(url, filepath, options = {}) {
|
||
|
|
const {
|
||
|
|
maxRetries = 3, // Aumentato il numero di tentativi predefiniti
|
||
|
|
initialDelay = 1000, // Ritardo iniziale
|
||
|
|
maxDelay = 10000, // Ritardo massimo
|
||
|
|
timeout = 30000, // Timeout della richiesta
|
||
|
|
validateContentType = true, // Verifica del tipo di contenuto
|
||
|
|
nomefileoriginale = true,
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
// Funzione per il backoff esponenziale
|
||
|
|
const getDelay = (attempt) => {
|
||
|
|
return Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay);
|
||
|
|
};
|
||
|
|
|
||
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
|
|
try {
|
||
|
|
// Verifica se il filepath esiste già
|
||
|
|
if (await this.isFileExistsAsync(filepath)) {
|
||
|
|
fs.unlinkSync(filepath);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attempt > 1) console.log(`📥 Tentativo ${attempt}/${maxRetries} - Scaricamento: ${url}`);
|
||
|
|
|
||
|
|
const response = await axios({
|
||
|
|
url,
|
||
|
|
method: 'GET',
|
||
|
|
responseType: 'stream',
|
||
|
|
timeout: timeout,
|
||
|
|
maxRedirects: 5,
|
||
|
|
headers: {
|
||
|
|
'User-Agent':
|
||
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||
|
|
Accept: 'image/jpeg,image/png,image/webp,image/gif,image/*', // Specifico per immagini
|
||
|
|
'Cache-Control': 'no-cache', // Evita problemi di caching
|
||
|
|
Connection: 'keep-alive',
|
||
|
|
},
|
||
|
|
validateStatus: (status) => status === 200, // Per immagini ci aspettiamo 200
|
||
|
|
maxContentLength: 10 * 1024 * 1024, // Limite di 10MB per immagine
|
||
|
|
});
|
||
|
|
|
||
|
|
// Verifica del content-type se richiesto
|
||
|
|
if (validateContentType) {
|
||
|
|
const contentType = response.headers['content-type'];
|
||
|
|
if (!contentType || !contentType.startsWith('image/')) {
|
||
|
|
throw new Error(`Content-Type non valido: ${contentType}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verifica della dimensione del file
|
||
|
|
const contentLength = parseInt(response.headers['content-length']);
|
||
|
|
if (contentLength && contentLength > 100 * 1024 * 1024) {
|
||
|
|
// 100MB limit
|
||
|
|
throw new Error('File troppo grande');
|
||
|
|
}
|
||
|
|
|
||
|
|
let downloadedBytes = 0;
|
||
|
|
response.data.on('data', (chunk) => {
|
||
|
|
downloadedBytes += chunk.length;
|
||
|
|
});
|
||
|
|
|
||
|
|
let writer = null;
|
||
|
|
|
||
|
|
if (nomefileoriginale) {
|
||
|
|
// Estrai il nome del file dall'URL o da Content-Disposition
|
||
|
|
//let fileName = this.extractFileNameFromUrl(url) || this.extractFileNameFromHeaders(response.headers);
|
||
|
|
let fileName = path.basename(response.data.responseUrl);
|
||
|
|
|
||
|
|
// Se il nome del file non è specificato, genera un nome predefinito
|
||
|
|
if (!fileName) {
|
||
|
|
fileName = `image_${Date.now()}.jpg`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Genera il percorso completo del file
|
||
|
|
const fullPath = path.join(path.dirname(filepath), fileName);
|
||
|
|
|
||
|
|
filepath = fullPath;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scrivi il file sul disco
|
||
|
|
writer = fs.createWriteStream(filepath);
|
||
|
|
|
||
|
|
response.data.pipe(writer);
|
||
|
|
|
||
|
|
await new Promise((resolve, reject) => {
|
||
|
|
writer.on('finish', resolve);
|
||
|
|
writer.on('error', (error) => {
|
||
|
|
fs.unlink(filepath, () => {}); // Pulizia in caso di errore
|
||
|
|
reject(error);
|
||
|
|
});
|
||
|
|
response.data.on('error', (error) => {
|
||
|
|
fs.unlink(filepath, () => {});
|
||
|
|
reject(error);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
console.info(`✅ Immagine scaricata con successo in ${filepath}`);
|
||
|
|
|
||
|
|
return { ris: true, filepath };
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`❌ Errore nel tentativo ${attempt}/${maxRetries}:`, error.message);
|
||
|
|
|
||
|
|
// Pulizia del file in caso di errore
|
||
|
|
if (await this.isFileExistsAsync(filepath)) {
|
||
|
|
fs.unlinkSync(filepath);
|
||
|
|
}
|
||
|
|
|
||
|
|
// se in error.message c'è '404' allora esci e ritorna code: 404
|
||
|
|
if (error.message.includes('404')) {
|
||
|
|
return { ris: false, code: 404 };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (attempt === maxRetries) {
|
||
|
|
console.error(`Download fallito dopo ${maxRetries} tentativi: ${error.message}`);
|
||
|
|
return { ris: false };
|
||
|
|
}
|
||
|
|
|
||
|
|
const delay = getDelay(attempt);
|
||
|
|
console.info(`🔄 Attendo ${delay}ms prima del prossimo tentativo...`);
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Funzione per estrarre il nome del file dall'URL
|
||
|
|
extractFileNameFromUrl(url) {
|
||
|
|
const match = url.match(/\/([^/?#]+)(?:[?#]|$)/);
|
||
|
|
return match ? decodeURIComponent(match[1]) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Funzione per estrarre il nome del file da Content-Disposition
|
||
|
|
extractFileNameFromHeaders(headers) {
|
||
|
|
const contentDisposition = headers['content-disposition'];
|
||
|
|
if (contentDisposition) {
|
||
|
|
const match = contentDisposition.match(/filename="([^"]+)"/);
|
||
|
|
if (match) {
|
||
|
|
return decodeURIComponent(match[1]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async isFileExistsAsync (filename) {
|
||
|
|
try {
|
||
|
|
let fileExists = await fs.promises
|
||
|
|
.stat(filename)
|
||
|
|
.then(() => true)
|
||
|
|
.catch(() => false);
|
||
|
|
// console.log(filename, 'esiste', fileExists)
|
||
|
|
return fileExists;
|
||
|
|
} catch (e) {
|
||
|
|
// console.log(filename, 'esiste', 'FALSE')
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = ImageDownloader;
|