FTP a SFTP použití ve výpočtech

Návod pro konzultanty, kteří potřebují z výpočtu na úkolu odeslat soubor na FTP nebo SFTP server, případně ho odtud stáhnout. Přístupové heslo se drží v Trezoru, takže není nikdy zapsané v textu šablony. Nahrazuje dřívější řešení přes curl.* (od v5.17) – tyto metody byly odstraněny a validace šablony na ně upozorní textem „For FTP/SFTP calls use FtpApi."

Kde API funguje

Objekty ftp a vault jsou vložené pouze do sandboxu výpočtů na úkolu (Task JS).

Kontext výpočtu

ftp a vault

Výpočet na úkolu nebo na tlačítku (Task JS)

ano

Výpočet na případu (Process JS)

ne

Identity výpočet

ne

Konzole výpočtů

ne

Trezor (vault.get()) a metoda getFtpClientWithVaultPassword() vyžadují TAS 5.17 nebo novější. Na starších instalacích si nejdřív ověřte verzi.

Předpoklady

Než začnete psát výpočet, musí být hotové dvě věci na straně administrátora.

1. Heslo v Trezoru

V Administrace → Trezor vytvořte položku typu secret a pojmenujte ji tak, aby bylo jasné, ke kterému systému patří – například ftp_partner_password. Ve výpočtu se na ni pak odkážete názvem.

Doporučená konvence názvu: {systém}_{protokol}_{účel}, tedy partner_ftp_password nebo eshop_sftp_import. Jedna položka na jeden servisní účet.

2. Povolené cesty v allowedExternalSources

Každá cesta předaná do FTP API – včetně vzdálené cesty na serveru – prochází kontrolou povolených zdrojů. Bez zapsání cílové cesty do konfigurace skončí výpočet chybou BANNED_SOURCE („Accessing this source is not allowed!").

  • konfigurační klíč: application.allowedExternalSources
  • env proměnná: TAS_DMS_ALLOWED_EXTERNAL_SOURCES, hodnoty oddělené znakem |
  • v administraci je pole jen pro čtení – mění se přes prostředí, ne v UI
TAS_DMS_ALLOWED_EXTERNAL_SOURCES=/tas/faktury|/out/objednavky|/data

Chování kontroly, které je dobré znát:

  • porovnává se prefix cesty, takže /data povolí i /data/2026/x.csv, ale zároveň i /database/... – volte cesty co nejkonkrétnější,
  • automaticky povolené jsou podsložky interního TMP (<paths.tmp>/apicall, /uploads, /pdf, /zip, /generated) a asset rooty,
  • paths.storagePath povolený není – pokud do něj chcete stahovat, musí být v konfiguraci.
Nejjednodušší postup u stahování: cílovou složku volte v TMP (<paths.tmp>/apicall/...). Ta je povolená automaticky a nemusíte kvůli ní zasahovat do konfigurace prostředí.

Připojení k serveru s heslem z Trezoru

Nejdřív si vyzvedněte referenci na heslo, pak s ní vytvořte klienta. Konfigurace se předává bez klíče password.

const passwordRef = vault.get('ftp_partner_password');

const client = ftp.getFtpClientWithVaultPassword({
protocol: 'ftp',
host: 'ftp.partner.cz',
port: 21,
username: 'tas-export',
secure: false,
}, passwordRef);

passwordRef je neprůhledná reference. Nelze ji vypsat do logu ani serializovat – JSON.stringify() i toString() vrátí jen [VaultSecretRef]. Heslo se rozbalí až uvnitř FTP API.

Varianta pro SFTP se liší jen protokolem a portem:

const passwordRef = vault.get('sftp_partner_password');

const client = ftp.getFtpClientWithVaultPassword({
protocol: 'sftp',
host: 'sftp.partner.cz',
port: 22,
username: 'tas-import',
timeout: 30000,
}, passwordRef);
Existuje i ftp.getFtpClient(config), kde se heslo předává v otevřeném textu. Použitelné je jen pro rychlý test nebo pro server bez autentizace – takové heslo je součástí šablony procesu a uvidí ho každý, kdo ji smí editovat. Do produkce vždy variantu s Trezorem.

Klíče konfigurace

Klíč

Povinný

Platí pro

Popis

protocol

ano

oba

"ftp" nebo "sftp", určuje použitý konektor

host

ano

oba

hostname nebo IP serveru

port

ne

oba

výchozí 21 pro FTP, 22 pro SFTP

username

prakticky ano

oba

servisní účet; u FTP bez uvedení jde o anonymní přístup

secure

ne

FTP

true = TLS (AUTH TLS / FTPES). Hodnota "implicit" se přeloží na explicitní TLS, čistě implicitní FTPS na portu 990 tímto API nefunguje

secureOptions

ne

FTP

TLS volby Node.js (rejectUnauthorized, ca)

timeout

ne

SFTP

timeout připojení v ms, výchozí 30000. U FTP se nepředává a hodnota se ignoruje

privateKey, passphrase

ne

SFTP

privátní klíč v PEM formátu a jeho heslo

privateKey nelze načíst z Trezoru – vault reference je použitelná pouze jako heslo. Pro autentizaci klíčem by klíč musel být v textu výpočtu, proto preferujte SFTP s heslem z Trezoru. U nešifrovaného FTP (secure: false) jdou přihlašovací údaje po síti v otevřeném textu – mimo interní síť použijte SFTP nebo secure: true.

Přehled operací

Metoda

Vrací

Poznámka

list(remotePath)

pole objektů

name, type (file / directory / symlink), size, modifiedAt, rights

upload(source, remotePath)

text

source je soubor z DMS, ne cesta na disku

download(remotePath, localPath)

text

localPath vždy uvádějte jako plnou cestu včetně jména souboru

delete(remotePath)

text

maže soubor, ne složku

exists(remotePath)

boolean

u FTP a SFTP se chová odlišně, viz níže

createDirectory(remotePath, recursive)

cesta / text

idempotentní – nad existující složkou nechybuje

removeDirectory(remotePath, recursive)

cesta / text

u FTP maže včetně obsahu bez ohledu na parametr

rename(oldPath, newPath)

cesta / text

slouží i k přesunu mezi složkami

Návratové hodnoty neparsujte. FTP konektor vrací hlášení serveru (např. 226 Transfer complete.), SFTP konektor cestu nebo hlášení knihovny. Spolehlivý signál úspěchu je jediný: volání nevyhodilo výjimku.

Co přijímá upload()

source je odkaz na soubor v DMS aktuálního případu, ne cesta na disku. Přijímá:

  • proměnnou typu příloha – vars['Faktura'],
  • ID souboru v DMS (číslo nebo číselný string),
  • název souboru v rámci aktuálního případu.

Soubor musí patřit aktuálnímu případu (nebo jeho nadřízenému či podřízenému), jinak operace skončí chybou Dms file does not belong to process. Pokud chcete poslat soubor vytvořený ve výpočtu na disku, uložte ho nejdřív do DMS přes storage.saveToDms(filePath, fileName) a vrácené ID předejte do upload().

Příklad 1: odeslání přílohy z úkolu na FTP

Výpočet na tlačítku „Odeslat partnerovi". Založí složku podle ID případu, odešle přílohu z proměnné a výsledek zapíše do stavové proměnné.

const passwordRef = vault.get('ftp_partner_password');

const client = ftp.getFtpClientWithVaultPassword({
protocol: 'ftp',
host: 'ftp.partner.cz',
port: 21,
username: 'tas-export',
secure: false,
}, passwordRef);

const remoteDir = `/tas/faktury/${lib.iprocId()}`;
const remoteFile = `${remoteDir}/faktura.pdf`;

try {
client.createDirectory(remoteDir);
client.upload(vars['Faktura'], remoteFile);

vars['StavOdeslani'].setValue('Odesláno');
proc.info('Invoice uploaded to FTP', { remoteFile });
} catch (err) {
vars['StavOdeslani'].setValue('Chyba odeslání');
proc.error('FTP upload failed', { err: err.message, remoteFile });
debug.error('Odeslání faktury na FTP se nezdařilo. Kontaktujte správce systému.');
}

Předpoklady: v allowedExternalSources je /tas/faktury, v Trezoru položka ftp_partner_password, v šabloně proměnné Faktura (příloha) a StavOdeslani (text).

Volba chování při chybě. debug.error() zablokuje dokončení úkolu a zprávu zobrazí uživateli – vhodné, když odeslání je podmínkou pro pokračování procesu. Pokud má proces běžet dál, debug.error() vynechte a spolehněte se na stavovou proměnnou a proc.error(). Chybu nikdy nepolykejte tiše.

Více příloh v jedné proměnné

Pokud proměnná obsahuje víc souborů, upload() s předanou proměnnou pošle jen první a do logu zapíše varování. Pro všechny soubory si vytáhněte ID a názvy a projděte je v cyklu:

const ids = lib.getDMSFileIds('*', '|').split('|').filter(Boolean);
const names = lib.getDMSFileNames('*', '|').split('|').filter(Boolean);

ids.forEach((id, index) => {
client.upload(id, `${remoteDir}/${names[index]}`);
});
Filtr '*' vrací všechny přílohy případu. Novější verze TAS umí filtrovat jen soubory z konkrétní proměnné třetím parametrem – lib.getDMSFileIds('', ';', vars['Prilohy'].getValue()). Podporu na své instalaci si ověřte.

Příklad 2: stažení jednoho souboru z FTP do DMS

Opačný směr. Výpočet stáhne soubor podle názvu z proměnné do pracovní složky v TMP a odtud ho uloží jako přílohu případu.

const fileName = vars['NazevSouboru'].getValue();
const remoteDir = `/tas/faktury/${lib.iprocId()}`;

const passwordRef = vault.get('ftp_partner_password');
const client = ftp.getFtpClientWithVaultPassword({
protocol: 'ftp',
host: 'ftp.partner.cz',
port: 21,
username: 'tas-export',
secure: false,
}, passwordRef);

const paths = storage.getPaths();
const remoteFile = `${remoteDir}/${fileName}`;
const workDir = `${paths.tmp}/apicall/ftp-${lib.iprocId()}`;
const localFile = `${workDir}/${fileName}`;

try {
storage.createDir(workDir);
client.download(remoteFile, localFile);
const dmsId = storage.saveToDms(localFile, fileName);
proc.info('File downloaded from FTP', { remoteFile, localFile, dmsId });
} catch (err) {
proc.error('FTP download failed', { err: err.message, remoteFile });
debug.error(`Soubor ${fileName} se nepodařilo stáhnout z FTP.`);
}

Předpoklady: v allowedExternalSources je /tas/faktury, cílová složka je v TMP (apicall, povolená automaticky), v Trezoru položka ftp_partner_password, v šabloně proměnná NazevSouboru (text).

localFile je vždy plná cesta včetně jména souboru, ne jen adresář. Bez uvedení by se použil paths.storagePath, který navíc nemusí být v povolených zdrojích. Cesta přes storage.getPaths() je nezávislá na konkrétní instalaci.

Příklad 3: dávkové stažení souborů ze SFTP do DMS

Výpočet stáhne všechny CSV soubory z /out/objednavky, uloží je jako přílohy případu a na serveru je přesune do podsložky processed, aby se nezpracovaly znovu.

const passwordRef = vault.get('sftp_partner_password');

const client = ftp.getFtpClientWithVaultPassword({
protocol: 'sftp',
host: 'sftp.partner.cz',
port: 22,
username: 'tas-import',
timeout: 30000,
}, passwordRef);

const sourceDir = '/out/objednavky';
const archiveDir = `${sourceDir}/processed`;

const paths = storage.getPaths();
const workDir = `${paths.tmp}/apicall/objednavky-${lib.iprocId()}`;
storage.createDir(workDir);

try {
const files = client.list(sourceDir);
const csvFiles = files.filter((file) => file.type === 'file' && file.name.toLowerCase().endsWith('.csv'));

client.createDirectory(archiveDir);

let imported = 0;
for (const file of csvFiles) {
const source = `${sourceDir}/${file.name}`;
const target = `${workDir}/${file.name}`;

client.download(source, target);
storage.saveToDms(target, file.name);
client.rename(source, `${archiveDir}/${file.name}`);
imported += 1;
}

vars['PocetImportovanych'].setValue(imported);
proc.info('Import from SFTP finished', { imported, sourceDir });
} catch (err) {
vars['StavImportu'].setValue('Chyba importu');
proc.error('SFTP import failed', { err: err.message, sourceDir });
}

Předpoklady: v allowedExternalSources je /out/objednavky (pokryje i podsložku processed), TMP složka apicall je povolená automaticky, v Trezoru je položka sftp_partner_password.

Životní cyklus připojení a výkon

Každé volání metody otevře nové připojení, provede operaci a připojení zavře. Neexistuje sdílená session mezi voláními, takže:

  • cyklus přes 200 souborů znamená 200 přihlášení – počítejte s tím u serverů s limitem souběžných spojení nebo s rate-limitem,
  • dávky zpracovávejte sekvenčně cyklem for ... of, nikdy paralelně,
  • u velkých přenosů hrozí timeout výpočtu – větší dávky řešte plánováním, ne výpočtem na tlačítku.

FTP versus SFTP: na co si dát pozor

Chování

SFTP

FTP

exists()

dotaz na existenci objektu – true pro soubory, složky i symlinky

testuje velikost souboru větší než 0 – false pro složky a pro prázdné soubory

createDirectory()

parametr recursive se respektuje

zakládá vždy rekurzivně, parametr se ignoruje

removeDirectory()

nad neprázdnou složkou bez true chybuje

maže složku včetně obsahu bez ohledu na parametr

Metadata z list()

plná – včetně owner, group, accessedAt

owner a accessedAt jsou vždy undefined

timeout

respektuje se, výchozí 30000 ms

ignoruje se, platí výchozí hodnota knihovny

Mazání složky nad FTP je destruktivnější, než parametr napovídá – obsah si nejdřív ověřte přes list(). Nad FTP také nepoužívejte exists() pro test existence složky; volejte přímo createDirectory(), které nad existující složkou nechybuje.

Časté chyby

Chyba

Příčina

Řešení

Accessing this source is not allowed! (BANNED_SOURCE)

cesta není v allowedExternalSources

doplnit cestu do TAS_DMS_ALLOWED_EXTERNAL_SOURCES, nebo stahovat do TMP apicall

Invalid VaultSecretRef

do FTP API šlo něco jiného než výstup vault.get()

referenci předávat bez jakékoli úpravy, nepřevádět na string

ENOTFOUND, ECONNREFUSED, ETIMEDOUT

nedostupný host, špatný port, firewall

ověřit host a port a průchodnost ze serveru TASu

File not found (FILE_NOT_FOUND)

source v upload() neodpovídá souboru v DMS

zkontrolovat ID nebo název přílohy

Dms file does not belong to process.

soubor patří jinému případu

pracovat se souborem aktuálního případu

No such file

vzdálená cesta neexistuje

ověřit přes list(), složku založit přes createDirectory()

For FTP/SFTP calls use FtpApi.

v šabloně zůstalo staré volání curl.*

přepsat na ftp.getFtpClientWithVaultPassword()

Chyby se propagují jako výjimka a zapisují se do logu výpočtů. Operace upload a delete se logují na úrovni info, ostatní na trace.

Checklist před nasazením

  • Kód neobsahuje await, async ani Promise.
  • Heslo je v Trezoru a výpočet používá getFtpClientWithVaultPassword(), ne heslo v textu.
  • Výpočet je na úkolu (Task JS), ne na případu.
  • Cílová vzdálená i lokální cesta je v TAS_DMS_ALLOWED_EXTERNAL_SOURCES, nebo jde o TMP apicall.
  • download() má explicitní plnou cestu k cílovému souboru včetně jména.
  • ID případu se čte přes lib.iprocId().
  • U FTP se nespoléhá na exists() pro složky ani na parametr recursive.
  • Operace jsou v try/catch a chyba se propíše do stavové proměnné nebo přes proc.error().
  • U dávek je odhadnutý počet spojení a doba běhu.

Alternativa bez výpočtu: SmartEvent (od v5.19)

Pokud stačí jedna operace bez další logiky, nemusíte psát výpočet – na šablonu úkolu lze navěsit SmartEvent SmartEventFtpRequester. Konfiguruje se formulářem a podporuje stejné operace jako výpočtové API: list, upload, download, delete, exists, createDirectory, removeDirectory, rename. Heslo se v konfiguraci zadává jako {{vault:nazev-polozky}} a vyhodnotí se z Trezoru.

Výpočet volte, když potřebujete rozhodovací logiku, cyklus přes více souborů nebo zápis výsledku do proměnných a DMS.

Frantisek Brych Updated by Frantisek Brych

Contact

Team assistant (opens in a new tab)

Powered by HelpDocs (opens in a new tab)