feat: enhance 'Almoxarifado' functionality by integrating barcode scanning for material entry and exit, improving user experience with loading indicators and error handling for better inventory management
This commit is contained in:
137
packages/backend/convex/actions/downloadImage.ts
Normal file
137
packages/backend/convex/actions/downloadImage.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
'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;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user