Skip to main content

Basic Worker example

Here’s a complete Cloudflare Worker that handles FTP operations through HTTP requests:
import { FTPClient } from "workerd-ftp";

export interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASSWORD: string;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    // Create FTP client
    const ftp = new FTPClient(env.FTP_HOST, {
      port: 21,
      user: env.FTP_USER,
      pass: env.FTP_PASSWORD,
      secure: false
    });

    try {
      await ftp.connect();

      if (url.pathname === '/upload' && request.method === 'POST') {
        // Upload a file
        const body = await request.text();
        const fileName = url.searchParams.get('file') || 'upload.txt';
        
        await ftp.upload(fileName, new TextEncoder().encode(body));
        
        return new Response(JSON.stringify({ 
          success: true, 
          message: `File ${fileName} uploaded successfully` 
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/download' && request.method === 'GET') {
        // Download a file
        const fileName = url.searchParams.get('file');
        
        if (!fileName) {
          return new Response('Missing file parameter', { status: 400 });
        }
        
        const data = await ftp.download(fileName);
        
        return new Response(data, {
          headers: { 
            'Content-Type': 'application/octet-stream',
            'Content-Disposition': `attachment; filename="${fileName}"`
          }
        });
      }

      if (url.pathname === '/list' && request.method === 'GET') {
        // List files
        const files = await ftp.list();
        
        return new Response(JSON.stringify({ files }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      return new Response('Not found', { status: 404 });

    } catch (error: any) {
      return new Response(JSON.stringify({ 
        error: error.message,
        code: error.code 
      }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' }
      });
    } finally {
      await ftp.close();
    }
  }
};
This example uses environment variables for FTP credentials. Configure these in your Cloudflare Worker settings or wrangler.toml.

wrangler.toml configuration

Configure your Worker with the necessary environment variables:
name = "ftp-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
node_compat = true

[vars]
FTP_HOST = "ftp.example.com"

# Use secrets for sensitive data
# Set with: wrangler secret put FTP_USER
# Set with: wrangler secret put FTP_PASSWORD
Never commit FTP passwords to your repository. Use wrangler secret put to set sensitive environment variables.

File upload endpoint

Create a dedicated endpoint for uploading files with multipart form data:
import { FTPClient } from "workerd-ftp";

export interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASSWORD: string;
}

async function handleUpload(request: Request, env: Env): Promise<Response> {
  // Parse form data
  const formData = await request.formData();
  const file = formData.get('file');
  const targetPath = formData.get('path') as string || '/';

  if (!file || !(file instanceof File)) {
    return new Response('No file provided', { status: 400 });
  }

  const ftp = new FTPClient(env.FTP_HOST, {
    user: env.FTP_USER,
    pass: env.FTP_PASSWORD
  });

  try {
    await ftp.connect();

    // Change to target directory if specified
    if (targetPath !== '/') {
      await ftp.chdir(targetPath);
    }

    // Upload the file
    const arrayBuffer = await file.arrayBuffer();
    const data = new Uint8Array(arrayBuffer);
    await ftp.upload(file.name, data);

    // Verify upload
    const size = await ftp.size(file.name);

    return new Response(JSON.stringify({
      success: true,
      fileName: file.name,
      size: size,
      path: targetPath
    }), {
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error: any) {
    return new Response(JSON.stringify({
      error: error.message,
      code: error.code
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  } finally {
    await ftp.close();
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === 'POST' && new URL(request.url).pathname === '/upload') {
      return handleUpload(request, env);
    }
    return new Response('Method not allowed', { status: 405 });
  }
};

File browser API

Build a complete file browser API with directory navigation and file information:
import { FTPClient } from "workerd-ftp";

export interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASSWORD: string;
}

interface FileInfo {
  name: string;
  size: number;
  isDirectory: boolean;
  isFile: boolean;
  modified?: Date;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const path = url.searchParams.get('path') || '/';

    const ftp = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASSWORD
    });

    try {
      await ftp.connect();

      if (url.pathname === '/browse') {
        // Navigate to directory
        if (path !== '/') {
          await ftp.chdir(path);
        }

        // Get current directory
        const cwd = await ftp.cwd();

        // Get extended listing with file info
        const entries = await ftp.extendedList();
        
        const files: FileInfo[] = entries.map(([name, stat]) => ({
          name,
          size: stat.size,
          isDirectory: stat.isDirectory,
          isFile: stat.isFile,
          modified: stat.mtime || undefined
        }));

        return new Response(JSON.stringify({
          currentPath: cwd,
          files: files
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/stat') {
        const fileName = url.searchParams.get('file');
        
        if (!fileName) {
          return new Response('Missing file parameter', { status: 400 });
        }

        const stat = await ftp.stat(fileName);

        return new Response(JSON.stringify({
          name: fileName,
          size: stat.size,
          isDirectory: stat.isDirectory,
          isFile: stat.isFile,
          modified: stat.mtime,
          created: stat.ctime
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/mkdir' && request.method === 'POST') {
        const dirName = url.searchParams.get('name');
        
        if (!dirName) {
          return new Response('Missing name parameter', { status: 400 });
        }

        await ftp.mkdir(dirName);

        return new Response(JSON.stringify({
          success: true,
          message: `Directory ${dirName} created`
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/delete' && request.method === 'DELETE') {
        const fileName = url.searchParams.get('file');
        const isDir = url.searchParams.get('isDirectory') === 'true';
        
        if (!fileName) {
          return new Response('Missing file parameter', { status: 400 });
        }

        if (isDir) {
          await ftp.rmdir(fileName);
        } else {
          await ftp.rm(fileName);
        }

        return new Response(JSON.stringify({
          success: true,
          message: `${isDir ? 'Directory' : 'File'} ${fileName} deleted`
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/rename' && request.method === 'POST') {
        const from = url.searchParams.get('from');
        const to = url.searchParams.get('to');
        
        if (!from || !to) {
          return new Response('Missing from or to parameter', { status: 400 });
        }

        await ftp.rename(from, to);

        return new Response(JSON.stringify({
          success: true,
          message: `Renamed ${from} to ${to}`
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      return new Response('Not found', { status: 404 });

    } catch (error: any) {
      return new Response(JSON.stringify({
        error: error.message,
        code: error.code
      }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' }
      });
    } finally {
      await ftp.close();
    }
  }
};

Streaming large files

Handle large file uploads and downloads efficiently using streams:
import { FTPClient } from "workerd-ftp";

export interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASSWORD: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const fileName = url.searchParams.get('file');

    if (!fileName) {
      return new Response('Missing file parameter', { status: 400 });
    }

    const ftp = new FTPClient(env.FTP_HOST, {
      user: env.FTP_USER,
      pass: env.FTP_PASSWORD
    });

    try {
      await ftp.connect();

      if (url.pathname === '/stream-download') {
        // Stream file download to client
        const readable = await ftp.downloadReadable(fileName);

        // Return the stream directly to the client
        // The stream will be consumed by the response
        const response = new Response(readable, {
          headers: {
            'Content-Type': 'application/octet-stream',
            'Content-Disposition': `attachment; filename="${fileName}"`
          }
        });

        // Clean up after the response is sent
        // Note: In a real scenario, you'd want to handle this more carefully
        setTimeout(async () => {
          await ftp.finalizeStream();
          await ftp.close();
        }, 100);

        return response;
      }

      if (url.pathname === '/stream-upload' && request.method === 'POST') {
        // Get content length for allocation
        const contentLength = request.headers.get('content-length');
        const allocate = contentLength ? parseInt(contentLength) : undefined;

        // Get writable stream
        const writable = await ftp.uploadWritable(fileName, allocate);

        // Pipe request body to FTP
        if (request.body) {
          await request.body.pipeTo(writable);
        }

        await ftp.finalizeStream();

        return new Response(JSON.stringify({
          success: true,
          message: `File ${fileName} uploaded via stream`
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      return new Response('Not found', { status: 404 });

    } catch (error: any) {
      return new Response(JSON.stringify({
        error: error.message,
        code: error.code
      }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' }
      });
    } finally {
      // Note: For streaming responses, cleanup timing is critical
      // Consider using ctx.waitUntil() for proper cleanup
    }
  }
};
Use ctx.waitUntil() in Cloudflare Workers to ensure cleanup tasks complete even after the response is sent.

Secure FTPS Worker

Example using FTPS (FTP over TLS) for secure connections:
import { FTPClient } from "workerd-ftp";

export interface Env {
  FTP_HOST: string;
  FTP_USER: string;
  FTP_PASSWORD: string;
  FTP_SECURE: string; // "true" or "false"
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Create secure FTP client
    const ftp = new FTPClient(env.FTP_HOST, {
      port: 21,
      user: env.FTP_USER,
      pass: env.FTP_PASSWORD,
      secure: env.FTP_SECURE === 'true' // Enable FTPS with TLS
    });

    try {
      await ftp.connect();
      console.log('Secure connection established');

      if (url.pathname === '/secure-upload' && request.method === 'POST') {
        const body = await request.text();
        const fileName = url.searchParams.get('file') || 'secure.txt';
        
        await ftp.upload(fileName, new TextEncoder().encode(body));
        
        return new Response(JSON.stringify({ 
          success: true, 
          message: `File ${fileName} uploaded securely via FTPS` 
        }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      return new Response('Not found', { status: 404 });

    } catch (error: any) {
      console.error('FTP error:', error);
      return new Response(JSON.stringify({ 
        error: error.message,
        code: error.code 
      }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' }
      });
    } finally {
      await ftp.close();
    }
  }
};
The library uses Cloudflare’s TCP Socket API with STARTTLS support. Set secure: true to enable FTPS (FTP over TLS).

Build docs developers (and LLMs) love