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).