Newton's Poleless Magnet
Join us to discuss the challenges, solutions and best practices for in-house JavaScript code sharing. Tuesday, 12/17 at 10am PT/1pm ET.Sign up here »


2.1.5 • Public • Published


Travis NPM NPM Discord Github

Qusly-core is an API wrapper around ssh2 and basic-ftp for building FTP/FTPS/SFTP clients. It's used in Qusly.


  • Supports FTP, FTPS, SFTP
  • Promises
  • Lots of utilities like createBlank
  • Splited transfer
  • Automatically calculates ETA and transfer speed
  • MS-dos support

Checkout roadmap to see what's coming.


$ npm install qusly-core

Quick start

An example of listing files:

import { Client } from 'qusly-core';
async function init() {
  const client = new Client();
  await client.connect({
    host: '',
    user: 'root',
    password: 'password'
    protocol: 'sftp',
    port: 22,
  const files = await client.readDir('/documents');
  await client.disconnect();

Example output:

    name: 'projects',
    type: 'directory',
    size: 4096,
    ext: ''
    user: 'root',
    group: 'root',
    date: '2019-05-10T18:52:00.000Z', // obj Date
    permissions: {
      user: 6,
      group: 6
    name: 'logs.txt',
    type: 'file',
    ext: 'txt'
    size: 43,
    user: 'root',
    group: 'root',
    date: '2019-05-29T22:00:00.000Z', // obj Date
    permissions: {
      user: 6,
      group: 6


Class Client:

Class TransferClient:




Class Client


  • Client.abort(): Promise<void>
    Aborts current data transfer. It closes all used file streams.

    const bytes = await client.abort();
    console.log(`Aborted at ${bytes} bytes`);

  • Client.connect(config: IConfig): Promise<void>
    Connects to server. You can use it to reload session.

    try {
      await client.connect({
        host: '',
        user: 'root', // default anonymous
        password: 'password', // default @anonymous
        protocol: 'ftp', // default ftp
        port: 21, // default 21
    } catch (error) {
      console.log('Failed to connect!', res.error);

  • Client.createBlank(type: 'folder' | 'file', path = './', files?: IFile[]): Promise<string>
    Creates an empty folder or file with unique name. If you've fetched files already, you can provide last argument to don't refetch files.

    const res = await client.createBlank('folder');
    console.log(`Created new folder - ${res.fileName}`);

  • Client.delete(path: string): Promise<void>
    An universal method to remove both files and folders.

    await client.delete('folder');

  • Client.disconnect(): Promise<void>
    Disconnects from server. Closes all opened sockets and file streams.

    await client.disconnect();

  • string, dest: Writable, options?: IDownloadOptions): Promise<void>
    Downloads a file. You can start at given offset by setting options to

      startAt: 65.536;

    import { createWriteStream } from 'fs';
    import { resolve } from 'path';
    const localPath = resolve('downloads', 'downloaded.rar');
    client.on('progress', e => {
      const rate = ((e.buffered / e.size) * 100).toFixed(2);
      console.log(`${rate}% ETA: ${e.eta}s`);
    await'file.rar', createWriteStream(localPath));

  • Client.exists(path: string): Promise<boolean>
    Checks if file exists.

    const exists = await client.exists('/home/image.png');
    if (exists) {
      console.log('File exists');
    } else {
      console.log("File doesn't exists");

  • Client.mkdir(path: string): Promise<void>
    Creates a directory.

    await client.mkdir('/home/documents/new folder');
    console.log(`Created a new directory`);

  • Client.move(srcPath: string, destPath: string): Promise<void>
    Moves a file from srcPath to destPath.

    await client.move('music/film.mp4', 'videos/film.mp4');

  • Client.pwd(): Promise<IPwdRes>
    Returns path of current working directory.

    const path = await client.pwd();
    console.log(`You're at ${path}`);

  • Client.readDir(path?: string): Promise<IFile[]>
    Reads content of a directory. If you don't provide path, it'll use working directory.

    const files = await client.readDir('/root/');

  • Client.rimraf(path: string): Promise<void>
    Removes a directory and all of its content, recursively.

    await client.rimraf('videos');
    console.log('Removed all files');

  • Client.send(command: string): Promise<string>
    Sends a raw command. Output depends on a protocol and server support!

    // It'll probably work on SFTP
    const res = await client.send('whoami');

  • Client.size(path: string): Promise<number>
    Returns size of a file or folder in bytes.

    const size = await client.size('file.rar');
    console.log(`Size: ${size} bytes`);

  • Client.stat(path: string): Promise<IStats>
    Returns info about file at given path.

    const res = await client.stat('/documents/unknown');
    console.log(`${res.type} - ${res.size} bytes`);

  • Client.touch(path: string): Promise<IRes>
    Creates an empty file.

    await client.touch('./empty file.txt');
    console.log('Created a new file!');

  • Client.unlink(path: string): Promise<IRes>
    Removes a file at path.

    await client.unlink('videos/file.mp4');
    console.log('Removed a file');

  • Client.upload(path: string, source: Readable, options?: ITransferOptions): Promise<void>
    Uploads a file.

    import { createReadStream, statSync } from 'fs';
    import { resolve } from 'path';
    const localPath = resolve('uploads', 'file.jpg');
    const fileSize = statSync(localPath).size;
    client.on('progress', e => {
      const rate = ((e.buffered / e.size) * 100).toFixed(2);
      console.log(`${rate}% ETA: ${e.eta}s`);
    await client.upload('uploaded file.jpg', createReadStream(path));

Class TransferClient

An utility class to split transfers.


  • TransferClient(type: ITransferType, splits = 1)

  • TransferClient.connect(config: IConfig): Promise<void>
    Connects clients to server.

    await client.connect({
      host: '',
    console.log(`All clients are connected!`);

  • TransferClient.getSplits(): number
    Gets splits length.

  • TransferClient.setSplits(count: number, config?: IConfig): Promise<void>
    Sets splits. If you're setting more than you had, you must provide config. If you're setting less than you had, it will automatically close rest of client.

    const client = new TransferClient('download', 2);
    console.log(client.getSplits()); // 2
    await client.setSplits(6, {
      host: '',
    console.log(client.getSplits()); // 6
    await client.setSplits(4);
    console.log(client.getSplits()); // 4

  • TransferClient.transfer(localPath: string, remotePath: string, id?: string): Promise<void>
    Transfers a file. You can set your own id or it'll be unique hash. To track progress, use event progress. With 2 splits, you can transfer files twice as fast.

    await client.transfer('logs.txt', '/documents/logs.txt');
    await client.transfer('video.mp4', '/content/video.mp4');

Interface IConfig

interface IConfig {
  protocol?: IProtocol;
  host?: string;
  port?: number;
  user?: string;
  password?: string;

Interface IDownloadOptions

interface IDownloadOptions extends ITransferOptions {
  startAt?: number;

Interface IFile

interface IFile {
  name?: string;
  type?: IFileType;
  size?: number;
  user?: string;
  group?: string;
  date?: Date;
  ext?: string;
  permissions?: {
    user?: number;
    group?: number;

Interface IProgress

interface IProgress {
  chunkSize?: number; // single chunk size in bytes
  buffered?: number; // already buffered size in bytes
  size?: number; // file size in bytes
  localPath?: string; // local file path
  remotePath?: string; // remote file path
  eta?: number; // estimated time arrival in seconds
  speed?: number; // transfer speed in bytes/s
  startAt?: Date; // transfer start
  percent?: number; // percent of buffered size
  context?: Client;

Interface IStats

interface IStats {
  size?: number;
  type?: IFileType;

Interface ITransferClientNew

interface ITransferClientNew {
  id?: string;
  type?: ITransferType;
  localPath?: string;
  remotePath?: string;
  context?: Client;

Interface ITransferClientProgress

interface ITransferClientProgress extends IProgress {
  id?: string;
  type?: ITransferType;

Interface ITransferOptions

interface ITransferOptions {
  quiet?: boolean;

Type IFileType

type IFileType = 'unknown' | 'file' | 'folder' | 'symbolic-link';

Type IProtocol

type IProtocol = 'sftp' | 'ftp' | 'ftps';

Type ITransferType

type ITransferType = 'download' | 'upload';



  • Client.on('abort') - File transfer has been aborted.

  • Client.on('connect') - Client has connected to server.

  • Client.on('disconnect') - Client has disconnected from server.

  • Client.on('progress', e: IProgress) - Triggered while transfering a file.

    client.on('progress', (e: IProgress) => {
      const { buffered, size, eta, speed } = data;
      const percent = (bytes / size * 100).toFixed(2);
      console.log(`${buffered}/${size}, ETA: ${eta}s, speed: ${speed}KB/s`);


  • TransferClient.on('new', e: ITransferClientNew) - Invoked on every new file transfer.

  • TransferClient.on('progress', e: ITransferClientProgress) - Triggered while transfering a file. This event comes with a lot of information. Some of that are:
    • Id, which you can use to identify transfer.
    • Type of transfer ITransferType
    • Single chunk size in bytes
    • Buffered size
    • File size
    • Estimated time arrival in seconds
    • Transfer speed in bytes/s
    • Percent of buffered size
    • Start time
client.on('progress', (e: ITransferClientProgress) => {
  const { id, type, eta } = data;
  console.log(`${id}${eta}s (${type}`);


  • Qusly - Elegant, full-featured FTP client.


npm i qusly-core

DownloadsWeekly Downloads






Unpacked Size

361 kB

Total Files


Last publish


  • avatar