Ploy
Ploy
Features

File Storage

Store and retrieve files with an S3/R2-like API.

File Storage

Ploy File Storage provides an S3/R2-like object store for your workers. It is designed for storing and retrieving file content such as user uploads, images, JSON documents, and any binary or text data. Unlike state and cache which store simple string values, file storage supports content types, file listing, and is optimized for larger payloads.

Configuration

Add a file storage binding in your ploy.yaml:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist
fs:
  FILES: default

The key (FILES) is the binding name available in your worker's env. The value (default) is the file storage identifier.

Run ploy types to generate TypeScript types:

env.d.ts
import type { FileStorageBinding } from "@meetploy/types";

export interface Env {
	FILES: FileStorageBinding;
}

Basic Example

src/index.ts
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (url.pathname === "/upload" && request.method === "POST") {
			const body = await request.text();
			await env.FILES.put("hello.txt", body, { contentType: "text/plain" });
			return Response.json({ success: true });
		}

		if (url.pathname === "/download") {
			const file = await env.FILES.get("hello.txt");
			if (!file) {
				return new Response("Not Found", { status: 404 });
			}
			return new Response(file.body, {
				headers: { "Content-Type": file.contentType },
			});
		}

		if (url.pathname === "/delete") {
			await env.FILES.delete("hello.txt");
			return Response.json({ success: true });
		}

		return new Response("File Storage Worker");
	},
} satisfies Ploy;

API Methods

put(key, value, options?) - Store a File

Stores a file with the given key. Overwrites any existing file at the same key. The optional options parameter lets you specify a content type.

await env.FILES.put("report.pdf", pdfBuffer, {
	contentType: "application/pdf",
});

// Content type defaults to "application/octet-stream" if not specified
await env.FILES.put("data.bin", binaryData);

get(key) - Retrieve a File

Returns the file object or null if the key doesn't exist. The returned object includes body (the file content) and contentType.

const file = await env.FILES.get("report.pdf");
// file is { body: ReadableStream | string, contentType: string } | null

if (file) {
	console.log(file.contentType); // "application/pdf"
	// Use file.body for the content
}

delete(key) - Delete a File

Removes a file from the store.

await env.FILES.delete("report.pdf");

list(options?) - List Files

Returns a list of keys in the store. Use the optional prefix parameter to filter results.

// List all files
const allFiles = await env.FILES.list();
// allFiles is { keys: string[] }

// List files under a prefix
const userFiles = await env.FILES.list({ prefix: "uploads/user-123/" });

File storage supports any content type. Use the contentType option in put() to ensure files are served with the correct MIME type when retrieved.

Common Patterns

Storing User Uploads

async function handleUpload(
	request: Request,
	env: PloyEnv,
	userId: string,
): Promise<Response> {
	const contentType =
		request.headers.get("Content-Type") || "application/octet-stream";
	const filename = request.headers.get("X-Filename") || "file";
	const key = `uploads/${userId}/${Date.now()}-${filename}`;

	const body = await request.arrayBuffer();
	await env.FILES.put(key, body, { contentType });

	return Response.json({ key });
}

Storing JSON Documents

// Store a JSON document
const config = { theme: "dark", layout: "grid", version: 2 };
await env.FILES.put("config/app.json", JSON.stringify(config), {
	contentType: "application/json",
});

// Retrieve and parse
const file = await env.FILES.get("config/app.json");
if (file) {
	const config = JSON.parse(file.body);
}

Organizing Files with Prefixes

// Store files under organized prefixes
await env.FILES.put("images/avatars/user-123.png", avatarData, {
	contentType: "image/png",
});
await env.FILES.put("images/avatars/user-456.png", avatarData, {
	contentType: "image/png",
});
await env.FILES.put("documents/invoices/inv-001.pdf", invoiceData, {
	contentType: "application/pdf",
});

// List all avatars
const avatars = await env.FILES.list({ prefix: "images/avatars/" });
// avatars.keys = ["images/avatars/user-123.png", "images/avatars/user-456.png"]

// List all documents
const docs = await env.FILES.list({ prefix: "documents/" });

Serving Files with Correct Headers

async function serveFile(env: PloyEnv, key: string): Promise<Response> {
	const file = await env.FILES.get(key);
	if (!file) {
		return new Response("Not Found", { status: 404 });
	}

	return new Response(file.body, {
		headers: {
			"Content-Type": file.contentType,
			"Cache-Control": "public, max-age=3600",
		},
	});
}

Multiple File Storage Bindings

You can configure multiple file stores for different use cases:

ploy.yaml
fs:
  FILES: default
  MEDIA: media
// General file storage
await env.FILES.put("data/export.csv", csvContent, {
	contentType: "text/csv",
});

// Media-specific storage
await env.MEDIA.put("images/hero.jpg", imageData, {
	contentType: "image/jpeg",
});

Differences from State and Cache

File StorageStateCache
Designed forFile content (binary/text)Simple key-value stringsTemporary key-value strings
Content typeSupports contentType metadataStrings onlyStrings only
Listinglist() with prefix filteringNo listingNo listing
TTLNone (persists until deleted)None (persists until deleted)Required on every set()
Use caseUploads, documents, images, exportsPreferences, flags, countersSessions, rate limiting, caching

Next Steps

  • State - Add durable key-value storage
  • Cache - Add temporary key-value caching with TTL
  • Databases - Add persistent SQLite databases
  • Workers - Learn about Ploy workers

How is this guide?

Last updated on