Ir para o conteúdo
Documentação

Guias Node.js

Integração com Node.js

Envie um arquivo ou lote à automação do painel e receba os resultados no seu servidor.

fetch nativo · Node.js 20+

Você vai precisar de uma automação publicada e uma chave com execute e read.

Nesta página

Configure os dados do painel

Copie a chave autorizada, o ID e a versão publicada da automação. Configure as quatro variáveis no ambiente do servidor.

Ambiente do servidor
DUCKFILE_API_URL=https://duckfile.com/api/v1/externalDUCKFILE_API_KEY=SUA_CHAVE_NO_SERVIDORDUCKFILE_AUTOMATION_ID=UUID_DA_AUTOMACAODUCKFILE_AUTOMATION_VERSION=1

Use HTTPS com certificado válido e mantenha a chave somente no backend.

Envie e receba os arquivos

Baixe o arquivo de integração, incorpore ao seu projeto e chame a função. Ela envia, acompanha e recebe cada resultado individualmente. Aceita um caminho ou uma lista de caminhos.

Node.js
import { executarAutomacao } from './duckfile.mjs'; const resultado = await executarAutomacao('imagem.png');console.log(resultado.status, resultado.arquivos);

Guarde os arquivos recebidos no seu próprio sistema. A execução não salva nada na biblioteca da conta.

Use os resultados no seu negócio

A função retorna o ID, o estado e os caminhos locais em arquivos. Em resultado parcial, preserve os itens recebidos e trate as falhas separadamente. A função só termina após baixar os resultados disponíveis.

Retorno ilustrativo
{"id":"UUID_DA_EXECUCAO","status":"COMPLETED","arquivos":[{"id":"UUID_DO_ARQUIVO","path":"resultado-UUID.webp","bytes":12345}],"falhas":[]}

Código da integração

Fonte de exemplo, sem SDK oficial. Você pode incorporar e adaptar a implementação, incluindo persistir a chave de idempotência e o ID da execução para recuperar falhas de rede. Nenhuma etapa da automação é definida nesse código.

duckfile.mjs
import { randomUUID } from 'node:crypto';import { readFile, mkdir, link, rm } from 'node:fs/promises';import { createWriteStream } from 'node:fs';import { Transform, Readable } from 'node:stream';import { pipeline } from 'node:stream/promises';import { basename, extname, join } from 'node:path';import { setTimeout as pause } from 'node:timers/promises';import { realpathSync } from 'node:fs';import { fileURLToPath } from 'node:url'; // Exemplo para incorporar ao seu servidor; não é um SDK oficial.export async function executarAutomacao(entrada, { idempotencyKey = randomUUID(), outputDir = '.' } = {}) {  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') throw new Error('Mantenha a validação TLS habilitada.');  const base = new URL(process.env.DUCKFILE_API_URL);  if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash || !base.pathname.replace(/\/$/, '').endsWith('/api/v1/external')) throw new Error('Configure a base HTTPS da API.');  const api = base.href.replace(/\/$/, '');  const key = process.env.DUCKFILE_API_KEY;  const automation = process.env.DUCKFILE_AUTOMATION_ID;  const version = Number(process.env.DUCKFILE_AUTOMATION_VERSION);  const uuid = /^[a-f\d]{8}-(?:[a-f\d]{4}-){3}[a-f\d]{12}$/i;  if (!key || /[\r\n]/.test(key) || !uuid.test(automation ?? '') || !Number.isSafeInteger(version) || version < 1) throw new Error('Configure chave, ID e versão publicados no painel.');  const paths = Array.isArray(entrada) ? entrada : [entrada];  if (!paths.length || paths.length > 100) throw new Error('Informe um arquivo ou lote.');  const types = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.avif': 'image/avif' };  const form = new FormData();  form.set('version', String(version));  let total = 0;  for (const path of paths) {    const type = types[extname(path).toLowerCase()], bytes = await readFile(path);    total += bytes.length;    if (!type || !bytes.length || bytes.length > 25000000 || total > 79000000) throw new Error('Use imagens de até 25 MB e respeite o limite total de 80 MB com o formulário.');    form.append('files', new Blob([bytes], { type }), basename(path));  }  async function json(path, body) {    let response;    try { response = await fetch(api + path, { method: body ? 'POST' : 'GET',      headers: { Authorization: 'Bearer ' + key, ...(body ? { 'Idempotency-Key': idempotencyKey } : {}) },      ...(body ? { body } : {}), signal: AbortSignal.timeout(body ? 150000 : 30000), redirect: 'error' });    } catch (error) { error.idempotencyKey = idempotencyKey; throw error; }    if (!response.ok) {      const problem = await response.json().catch(() => ({}));      const error = new Error('API: HTTP ' + response.status + ' ' + (problem.code ?? '') + '. Preserve a chave de idempotência em caso de reenvio.');      error.idempotencyKey = idempotencyKey; throw error;    }    return (await response.json()).data;  }  let run = await json('/automations/' + automation + '/runs', form);  if (!uuid.test(run.id)) throw new Error('ID de execução inválido.');  const deadline = Date.now() + 35 * 60 * 1000;  const terminal = ['COMPLETED', 'PARTIAL', 'FAILED', 'CANCELLED', 'EXPIRED'];  while (!terminal.includes(run.status)) {    if (Date.now() >= deadline) throw new Error('Prazo local atingido. Acompanhe a execução ' + run.id + ' antes de reenviar.');    await pause(2000);    run = await json('/runs/' + run.id);  }  await mkdir(outputDir, { recursive: true });  const arquivos = [];  for (const file of run.files ?? []) {    if (!file.result) continue;    if (!uuid.test(file.id) || file.result.path !== '/runs/' + run.id + '/files/' + file.id) throw new Error('Caminho de resultado inválido.');    const expected = Number(file.result.bytes);    if (!Number.isSafeInteger(expected) || expected < 1 || expected > 100000000) throw new Error('Tamanho de resultado inválido.');    const suffix = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/avif': 'avif' }[file.result.mimeType];    if (!suffix) throw new Error('Formato de resultado inválido.');    const path = join(outputDir, 'resultado-' + file.id + '.' + suffix);    const partial = path + '.' + randomUUID() + '.part';    const response = await fetch(api + file.result.path, { headers: { Authorization: 'Bearer ' + key }, signal: AbortSignal.timeout(120000), redirect: 'error' });    if (!response.ok || !response.body) throw new Error('Recebimento: HTTP ' + response.status);    let received = 0;    try {      await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, encoding, callback) {        received += chunk.length;        callback(received > expected ? new Error('Tamanho de resultado inválido.') : null, chunk);      } }), createWriteStream(partial, { mode: 0o600, flags: 'wx' }));      if (received !== expected) throw new Error('Arquivo recebido incompleto.');      await link(partial, path); // Publicação exclusiva: nunca substitui um resultado existente.    } finally { await rm(partial, { force: true }); }    arquivos.push({ id: file.id, path, bytes: received });  }  const falhas = (run.files ?? []).filter(file => file.status !== 'COMPLETED' || file.partial).map(file => ({ id: file.id, errorCode: file.errorCode }));  if (!arquivos.length) throw new Error('Nenhum arquivo disponível: ' + run.status);  return { id: run.id, status: run.status, idempotencyKey, arquivos, falhas };}if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {  console.log(JSON.stringify(await executarAutomacao(process.argv.slice(2)), null, 2));}

Trate falhas e reenvios

Não crie outra execução apenas porque a conexão caiu. Preserve a chave de idempotência e os mesmos arquivos. Acompanhe o ID já recebido; mudança de versão, falta de crédito e falha de validação exigem tratamento próprio.