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 } ` );
}
Plain listing
Extended listing
Filter by type
// Simple string array
const files = await client . list ();
files . forEach ( name => console . log ( name ));
// Returns [filename, FTPFileInfo] tuples
const entries = await client . extendedList ( '/uploads' );
for ( const [ name , info ] of entries ) {
if ( info . isDirectory ) {
console . log ( `[DIR] ${ name } ` );
} else {
console . log ( `[FILE] ${ name } ( ${ info . size } bytes)` );
}
}
const entries = await client . extendedList ();
// Get only files
const files = entries . filter (([ _ , info ]) => info . isFile );
// Get only directories
const dirs = entries . filter (([ _ , info ]) => info . isDirectory );
// Get files larger than 1MB
const large = entries . filter (([ _ , info ]) =>
info . isFile && info . size > 1024 * 1024
);
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.
Navigating directories
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
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
Use absolute paths
When possible, use absolute paths to avoid confusion: await client . mkdir ( '/uploads/2026/march' );
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 ;
}
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