Skip to content

Storage

Unified file storage abstraction with interchangeable backends for S3-compatible object storage, local filesystem, and in-memory key-value caching.

Quick Reference

ClassExtendsBackendImplements
MinioHelperBaseStorageHelperS3-compatible (MinIO)IStorageHelper
BunS3HelperBaseStorageHelperS3-compatible (Bun-native)IStorageHelper
DiskHelperBaseStorageHelperLocal filesystemIStorageHelper
MemoryStorageHelperBaseHelperIn-memory key-value--

Import Paths

typescript
// Disk and in-memory storage (from base package)
import { DiskHelper, MemoryStorageHelper } from '@venizia/ignis-helpers';

// MinIO storage (separate export path)
import { MinioHelper } from '@venizia/ignis-helpers/minio';

// Bun S3 storage (separate export path, Bun runtime only)
import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';

// Types
import type {
  IStorageHelper,
  IStorageHelperOptions,
  IDiskHelperOptions,
  IUploadFile,
  IUploadResult,
  IFileStat,
  IBucketInfo,
  IObjectInfo,
  IListObjectsOptions,
} from '@venizia/ignis-helpers';
import type { IMinioHelperOptions } from '@venizia/ignis-helpers/minio';
import type { IBunS3HelperOptions } from '@venizia/ignis-helpers/bun-s3';

Creating an Instance

MinIO Storage

MinioHelper connects to MinIO or any S3-compatible object storage server. The constructor accepts all minio.ClientOptions properties alongside IStorageHelperOptions.

typescript
import { MinioHelper } from '@venizia/ignis-helpers/minio';

const storage = new MinioHelper({
  endPoint: 'localhost',
  port: 9000,
  useSSL: false,
  accessKey: 'minioadmin',
  secretKey: 'minioadmin',
});

IMinioHelperOptions

IMinioHelperOptions extends both IStorageHelperOptions and the minio ClientOptions type, so all minio Client options are accepted.

typescript
interface IMinioHelperOptions extends IStorageHelperOptions, ClientOptions {}
OptionTypeDefaultDescription
endPointstring--MinIO server hostname.
portnumber--Server port.
useSSLboolean--Enable HTTPS.
accessKeystring--Access key credential.
secretKeystring--Secret key credential.
scopestring'MinioHelper'Logger scope name.
identifierstring'MinioHelper'Helper identifier.

NOTE

The underlying minio.Client is stored as a private property. Use the IStorageHelper methods for all operations. If you need direct minio SDK access, extend MinioHelper in a subclass.

Bun S3 Storage

BunS3Helper provides S3-compatible storage using Bun's native S3Client for high-performance object operations. Bucket management operations (list, create, delete) use AWS Signature V4 signed requests, while object operations use Bun's native S3 API.

IMPORTANT

BunS3Helper requires the Bun runtime. It uses Bun's built-in S3Client class which is not available in Node.js.

typescript
import { BunS3Helper } from '@venizia/ignis-helpers/bun-s3';

const storage = new BunS3Helper({
  accessKey: 'minioadmin',
  secretKey: 'minioadmin',
  endpoint: 'http://localhost:9000',
  region: 'us-east-1',
});

IBunS3HelperOptions

OptionTypeDefaultDescription
accessKeystring--S3 access key credential.
secretKeystring--S3 secret key credential.
endpointstring--S3-compatible endpoint URL (e.g., 'http://localhost:9000').
regionstring'us-east-1'AWS region for signing.
sessionTokenstring--Optional session token for temporary credentials.
scopestring'BunS3Helper'Logger scope name.
identifierstring'BunS3Helper'Helper identifier.

Disk Storage

DiskHelper provides local filesystem storage using a bucket-based directory structure. The basePath directory is created automatically if it does not exist.

typescript
import { DiskHelper } from '@venizia/ignis-helpers';

const storage = new DiskHelper({
  basePath: './app_data/storage',
});

IDiskHelperOptions

OptionTypeDefaultDescription
basePathstring--Base directory where buckets will be created. Resolved to an absolute path internally. Created automatically if it does not exist.
scopestring'DiskHelper'Logger scope name.
identifierstring'DiskHelper'Helper identifier.

The resulting directory structure maps buckets to subdirectories:

app_data/storage/           <-- basePath
├── bucket-1/               <-- bucket (directory)
│   ├── file1.pdf           <-- object (file)
│   └── file2.jpg
└── user-uploads/
    ├── avatar.png
    └── resume.pdf

In-Memory Storage

MemoryStorageHelper is a standalone, generic key-value store for caching or temporary state within a single process. It does not implement IStorageHelper and has no bucket or file operations.

typescript
import { MemoryStorageHelper } from '@venizia/ignis-helpers';

// Direct instantiation
const cache = new MemoryStorageHelper();

// With custom scope for logging
const cache = new MemoryStorageHelper({ scope: 'SessionCache' });

// With typed container using the factory method
const cache = MemoryStorageHelper.newInstance<{ counter: number; name: string }>();

Constructor Options

OptionTypeDefaultDescription
scopestring'MemoryStorageHelper'Logger scope name.

Usage

DiskHelper, MinioHelper, and BunS3Helper implement the same IStorageHelper interface, making them interchangeable. All examples below apply to all three unless noted otherwise.

Uploading Files

Pass an array of IUploadFile objects to upload(). The method validates all file names before writing, then uploads in parallel.

typescript
const results = await storage.upload({
  bucket: 'my-bucket',
  files: [
    {
      originalName: 'report.pdf',
      mimetype: 'application/pdf',
      buffer: fileBuffer,
      size: fileBuffer.length,
      encoding: '7bit',
    },
  ],
});

console.log(results);
// [{ bucketName: 'my-bucket', objectName: 'report.pdf', link: '/static-assets/my-bucket/report.pdf' }]

By default, file names are lowercased with spaces replaced by underscores. The default link prefix differs by backend: MinioHelper and BunS3Helper use /static-assets/{bucket}/{name}, DiskHelper uses /static-resources/{bucket}/{name}. Override either with custom functions:

typescript
const results = await storage.upload({
  bucket: 'my-bucket',
  files: files,
  normalizeNameFn: ({ originalName, folderPath }) => {
    const timestamp = Date.now();
    return folderPath
      ? `${folderPath}/${timestamp}_${originalName}`
      : `${timestamp}_${originalName}`;
  },
  normalizeLinkFn: ({ bucketName, normalizeName }) => {
    return `/files/${bucketName}/${normalizeName}`;
  },
});

Upload with Folder Path

When folderPath is provided in an IUploadFile, the default normalization creates subdirectory-based paths:

typescript
const results = await storage.upload({
  bucket: 'my-bucket',
  files: [
    {
      originalName: 'avatar.png',
      mimetype: 'image/png',
      buffer: avatarBuffer,
      size: avatarBuffer.length,
      folderPath: 'users',
    },
  ],
});
// objectName: 'users/avatar.png'

WARNING

DiskHelper uses /static-resources/ as the default link prefix, while MinioHelper and BunS3Helper use /static-assets/. Provide a normalizeLinkFn if you need consistent links across storage backends.

Downloading Files

Retrieve a file as a Node.js Readable stream:

typescript
const fileStream = await storage.getFile({
  bucket: 'my-bucket',
  name: 'report.pdf',
});

// Pipe to an HTTP response
fileStream.pipe(response);

// Or write to disk
import fs from 'node:fs';
const writeStream = fs.createWriteStream('./downloads/report.pdf');
fileStream.pipe(writeStream);

MinIO-Specific Options

MinioHelper supports additional options for server-side encryption and versioning:

typescript
const fileStream = await minioStorage.getFile({
  bucket: 'my-bucket',
  name: 'report.pdf',
  options: {
    versionId: 'specific-version-id',
    SSECustomerAlgorithm: 'AES256',
    SSECustomerKey: 'encryption-key',
    SSECustomerKeyMD5: 'key-md5-hash',
  },
});

Getting File Metadata

typescript
const stat = await storage.getStat({
  bucket: 'my-bucket',
  name: 'report.pdf',
});

console.log(stat);
// {
//   size: 204800,
//   lastModified: 2025-01-15T10:30:00.000Z,
//   metadata: { mimetype: 'application/pdf' },
//   etag: 'abc123',       // MinioHelper only
//   versionId: 'v1',      // MinioHelper only (if versioning enabled)
// }

NOTE

DiskHelper populates metadata.mimetype using the getMimeType() extension-based lookup. It does not return etag or versionId. MinioHelper returns full metadata from the MinIO server including the original upload metadata, etag, and versionId. BunS3Helper returns metadata with contentType and mimetype from the S3 stat response, plus etag and lastModified.

Listing Files

typescript
// List all objects in a bucket
const objects = await storage.listObjects({ bucket: 'my-bucket' });

// List with prefix filter
const docs = await storage.listObjects({
  bucket: 'my-bucket',
  prefix: 'documents/',
});

// Recursive listing (includes files in subdirectories)
const allFiles = await storage.listObjects({
  bucket: 'my-bucket',
  useRecursive: true,
});

// Limit the number of results
const firstTen = await storage.listObjects({
  bucket: 'my-bucket',
  maxKeys: 10,
});

console.log(allFiles);
// [
//   { name: 'report.pdf', size: 204800, lastModified: Date, etag: '...' },
//   { name: 'avatar.png', size: 51200, lastModified: Date },
// ]

Deleting Files

typescript
// Delete a single object
await storage.removeObject({ bucket: 'my-bucket', name: 'old-file.pdf' });

// Delete multiple objects
await storage.removeObjects({
  bucket: 'my-bucket',
  names: ['file1.pdf', 'file2.jpg', 'file3.png'],
});

NOTE

DiskHelper's removeObject() throws if the file does not exist. DiskHelper's removeObjects() processes deletions sequentially. MinioHelper's removeObjects() delegates to the minio SDK's batch removal. BunS3Helper's removeObjects() deletes in parallel via Promise.all().

Bucket Operations

typescript
// Check if a bucket exists
const exists = await storage.isBucketExists({ name: 'my-bucket' });

// Create a new bucket
const bucket = await storage.createBucket({ name: 'my-bucket' });
// Returns: { name: 'my-bucket', creationDate: Date }

// List all buckets
const buckets = await storage.getBuckets();
// Returns: [{ name: 'bucket-1', creationDate: Date }, ...]

// Get a specific bucket
const bucket = await storage.getBucket({ name: 'my-bucket' });
// Returns: { name: 'my-bucket', creationDate: Date } | null

// Remove a bucket
const removed = await storage.removeBucket({ name: 'my-bucket' });

IMPORTANT

DiskHelper's removeBucket() requires the bucket directory to be empty. It throws if files remain. Remove all objects first, then remove the bucket.

In-Memory Storage Operations

MemoryStorageHelper provides a simple key-value API, separate from the bucket-based IStorageHelper interface:

typescript
const cache = new MemoryStorageHelper();

// Store a value
cache.set('user:123', { name: 'Alice', role: 'admin' });

// Retrieve a typed value
const user = cache.get<{ name: string; role: string }>('user:123');

// Check if a key exists
cache.isBound('user:123'); // true

// Get all keys
cache.keys(); // ['user:123']

// Access the underlying container
cache.getContainer(); // { 'user:123': { name: 'Alice', role: 'admin' } }

// Clear all stored data
cache.clear();

Name Validation

All bucket and file operations validate names using isValidName() before execution. The following are rejected:

RuleExampleReason
Contains .., /, or \../etc/passwdPath traversal
Starts with ..hiddenHidden file
Contains ;, |, &, $, `, <, >, {, }, [, ], !, #file;rm -rfShell injection
Contains \n, \r, or \0file\nnameHeader injection
Longer than 255 characters(very long string)DoS prevention
Empty or whitespace-only"", " "Invalid input
typescript
storage.isValidName('my-file.pdf');    // true
storage.isValidName('../etc/passwd');  // false
storage.isValidName('.hidden');        // false

MIME Type Detection

getMimeType() determines the MIME type from a filename's extension:

typescript
storage.getMimeType('photo.jpg');    // 'image/jpeg'
storage.getMimeType('data.csv');     // 'text/csv'
storage.getMimeType('unknown.xyz');  // 'application/octet-stream'

getFileType() categorizes a MIME type into a broad group:

typescript
storage.getFileType({ mimeType: 'image/png' });        // 'image'
storage.getFileType({ mimeType: 'video/mp4' });         // 'video'
storage.getFileType({ mimeType: 'text/plain' });        // 'text'
storage.getFileType({ mimeType: 'application/pdf' });   // 'unknown'

Common Patterns

Storage Abstraction

Use IStorageHelper to write storage-agnostic code:

typescript
class FileService {
  constructor(private storage: IStorageHelper) {}

  async uploadFile(bucket: string, file: IUploadFile) {
    return this.storage.upload({ bucket, files: [file] });
  }
}

// Swap backends without changing service code
const devService = new FileService(new DiskHelper({ basePath: './files' }));
const prodService = new FileService(new MinioHelper({ /* ... */ }));
const bunService = new FileService(new BunS3Helper({ /* ... */ }));

Environment-Based Selection

typescript
import { applicationEnvironment } from '@venizia/ignis-helpers';

const createStorage = (): IStorageHelper => {
  if (applicationEnvironment.get('STORAGE_TYPE') === 'minio') {
    return new MinioHelper({
      endPoint: applicationEnvironment.get('MINIO_HOST'),
      port: Number(applicationEnvironment.get('MINIO_PORT')),
      accessKey: applicationEnvironment.get('MINIO_ACCESS_KEY'),
      secretKey: applicationEnvironment.get('MINIO_SECRET_KEY'),
      useSSL: applicationEnvironment.get('MINIO_USE_SSL') === 'true',
    });
  }

  return new DiskHelper({
    basePath: applicationEnvironment.get('DISK_STORAGE_PATH') || './storage',
  });
};

Troubleshooting

"[createBucket] Invalid name to create bucket!"

Cause: The bucket name failed isValidName() validation. The name may contain path traversal characters, start with a dot, contain shell-special characters, or exceed 255 characters.

Fix: Use a simple alphanumeric bucket name:

typescript
// Wrong
await storage.createBucket({ name: '../my-bucket' });
await storage.createBucket({ name: '.hidden-bucket' });

// Correct
await storage.createBucket({ name: 'my-bucket' });

"[removeBucket] Invalid name to remove bucket!"

Cause: Same as above -- the bucket name failed validation.

Fix: Provide a valid bucket name that passes isValidName().

"[createBucket] Bucket already exists | name: {name}"

Cause: DiskHelper throws when calling createBucket() on an existing bucket directory.

Fix: Check existence first:

typescript
const exists = await storage.isBucketExists({ name: 'my-bucket' });
if (!exists) {
  await storage.createBucket({ name: 'my-bucket' });
}

"[removeBucket] Bucket does not exist | name: {name}"

Cause: DiskHelper throws when attempting to remove a bucket directory that does not exist.

Fix: Check existence before removal:

typescript
const exists = await storage.isBucketExists({ name: 'my-bucket' });
if (exists) {
  await storage.removeBucket({ name: 'my-bucket' });
}

"[removeBucket] Bucket is not empty | name: {name}"

Cause: DiskHelper's removeBucket() requires the bucket directory to be empty before removal.

Fix: Remove all objects first:

typescript
const objects = await storage.listObjects({ bucket: 'my-bucket', useRecursive: true });
if (objects.length > 0) {
  await storage.removeObjects({
    bucket: 'my-bucket',
    names: objects.map(o => o.name!),
  });
}
await storage.removeBucket({ name: 'my-bucket' });

"[upload] Bucket does not exist | name: {bucket}"

Cause: The target bucket does not exist. Both DiskHelper and MinioHelper validate bucket existence before uploading.

Fix: Create the bucket before uploading:

typescript
const exists = await storage.isBucketExists({ name: 'uploads' });
if (!exists) {
  await storage.createBucket({ name: 'uploads' });
}
await storage.upload({ bucket: 'uploads', files: [...] });

"[upload] Invalid original file name"

Cause: A file's originalName failed isValidName() validation.

Fix: Sanitize file names before uploading, or use normalizeNameFn to control the stored name:

typescript
await storage.upload({
  bucket: 'my-bucket',
  files: files,
  normalizeNameFn: ({ originalName }) => {
    return originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
  },
});

"[upload] Invalid file size"

Cause: A file's size property is 0, undefined, or falsy.

Fix: Ensure every file in the upload array has a valid size value:

typescript
const file: IUploadFile = {
  originalName: 'doc.pdf',
  mimetype: 'application/pdf',
  buffer: fileBuffer,
  size: fileBuffer.length, // Must be > 0
};

"[getFile] File not found | bucket: {bucket} | name: {name}"

Cause: DiskHelper throws when the requested file does not exist on the filesystem.

Fix: Verify the file exists before attempting to retrieve it, or handle the error:

typescript
try {
  const stream = await storage.getFile({ bucket: 'my-bucket', name: 'file.pdf' });
} catch (error) {
  // File not found -- handle gracefully
}

MinioHelper connection errors

Cause: Network or configuration issue between the application and the MinIO server.

Checklist:

  • The MinIO server is running and reachable at the configured endPoint and port
  • useSSL matches the server's TLS configuration
  • accessKey and secretKey are correct
  • Network and firewall rules allow the connection

See Also