138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
'use node';
|
|
|
|
import { action } from '../_generated/server';
|
|
import { v } from 'convex/values';
|
|
|
|
/**
|
|
* Baixa uma imagem de uma URL externa e converte para base64.
|
|
*
|
|
* Esta action roda no servidor (Node.js), então não tem restrições de CORS
|
|
* do navegador. Pode baixar imagens de qualquer domínio.
|
|
*
|
|
* @param url - URL da imagem a ser baixada
|
|
* @returns String base64 da imagem (data URL) ou null se falhar
|
|
*/
|
|
export const downloadImageAsBase64 = action({
|
|
args: {
|
|
url: v.string()
|
|
},
|
|
returns: v.union(v.string(), v.null()),
|
|
handler: async (ctx, args): Promise<string | null> => {
|
|
const { url } = args;
|
|
|
|
try {
|
|
// Validar URL
|
|
let urlObj: URL;
|
|
try {
|
|
urlObj = new URL(url);
|
|
} catch {
|
|
console.error('URL inválida:', url);
|
|
return null;
|
|
}
|
|
|
|
// Verificar se é uma URL HTTP/HTTPS
|
|
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
|
console.error('Protocolo não suportado:', urlObj.protocol);
|
|
return null;
|
|
}
|
|
|
|
// Baixar a imagem (server-side não tem CORS)
|
|
// Tentar múltiplas estratégias para evitar bloqueios (403) de CDNs
|
|
type HeadersStrategy = Record<string, string>;
|
|
|
|
const estrategias: Array<{ name: string; headers: HeadersStrategy }> = [
|
|
// Estratégia 1: Headers completos de navegador moderno
|
|
{
|
|
name: 'headers-completos',
|
|
headers: {
|
|
'User-Agent':
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
|
'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
Referer: urlObj.origin + '/',
|
|
'Sec-Fetch-Dest': 'image',
|
|
'Sec-Fetch-Mode': 'no-cors',
|
|
'Sec-Fetch-Site': 'cross-site',
|
|
'Cache-Control': 'no-cache'
|
|
} as HeadersStrategy
|
|
},
|
|
// Estratégia 2: Headers mínimos mas realistas
|
|
{
|
|
name: 'headers-minimos',
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
Accept: 'image/*',
|
|
Referer: urlObj.origin + '/'
|
|
} as HeadersStrategy
|
|
},
|
|
// Estratégia 3: Apenas User-Agent básico
|
|
{
|
|
name: 'user-agent-basico',
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0'
|
|
} as HeadersStrategy
|
|
},
|
|
// Estratégia 4: Sem headers (máximo compatibilidade)
|
|
{
|
|
name: 'sem-headers',
|
|
headers: {} as HeadersStrategy
|
|
}
|
|
];
|
|
|
|
let ultimoErro: { status?: number; statusText?: string; message?: string } | null = null;
|
|
|
|
for (const estrategia of estrategias) {
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: estrategia.headers,
|
|
signal: AbortSignal.timeout(10000) // Timeout de 10 segundos
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Verificar Content-Type
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType && contentType.startsWith('image/')) {
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
const buffer = Buffer.from(arrayBuffer);
|
|
const base64 = buffer.toString('base64');
|
|
const dataUrl = `data:${contentType};base64,${base64}`;
|
|
console.log(
|
|
`✅ Imagem baixada usando estratégia "${estrategia.name}": ${url} (${buffer.length} bytes)`
|
|
);
|
|
return dataUrl;
|
|
} else {
|
|
console.warn(`Estratégia "${estrategia.name}": Content-Type inválido:`, contentType);
|
|
}
|
|
} else {
|
|
ultimoErro = {
|
|
status: response.status,
|
|
statusText: response.statusText
|
|
};
|
|
console.warn(
|
|
`Estratégia "${estrategia.name}" falhou: ${response.status} ${response.statusText}`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
ultimoErro = { message: errorMessage };
|
|
console.warn(`Estratégia "${estrategia.name}" lançou exceção:`, errorMessage);
|
|
}
|
|
}
|
|
|
|
// Se todas as estratégias falharam, logar erro detalhado
|
|
console.error(
|
|
'❌ Todas as estratégias falharam ao baixar imagem:',
|
|
url,
|
|
'Último erro:',
|
|
ultimoErro
|
|
);
|
|
return null;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
console.error('Erro ao baixar imagem de URL:', url, errorMessage);
|
|
return null;
|
|
}
|
|
}
|
|
});
|