Skip to main content

Overview

The FTPClient provides methods for navigating the FTP server’s directory structure and managing directories.

Listing directories

Get a list of files and directories in the current or specified directory:
const files = await client.list();
// Returns array of strings: ['file1.txt', 'file2.pdf', 'subdir/']

const filesInDir = await client.list('/path/to/dir');
// Lists contents of specific directory
The list() method returns a simple string array, implemented in src/classes/ftp-client.ts:511:
public async list(dirName?: string): Promise<string[]> {
  await this.lock.lock();
  if (this.conn === undefined) {
    this.lock.unlock();
    throw FTPClient.notInit();
  }
  
  const listing = await this.commandWithData(Commands.PlainList, dirName);
  return listing.trimEnd().split("\r\n");
}

Extended listings

For detailed file information, use extendedList() which returns structured metadata:
const entries = await client.extendedList();

for (const [filename, info] of entries) {
  console.log(filename);
  console.log(`  Size: ${info.size} bytes`);
  console.log(`  Type: ${info.isDirectory ? 'directory' : 'file'}`);
  console.log(`  Modified: ${info.mtime}`);
}
// Simple string array
const files = await client.list();
files.forEach(name => console.log(name));
The extendedList() method uses the MLSD command when available (machine-readable listing), providing detailed metadata. See src/classes/ftp-client.ts:522 for implementation details.

Get current directory

Retrieve the current working directory path:
const currentPath = await client.cwd();
console.log(`Current directory: ${currentPath}`);
The cwd() method executes the FTP PWD (Print Working Directory) command:
public async cwd(): Promise<string> {
  await this.lock.lock();
  if (this.conn === undefined) {
    this.lock.unlock();
    throw FTPClient.notInit();
  }
  const res = await this.command(Commands.PWD);
  this.lock.unlock();
  this.assertStatus(StatusCodes.DirCreated, res);
  const r = Regexes.path.exec(res.message);
  if (r === null) {
    throw { error: "Could not parse server response", ...res };
  }
  return r[1];
}

Change directory

Navigate to a different directory:
await client.chdir('/path/to/directory');

// Relative paths work too
await client.chdir('subdirectory');
await client.chdir('../parent-directory');
The chdir() method is implemented in src/classes/ftp-client.ts:187:
public async chdir(path: string): Promise<void> {
  await this.lock.lock();
  if (this.conn === undefined) {
    this.lock.unlock();
    throw FTPClient.notInit();
  }
  const res = await this.command(Commands.CWD, path);
  this.lock.unlock();
  this.assertStatus(StatusCodes.ActionOK, res);
}

Go up one level

Move to the parent directory:
await client.cdup();
// Equivalent to: await client.chdir('..');
The cdup() method uses the FTP CDUP command, which is specifically designed for moving to the parent directory. See src/classes/ftp-client.ts:201.

Creating directories

Create a new directory on the server:
await client.mkdir('new-directory');

// Create with absolute path
await client.mkdir('/uploads/2026/march');
The mkdir() method is implemented in src/classes/ftp-client.ts:493:
public async mkdir(dirName: string): Promise<boolean> {
  await this.lock.lock();
  if (this.conn === undefined) {
    this.lock.unlock();
    throw FTPClient.notInit();
  }
  
  const res = await this.command(Commands.MKDIR, dirName);
  this.assertStatus(StatusCodes.DirCreated, res);
  
  this.lock.unlock();
  return true;
}
Most FTP servers don’t support creating nested directories in one call. Create parent directories first:
await client.mkdir('uploads');
await client.mkdir('uploads/2026');
await client.mkdir('uploads/2026/march');

Removing directories

Delete an empty directory:
await client.rmdir('old-directory');
The directory must be empty before you can remove it. Delete all files and subdirectories first, or you’ll receive an error.
The rmdir() method is implemented in src/classes/ftp-client.ts:476:
public async rmdir(dirName: string): Promise<void> {
  await this.lock.lock();
  if (this.conn === undefined) {
    this.lock.unlock();
    throw FTPClient.notInit();
  }
  
  const res = await this.command(Commands.RMDIR, dirName);
  this.assertStatus(StatusCodes.ActionOK, res);
  
  this.lock.unlock();
}

Example: Directory navigation

import { FTPClient } from 'workerd-ftp';

const client = new FTPClient('ftp.example.com', {
  user: 'username',
  pass: 'password'
});

await client.connect();

// Get starting directory
const startDir = await client.cwd();
console.log(`Starting in: ${startDir}`);

// Navigate to uploads
await client.chdir('uploads');
console.log(`Now in: ${await client.cwd()}`);

// List files
const files = await client.list();
console.log(`Files: ${files.join(', ')}`);

// Go back to parent
await client.cdup();
console.log(`Back to: ${await client.cwd()}`);

await client.close();

Example: Recursive directory listing

async function listRecursive(
  client: FTPClient,
  path: string = '',
  indent: number = 0
): Promise<void> {
  const entries = await client.extendedList(path);
  
  for (const [name, info] of entries) {
    // Skip . and ..
    if (name === '.' || name === '..') continue;
    
    const prefix = '  '.repeat(indent);
    const fullPath = path ? `${path}/${name}` : name;
    
    if (info.isDirectory) {
      console.log(`${prefix}[DIR]  ${name}/`);
      // Recursively list subdirectory
      await listRecursive(client, fullPath, indent + 1);
    } else {
      console.log(`${prefix}[FILE] ${name} (${info.size} bytes)`);
    }
  }
}

// Usage
await listRecursive(client);

Example: Create nested directories

async function mkdirp(client: FTPClient, path: string): Promise<void> {
  const parts = path.split('/').filter(p => p.length > 0);
  let current = '';
  
  for (const part of parts) {
    current += '/' + part;
    
    try {
      await client.mkdir(current);
      console.log(`Created: ${current}`);
    } catch (error: any) {
      // Directory might already exist (error code 550)
      if (error.code !== 550) {
        throw error;
      }
      console.log(`Already exists: ${current}`);
    }
  }
}

// Usage
await mkdirp(client, '/uploads/2026/march/reports');

Example: Delete directory recursively

async function rmdirRecursive(
  client: FTPClient,
  path: string
): Promise<void> {
  const entries = await client.extendedList(path);
  
  for (const [name, info] of entries) {
    // Skip . and ..
    if (name === '.' || name === '..') continue;
    
    const fullPath = `${path}/${name}`;
    
    if (info.isDirectory) {
      // Recursively delete subdirectory
      await rmdirRecursive(client, fullPath);
      await client.rmdir(fullPath);
      console.log(`Removed directory: ${fullPath}`);
    } else {
      // Delete file
      await client.rm(fullPath);
      console.log(`Deleted file: ${fullPath}`);
    }
  }
}

// Usage
await rmdirRecursive(client, '/old-uploads');
await client.rmdir('/old-uploads');

Example: Complete directory workflow

import { FTPClient } from 'workerd-ftp';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const client = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASS
    });
    
    try {
      await client.connect();
      
      // Create organized directory structure
      const today = new Date();
      const year = today.getFullYear();
      const month = String(today.getMonth() + 1).padStart(2, '0');
      const day = String(today.getDate()).padStart(2, '0');
      
      const uploadPath = `uploads/${year}/${month}`;
      
      // Create directories (skip if exists)
      try {
        await client.mkdir('uploads');
      } catch (e) { /* already exists */ }
      
      try {
        await client.mkdir(`uploads/${year}`);
      } catch (e) { /* already exists */ }
      
      try {
        await client.mkdir(uploadPath);
      } catch (e) { /* already exists */ }
      
      // Navigate to today's directory
      await client.chdir(uploadPath);
      
      // Upload file
      const data = new TextEncoder().encode('Daily report');
      await client.upload(`report-${day}.txt`, data);
      
      // List files in current directory
      const files = await client.list();
      
      return Response.json({
        directory: uploadPath,
        files
      });
    } finally {
      await client.close();
    }
  }
};

Best practices

1

Track your location

Keep track of your current directory, especially when performing multiple operations:
const startDir = await client.cwd();
await client.chdir('work-dir');
// ... perform operations ...
await client.chdir(startDir); // Return to start
2

Use absolute paths

When possible, use absolute paths to avoid confusion:
await client.mkdir('/uploads/2026/march');
3

Handle existing directories

Wrap mkdir() in try-catch to handle already-existing directories gracefully:
try {
  await client.mkdir('directory');
} catch (error: any) {
  if (error.code !== 550) throw error;
}
4

Clean up empty directories

Delete directories you no longer need to keep the server organized:
await client.rmdir('temporary-dir');

Next steps

File operations

Upload, download, and manage files

Streaming

Stream large files efficiently

Build docs developers (and LLMs) love