Theme
Remote transfer clients Preview
Five client objects, one interface. Learn one and you know all five; only the constructor and the connection fields differ.
| Constructor | Reaches |
|---|---|
new SftpClient() | Any SFTP over SSH server |
new FtpsClient() | Plain FTP, explicit FTPS, implicit FTPS |
new S3Client() | Amazon S3 and S3 compatible services such as MinIO, Wasabi and Backblaze B2 |
new AzureClient() | Azure Blob Storage |
new GCSClient() | Google Cloud Storage |
This is not the same thing as a virtual file system
A remote client transfers files between the Connector host's local disk and a remote system. It does not read or write a virtual file system, and your users never see it.
To move a file from storage a user can see out to a partner's SFTP server, you need both: ExportFile to get it onto local disk, then a client to push it. Alternatively use MoveToVFS, which goes virtual file system to virtual file system with no local staging at all, and is usually the better answer.
The shape of every script that uses one
js
var cli = new SftpClient();
cli.Host = "sftp.partner.com:22";
cli.User = "acme";
cli.Pass = partnerPassword;
cli.HostKeySHA256 = "SHA256:8ZQx1n2Rk8yWv4hQ7LdT3mFbC5aJ9pXsYuOeR6iKgNc";
if (!cli.Connect()) {
Log.Error("could not connect to the partner SFTP server");
Exit(1);
}
var ok = cli.Upload("/var/outbox/*.csv", "/incoming");
cli.Close();
if (!ok) {
Log.Error("one or more files failed to upload");
}Swap the constructor and the connection fields for any other protocol and the rest is unchanged.
Connection fields
SftpClient
js
var cli = new SftpClient();
cli.Host = "sftp.example.com:22"; // port defaults to 22
cli.User = "alice";
cli.Pass = password; // ignored when KeyFile is set
cli.KeyFile = "/keys/id_ed25519"; // PEM or PuTTY .ppk
cli.KeyFilePass = keyPassphrase;
cli.PrivateKeyPEM = pemText; // inline key, takes precedence over KeyFile
cli.HostID = "partner-sftp"; // a label for log lines only
cli.HostKeySHA256 = "SHA256:base64..."; // recommended, matches ssh-keygen -E sha256
cli.HostKeySHA1 = "SHA1:base64...";
cli.HostKeyMD5 = "aa:bb:cc:...";Host key verification is on, and turning it off is a real decision
Set exactly one of HostKeySHA256, HostKeySHA1 or HostKeyMD5. With none of them set, Connect() is refused: an unauthenticated SFTP connection is not something this client will make by accident.
cli.InsecureSkipVerify = true disables the check entirely, and then anyone who can intercept the connection can impersonate the server, take the credential you are about to send, and read every file you transfer. Use it in a lab and nowhere else.
Get the fingerprint from the partner, out of band, and paste it into the script:
ssh-keyscan -t ed25519 sftp.partner.com | ssh-keygen -lf -FtpsClient
js
var cli = new FtpsClient();
cli.Host = "ftp.example.com:21";
cli.User = "alice";
cli.Pass = password;
cli.TLSMode = TLS_MODE_EXPLICIT; // TLS_MODE_NONE | TLS_MODE_EXPLICIT | TLS_MODE_IMPLICIT
cli.InsecureSkipVerify = false; // accepts any certificate when true| Constant | Meaning |
|---|---|
TLS_MODE_NONE | Plain FTP. No encryption at all |
TLS_MODE_EXPLICIT | Starts plain and upgrades with AUTH TLS. The usual choice |
TLS_MODE_IMPLICIT | TLS from the first byte, normally port 990 |
The field is InsecureSkipVerify, not TLSInsecure
Older Syncplify Server examples use cli.TLSInsecure. That field does not exist here, and assigning to it does nothing at all: certificate verification stays on, and a self signed server keeps being refused, with no clue as to why.
SetMode() switches the FTP transfer type after connecting. Binary is the default and is correct for everything except plain text where you want line ending translation:
js
if (cli.Connect()) {
cli.SetMode(FTP_MODE_A); // ASCII; FTP_MODE_I is binary and the default
cli.Upload("/var/outbox/*.txt", "/incoming");
cli.Close();
}S3Client
js
var cli = new S3Client();
cli.Region = "eu-west-1";
cli.AccessKey = accessKey;
cli.SecretKey = secretKey;
cli.Bucket = "acme-archive";
cli.Endpoint = ""; // set for MinIO, Wasabi, Backblaze and friends
cli.UsePathStyle = false; // true for most S3 compatible services
cli.EncryptionPass = ""; // optional server side encryption passphraseWith AccessKey and SecretKey both empty, the standard AWS credential chain is used: environment variables, the shared credentials file, an instance role. On a Connector running in EC2 that is usually the cleanest option, because no key ends up in the script.
js
cli.Endpoint = "https://s3.us-west-000.backblazeb2.com";
cli.UsePathStyle = true;AzureClient
js
var cli = new AzureClient();
cli.AccountName = "mystorageaccount";
cli.AccountKey = accountKey; // base64 shared key
cli.SASToken = ""; // an alternative to AccountKey
cli.Container = "my-container";
cli.ContainerPath = ""; // optional prefix inside the container
cli.ServiceURL = ""; // override for Azurite or a sovereign cloud
cli.EncryptionPass = "";A SAS token scoped to one container, with only the permissions the script needs and a real expiry, is much safer than an account key, which is full control of the whole storage account.
GCSClient
js
var cli = new GCSClient();
cli.Bucket = "acme-gcs-bucket";
cli.BucketPath = ""; // optional prefix
cli.ProjectID = "";
cli.CredentialsFile = "/keys/service-account.json"; // omit for Application Default Credentials
cli.EncryptionPass = "";Configure
Every client also accepts a URL plus the same JSON payload shape that new VirtualFS uses, which is handy when the same connection details are already written down somewhere:
js
var cli = new SftpClient().Configure(
"sftp://alice@sftp.partner.com:22?hostKeySHA256=SHA256:8ZQx...",
JSON.stringify({ private_key: pemText })
);Credentials belong in the payload, not the URL. For S3Client, only region, endpoint and pathStyle are read from the URL; concurrency and TLS options there are ignored, so set those as fields.
Transfers
js
Upload(what, toWhere): boolean; UploadR(what, toWhere): boolean;
Download(what, toWhere): boolean; DownloadR(what, toWhere): boolean;
UploadWithPath(what, toWhere, skip): boolean; UploadWithPathR(...): boolean;
DownloadWithPath(what, toWhere, skip): boolean; DownloadWithPathR(...): boolean;what takes a glob. The R suffix means recursive. WithPath keeps the directory structure but drops skip leading path components:
js
// Local file: /data/reports/2026/q1/summary.csv
// skip = 2 removes "/data/reports"
// Remote path: /inbox/2026/q1/summary.csv
cli.UploadWithPathR("/data/reports/**/*.csv", "/inbox", 2);Every transfer method returns true only when every matching file transferred. A false means at least one failed, and which one is in the Connector log.
Directory and file operations
js
ListDir(dir, types): DirListItem[]; ListDirR(dir, types): DirListItem[];
ListFiles(dir, mask): DirListItem[]; ListFilesR(dir, mask): DirListItem[];
MakeDir(dir): boolean; RenDir(what, toWhere): boolean;
DelDir(dir): boolean; DelTree(dir): boolean;
Stat(obj): DirListItem | null; Rename(what, toWhere): boolean;
Delete(what): boolean;types is LIST_ALL, LIST_FILES or LIST_DIRS. Stat returns null when the object does not exist.
js
var entries = cli.ListFiles("/incoming", "*.csv");
entries.forEach(function (e) {
Log.Info(e.Name + " " + e.Size + " bytes");
});Object storage has no real folders
S3, Azure Blob and Google Cloud Storage emulate directories with key prefixes. RenDir and Rename there are a copy followed by a delete, not an atomic operation, so they are slow on large trees and can leave both copies behind if the delete fails.
Transfer options
Every client exposes Options:
js
cli.Options.UploadPolicy = AlwaysOverwrite;
cli.Options.UploadWithTempName = true;
cli.Options.DeleteSourceAfterUpload = true;Overwrite policy
| Constant | When the destination already exists |
|---|---|
NeverOverwrite | Skip the file. The default |
AlwaysOverwrite | Replace it |
OverwriteIfDiffSize | Replace it only when the sizes differ |
OverwriteIfNewer | Replace it only when the source is newer |
Set with Options.UploadPolicy and Options.DownloadPolicy. The default surprises people: a file that is already there is silently skipped, and the transfer still reports success.
Transfer behavior
| Field | Default | Effect |
|---|---|---|
StopOnTransferError | false | Abandon the whole batch on the first failure |
AdjustTimeOnDownload | true | Give the local copy the remote file's modification time |
AdjustTimeOnUpload | true | Give the remote copy the local file's modification time |
DownloadWithTempName | false | Download to name.tempfile and rename on success |
UploadWithTempName | false | Upload to name.tempfile and rename on success |
DeleteSourceAfterDownload | false | Makes a download a move |
DeleteSourceAfterUpload | false | Makes an upload a move |
OnDownloadGrantTo | "" | POSIX user to chown downloads to. Linux only, and only when the Connector runs as root |
ConnectTimeoutSeconds | 30 | Dial timeout. 0 means no timeout. Ignored by S3, Azure and GCS, which manage their own |
Temp names are worth turning on
Without UploadWithTempName, whatever is watching the destination sees the file appear at zero bytes and grow. Anything that picks files up by name will grab a half written one. With it on, the final name only ever appears when the content is complete. The same argument applies to DownloadWithTempName.
Versioning
| Field | Default | Effect |
|---|---|---|
VersionedUpload, VersionedDownload | false | Move the existing file into a .ver/ subfolder before overwriting |
VersionsToKeepRemote, VersionsToKeepLocal | 3 | How many old copies to retain before pruning |
On the fly encryption
| Field | Default | Effect |
|---|---|---|
OTFE | false | Encrypt on upload, decrypt on download |
OTFEAlgorithm | OTFEAlgoAES256GCM | Or OTFEAlgoOpenPGP |
OTFEKey | "" | The passphrase |
The remote side only ever holds ciphertext; the local side only ever sees plaintext. It is a good fit for pushing backups into a bucket you do not fully trust.
js
cli.Options.OTFE = true;
cli.Options.OTFEAlgorithm = OTFEAlgoAES256GCM;
cli.Options.OTFEKey = archivePassphrase;Lose the OTFE key and the data is gone
Nothing else can decrypt it. There is no recovery path, no escrow and no Syncplify held copy. Record the passphrase somewhere durable before you start writing encrypted data, and remember that it lives in your script source where every Connector administrator can read it.
Always close
js
if (cli.Connect()) {
// work
cli.Close();
}Connect() returns false rather than throwing, so an unchecked call quietly does nothing and every subsequent operation fails for a reason that never appears.
A remote transfer does not belong on a before handler
Connecting to another company's server and pushing files takes seconds at best. On a before handler that is time added to somebody's upload, and on a fail closed handler an unreachable partner means your users cannot transfer anything. Put remote transfers on an after handler marked Run in the background, with a generous timeout.