Disk Storage
Universal file storage abstraction for Node.js and Bun. Write once, store anywhere — filesystem, S3, Azure Blob, or any cloud provider.
Standalone package —
@minimajs/diskhas no dependency on the rest of the minimajs framework. Use it in any Node.js or Bun project, with Express, Fastify, Hono, or no HTTP framework at all.
Features
- 🌐 Universal API - Same interface for all storage providers
- 📦 Multiple Drivers - Filesystem, S3, Azure, Memory, and more
- 🔀 Protocol Routing - Route to different drivers based on URL prefixes
- 🚀 Streaming - Efficient stream-based operations
- 🔒 Type Safe - Full TypeScript support
- 🧩 Plugins -
storeAs,partition,atomicWrite,checksum,uploadProgress,downloadProgress,compression,encryptionand more - 🌍 Web Standards - Works with
File,Blob, andReadableStreamnatively
Installation
npm install @minimajs/disk
# or
bun add @minimajs/diskFor cloud providers:
npm install @minimajs/aws-s3
npm install @minimajs/azure-blobQuick Start
Unified API Tour
import { createDisk, createTempDisk } from "@minimajs/disk";
import { createS3Driver } from "@minimajs/aws-s3";
import { createAzureBlobDriver } from "@minimajs/azure-blob";
import { encryption, partition, storeAs } from "@minimajs/disk/plugins";
import { createFsDriver } from "@minimajs/disk/adapters";
// Local disk (default driver)
const localDisk = createDisk();
await localDisk.put("hello.txt", "hello world"); // -> File
await localDisk.get("hello.txt"); // -> File | null
// Ephemeral temp disk for short-lived artifacts
const tempDisk = createTempDisk();
await tempDisk.put("preview.txt", "temporary data");
// Encrypted S3 storage
const s3Disk = createDisk({ driver: createS3Driver({}) }, encryption({ password: "abc" }));
const report = new File(["abc file data"], "abc.txt", { type: "text/plain" });
const encryptedFile = await s3Disk.put(report); // encrypted before upload
encryptedFile.href; // s3://bucket/abc.txt
encryptedFile.type; // text/plain
// Date-partitioned local filesystem storage
const fsDisk = createDisk({ driver: createFsDriver({ root: `file://${process.cwd()}/` }) }, partition({ by: "date" }));
const partitionedFile = await fsDisk.put(new File(["abc"], "abc.txt"));
partitionedFile.href; // file://${process.cwd()}/2026/03/01/abc.txt
// Azure Blob storage with UUID filenames
const azureDisk = createDisk({ driver: createAzureBlobDriver({}) }, storeAs("uuid"));
const publicFile = await azureDisk.put(new File(["abc"], "abc.txt"));
publicFile.href; // https://container.../f47ac10b-58cc-4372-a567-0e02b2c3d479.txt
// Stream-first file handling (no full in-memory loading)
const movie = await localDisk.get("movie.mp4");
if (movie) {
movie instanceof File; // true
movie.stream(); // readable stream
await fsDisk.put(movie); // copy between disks efficiently
await movie.bytes(); // Uint8Array<ArrayBuffer>
movie.size; // file size in bytes
}Multi-Plugin Pipeline
import { createDisk } from "@minimajs/disk";
import { createFsDriver } from "@minimajs/disk/adapters";
import { storeAs, partition, compression, encryption, checksum } from "@minimajs/disk/plugins";
const disk = createDisk(
{ driver: createFsDriver({ root: "file:///var/uploads/" }) },
storeAs("uuid-original"),
partition({ by: "date", format: "yyyy/MM/dd" }),
compression({ algorithm: "gzip" }),
encryption({ password: process.env.DISK_ENCRYPTION_PASSWORD! }),
checksum()
);
const hello = new File(["hello"], "report.txt", { type: "text/plain" });
await disk.put(hello);Filesystem Storage
import { createDisk } from "@minimajs/disk";
import { createFsDriver } from "@minimajs/disk/adapters";
const disk = createDisk({
driver: createFsDriver({
root: "file:///var/uploads/",
publicUrl: "https://cdn.example.com",
}),
});
// Store a file
const file = await disk.put("avatar.jpg", imageData);
console.log(file.href); // file:///var/uploads/avatar.jpg
// Retrieve a file
const retrieved = await disk.get("avatar.jpg");
if (retrieved) {
const buffer = await retrieved.arrayBuffer();
const text = await retrieved.text();
const stream = retrieved.stream();
}
// Check existence
const exists = await disk.exists("avatar.jpg");
// Get public URL
const url = await disk.url("avatar.jpg");
console.log(url); // https://cdn.example.com/avatar.jpg
// Delete
await disk.delete("avatar.jpg");AWS S3 Storage
import { createDisk } from "@minimajs/disk";
import { createS3Driver } from "@minimajs/aws-s3";
const disk = createDisk({
driver: createS3Driver({
bucket: "my-bucket",
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
});
// Same API as filesystem!
await disk.put("avatar.jpg", imageData);
const file = await disk.get("avatar.jpg");Memory Storage (Testing)
import { createDisk } from "@minimajs/disk";
import { createMemoryDriver } from "@minimajs/disk/adapters";
const driver = createMemoryDriver();
const disk = createDisk({ driver });
// Perfect for tests
await disk.put("test.txt", "Hello World");
// Clear after tests
driver.clear();Core Concepts
DiskFile
All get(), put(), copy(), and move() operations return a DiskFile instance:
const file = await disk.put("document.pdf", pdfData);
// File properties
file.href; // Storage identifier (e.g., "s3://bucket/document.pdf")
file.name; // Filename (e.g., "document.pdf")
file.size; // File size in bytes
file.type; // MIME type (e.g., "application/pdf")
file.lastModified; // Timestamp
file.metadata; // Custom metadata
// Read file content
const buffer = await file.arrayBuffer();
const text = await file.text();
const bytes = await file.bytes();
const stream = file.stream(); // ReadableStreamLimitation: wrapping in Blob or File
DiskFile extends File and its content is streamed lazily. Wrapping it in new Blob([diskFile]) or new File([diskFile], ...) produces an empty blob — the Blob constructor reads internal parts that are not populated for streamed files.
// ❌ Wrong — results in an empty Blob
const blob = new Blob([diskFile]);
// ✅ Correct — buffer first, then wrap
const bytes = await diskFile.bytes();
const blob = new Blob([bytes], { type: diskFile.type });
// ✅ Correct — pass DiskFile directly where File/Blob is accepted
// (fetch body, FormData, disk.put — all call .stream() internally and work fine)
formData.append("file", diskFile);
await fetch(url, { body: diskFile });
await disk.put("copy.jpg", diskFile);Storage Identifiers (href)
Each file has an href that uniquely identifies it within the storage system:
- Filesystem:
file:///var/uploads/avatar.jpg - S3:
s3://bucket/path/to/file.jpg - Azure:
https://account.blob.core.windows.net/container/file.jpg
API Reference
Disk Operations
put(path, data, options?)
Store a file by path.
const file = await disk.put("uploads/photo.jpg", imageData, {
type: "image/jpeg",
metadata: { userId: "123", album: "vacation" },
});Parameters:
path: string- File pathdata: Blob | File | string | ArrayBuffer | ReadableStream- File contentoptions?: PutOptionstype?: string- MIME typemetadata?: Record<string, string>- Custom metadatalastModified?: Date- Last modified datesignal?: AbortSignal- Cancel the upload
Returns: Promise<DiskFile>
put(file, options?)
Store a File object directly. The file is stored under its original name (file.name) — the filename is preserved as-is.
// Stored under the file's original name
const uploaded = await disk.put(uploadedFile);
console.log(uploaded.name); // same as uploadedFile.name
// With additional options
const uploaded = await disk.put(uploadedFile, {
metadata: { userId: "123" },
});To generate unique or structured names automatically, use the storeAs plugin.
Parameters:
file: File- A Web APIFileobject (e.g., from a multipart upload)options?: PutOptions- MIME type is inferred fromfile.typeif not set
Returns: Promise<DiskFile>
get(path, options?)
Retrieve a file.
const file = await disk.get("uploads/photo.jpg");
if (file) {
const data = await file.arrayBuffer();
}Parameters:
path: string- File pathoptions?: { signal?: AbortSignal }- Pass anAbortSignalto cancel the operation
Returns: Promise<DiskFile | null>
exists(path, options?)
Check if a file exists.
const exists = await disk.exists("uploads/photo.jpg");Parameters:
path: string- File pathoptions?: { signal?: AbortSignal }
Returns: Promise<boolean>
delete(source, options?)
Delete a file by path, File, or DiskFile.
// By path
await disk.delete("uploads/photo.jpg");
// By DiskFile — uses file.href (storage identifier)
const file = await disk.get("uploads/photo.jpg");
await disk.delete(file);
// By plain File — uses file.name as the path
await disk.delete(uploadedFile);Parameters:
source: string | File- File path,File, orDiskFileoptions?: { signal?: AbortSignal }
Returns: Promise<string> — the resolved href of the deleted file
url(path, options?)
Get public URL for a file.
const url = await disk.url("uploads/photo.jpg");Parameters:
path: string- File pathoptions?: UrlOptions
Returns: Promise<string>
copy(from, to, options?)
Copy a file.
// From path
await disk.copy("uploads/photo.jpg", "backups/photo.jpg");
// From DiskFile
const file = await disk.get("uploads/photo.jpg");
await disk.copy(file, "backups/photo.jpg");Parameters:
from: string | File- Source file path,File, orDiskFileto: string- Destination pathoptions?: { signal?: AbortSignal }
Returns: Promise<DiskFile>
move(from, to, options?)
Move/rename a file.
await disk.move("uploads/photo.jpg", "archive/photo.jpg");Parameters:
from: string | File- Source file path,File, orDiskFileto: string- Destination pathoptions?: { signal?: AbortSignal }
Returns: Promise<DiskFile>
list(prefix?, options?)
List files with optional prefix filtering.
for await (const file of disk.list("uploads/")) {
console.log(file.href, file.size);
}
// With limit
for await (const file of disk.list("uploads/", { limit: 10 })) {
console.log(file.name);
}Parameters:
prefix?: string- Filter by prefix (optional)options?: ListOptionslimit?: number- Maximum number of files to returnsignal?: AbortSignal- Cancel the listing mid-iteration
Returns: AsyncIterable<DiskFile>
metadata(path, options?)
Get file metadata without downloading content.
const metadata = await disk.metadata("uploads/photo.jpg");
if (metadata) {
console.log(metadata.size, metadata.type, metadata.lastModified);
}Parameters:
path: string- File pathoptions?: { signal?: AbortSignal }
Returns: Promise<FileMetadata | null>
Cancellation
All disk operations accept an optional signal option. Pass an AbortSignal to cancel long-running operations when the caller is no longer interested — for example when an HTTP client disconnects.
import { request } from "@minimajs/server";
import { disk } from "../disk.js";
// Cancel disk I/O if the HTTP client disconnects
async function serveFile() {
const signal = request.signal();
return disk.get("uploads/video.mp4", { signal });
}Combining with a timeout:
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(new Error("Timeout")), 10_000);
try {
const file = await disk.get("uploads/large.zip", { signal: ac.signal });
return file;
} finally {
clearTimeout(timer);
}Cancelling list iteration:
const signal = request.signal();
for await (const file of disk.list("uploads/", { signal })) {
await process(file);
}When a signal aborts, the operation throws a DOMException with name: "AbortError" (or whatever reason was passed to controller.abort(reason)). Check for it with:
try {
await disk.get("file.txt", { signal });
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
// Cancelled — client disconnected or timeout
}
}Plugins
Plugins extend disk behavior by hooking into file operations. Pass them as rest arguments to createDisk:
import { createDisk } from "@minimajs/disk";
import { storeAs, partition, atomicWrite, checksum, uploadProgress, downloadProgress } from "@minimajs/disk/plugins";
const disk = createDisk(
{ driver: createFsDriver({ root: "./uploads" }) },
storeAs("uuid"),
partition({ by: "date" }),
atomicWrite(),
checksum()
);storeAs(nameStrategy | nameGenerator)
Automatically rename files when a File object is passed to put. By default, put(file) preserves the original filename — use storeAs to opt into UUID-based or custom naming.
import { storeAs } from "@minimajs/disk/plugins";
// UUID filename — "550e8400-….jpg"
const disk = createDisk({ driver }, storeAs("uuid"));
// UUID prefix + original name — "550e8400-…-photo.jpg"
const disk = createDisk({ driver }, storeAs("uuid-original"));
// Custom generator — full control (sync or async)
const disk = createDisk(
{ driver },
storeAs((file) => `${new Date().getFullYear()}/${randomUUID()}${extname(file.name)}`)
);When the name is changed, the original filename is saved in file.metadata.originalName.
| Strategy | Example output |
|---|---|
"uuid" (default) | 550e8400-….jpg |
"uuid-original" | 550e8400-…-photo.jpg |
(file) => string | whatever you return |
Only applies when data instanceof File. Calls with a plain path are unaffected.
Drivers
| Driver | Package | Use Case |
|---|---|---|
| Filesystem | @minimajs/disk | Local / development |
| Memory | @minimajs/disk | Testing |
| AWS S3 | @minimajs/aws-s3 | Production |
| Azure Blob | @minimajs/azure-blob | Production |
- Filesystem Driver - Local file storage
- AWS S3 Driver - Amazon S3 storage
- Azure Blob Driver - Microsoft Azure Blob Storage
- Memory Driver - In-memory storage for testing
- Protocol Disk - Multi-driver routing by URL prefix
Creating Custom Drivers
Implement the DiskDriver interface:
import type { DiskDriver, FileMetadata, PutOptions, ListOptions, UrlOptions } from "@minimajs/disk";
class CustomDriver implements DiskDriver {
async put(href: string, stream: ReadableStream, options?: PutOptions): Promise<FileMetadata> { … }
async get(href: string, options: { signal?: AbortSignal }): Promise<[ReadableStream, FileMetadata] | null> { … }
async delete(href: string, options: { signal?: AbortSignal }): Promise<void> { … }
async exists(href: string, options: { signal?: AbortSignal }): Promise<boolean> { … }
async copy(from: string, to: string, options: { signal?: AbortSignal }): Promise<void> { … }
async move(from: string, to: string, options: { signal?: AbortSignal }): Promise<void> { … }
async *list(prefix?: string, options?: ListOptions): AsyncIterable<FileMetadata> { … }
async metadata(href: string, options: { signal?: AbortSignal }): Promise<FileMetadata | null> { … }
async url(href: string, options?: UrlOptions): Promise<string> { … }
}Error Handling
import { DiskReadError, DiskWriteError, DiskFileNotFoundError, DiskMetadataError } from "@minimajs/disk";
try {
await disk.get("missing.txt");
} catch (error) {
if (error instanceof DiskFileNotFoundError) {
console.log("File not found:", error.href);
} else if (error instanceof DiskReadError) {
console.log("Failed to read:", error.href);
}
}Error Types
DiskError- Base error classDiskReadError- Failed to read fileDiskWriteError- Failed to write fileDiskFileNotFoundError- File not foundDiskCopyError- Copy operation failedDiskMoveError- Move operation failedDiskDeleteError- Delete operation failedDiskUrlError- URL generation failedDiskMetadataError- Metadata retrieval failedDiskConfigError- Invalid configuration