The @memberjunction/storage library provides a unified interface for interacting with various cloud storage providers. It abstracts the complexities of different storage services behind a consistent API, making it easy to work with files stored across different cloud platforms.
This library is a key component of the MemberJunction platform, providing seamless file storage operations across multiple cloud providers. It offers a provider-agnostic approach to file management, allowing applications to switch between storage providers without code changes.
FileStorageBase abstract classFileStorageBasenpm install @memberjunction/storage
This package depends on:
@memberjunction/core - Core MemberJunction functionality@memberjunction/core-entities - Entity definitions including FileStorageProviderEntity@memberjunction/global - Global utilities and class registrationFirst, configure the environment variables required for your chosen storage provider(s). You can implement multiple providers simultaneously and switch between them based on your application requirements.
Refer to the documentation for each storage provider for detailed configuration requirements (see the "Storage Provider Configuration" section below).
STORAGE_AZURE_CONTAINER=your-container-name
STORAGE_AZURE_ACCOUNT_NAME=your-account-name
STORAGE_AZURE_ACCOUNT_KEY=your-account-key
The library provides high-level utility functions that work with MemberJunction's entity system:
import { createUploadUrl, createDownloadUrl, deleteObject, moveObject } from '@memberjunction/storage';
import { FileStorageProviderEntity } from '@memberjunction/core-entities';
import { Metadata } from '@memberjunction/core';
// Load a FileStorageProviderEntity from the database
async function fileOperationsExample() {
const md = new Metadata();
const provider = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers');
await provider.Load('your-provider-id');
// Create pre-authenticated upload URL
const { updatedInput, UploadUrl } = await createUploadUrl(
provider,
{
ID: '123',
Name: 'documents/report.pdf',
ProviderID: provider.ID
}
);
// The client can use the UploadUrl directly to upload the file
console.log(`Upload URL: ${UploadUrl}`);
console.log(`File status: ${updatedInput.Status}`); // 'Uploading'
console.log(`Content type: ${updatedInput.ContentType}`); // 'application/pdf'
// If a ProviderKey was returned, use it for future operations
const fileIdentifier = updatedInput.ProviderKey || updatedInput.Name;
// Later, create pre-authenticated download URL
const downloadUrl = await createDownloadUrl(provider, fileIdentifier);
console.log(`Download URL: ${downloadUrl}`);
// Move the file to a new location
const moved = await moveObject(
provider,
fileIdentifier,
'documents/archived/report_2024.pdf'
);
console.log(`File moved: ${moved}`);
// Delete the file when no longer needed
const deleted = await deleteObject(provider, 'documents/archived/report_2024.pdf');
console.log(`File deleted: ${deleted}`);
}
You can also work directly with a storage provider by instantiating it using the MemberJunction class factory:
import { FileStorageBase } from '@memberjunction/storage';
import { MJGlobal } from '@memberjunction/global';
async function directProviderExample() {
// Create storage instance using the class factory (recommended)
const storage = MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
FileStorageBase,
'Azure Blob Storage'
);
// Initialize the storage provider if needed
await storage.initialize();
// List all files in a directory
const result = await storage.ListObjects('documents/');
console.log('Files:', result.objects);
console.log('Directories:', result.prefixes);
// Display detailed metadata for each file
for (const file of result.objects) {
console.log(`\nFile: ${file.name}`);
console.log(` Path: ${file.path}`);
console.log(` Full Path: ${file.fullPath}`);
console.log(` Size: ${file.size} bytes`);
console.log(` Type: ${file.contentType}`);
console.log(` Modified: ${file.lastModified}`);
console.log(` Is Directory: ${file.isDirectory}`);
}
// Create a directory
const dirCreated = await storage.CreateDirectory('documents/reports/');
console.log(`Directory created: ${dirCreated}`);
// Upload a file directly with metadata
const content = Buffer.from('Hello, World!');
const uploaded = await storage.PutObject(
'documents/reports/hello.txt',
content,
'text/plain',
{
author: 'John Doe',
department: 'Engineering',
version: '1.0'
}
);
console.log(`File uploaded: ${uploaded}`);
// Get file metadata without downloading content
const metadata = await storage.GetObjectMetadata('documents/reports/hello.txt');
console.log('File metadata:', metadata);
// Download file content
const fileContent = await storage.GetObject('documents/reports/hello.txt');
console.log('File content:', fileContent.toString('utf8'));
// Copy a file
const copied = await storage.CopyObject(
'documents/reports/hello.txt',
'documents/archive/hello-backup.txt'
);
console.log(`File copied: ${copied}`);
// Check if a file exists
const exists = await storage.ObjectExists('documents/reports/hello.txt');
console.log(`File exists: ${exists}`);
// Check if a directory exists
const dirExists = await storage.DirectoryExists('documents/reports/');
console.log(`Directory exists: ${dirExists}`);
// Delete a directory and all its contents
const dirDeleted = await storage.DeleteDirectory('documents/reports/', true);
console.log(`Directory deleted: ${dirDeleted}`);
}
Providers with native search capabilities support the SearchFiles method for finding files by name, content, and metadata:
import { FileStorageBase, FileSearchOptions, UnsupportedOperationError } from '@memberjunction/storage';
import { MJGlobal } from '@memberjunction/global';
async function searchExample() {
const storage = MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
FileStorageBase,
'Google Drive Storage'
);
try {
// Simple search for files matching a query
const results = await storage.SearchFiles('quarterly report');
console.log(`Found ${results.results.length} files`);
for (const file of results.results) {
console.log(` ${file.path} (${file.size} bytes)`);
if (file.excerpt) {
console.log(` Excerpt: ${file.excerpt}`);
}
}
// Advanced search with filters
const pdfResults = await storage.SearchFiles('budget 2024', {
fileTypes: ['pdf', 'docx'],
modifiedAfter: new Date('2024-01-01'),
pathPrefix: 'documents/finance/',
maxResults: 50
});
// Content search (searches inside files)
const contentResults = await storage.SearchFiles('machine learning', {
searchContent: true,
fileTypes: ['pdf', 'docx', 'txt']
});
// Check for more results
if (contentResults.hasMore) {
console.log(`Total matches: ${contentResults.totalMatches}`);
console.log(`Next page token: ${contentResults.nextPageToken}`);
}
} catch (error) {
if (error instanceof UnsupportedOperationError) {
console.log('This provider does not support file search');
// Fall back to ListObjects or other approaches
} else {
throw error;
}
}
}
Provider Search Support:
For providers without native search, consider using ListObjects with client-side filtering or implementing an external search index.
CreatePreAuthUploadUrlPayloadtype CreatePreAuthUploadUrlPayload = {
UploadUrl: string; // Pre-authenticated URL for upload
ProviderKey?: string; // Optional provider-specific key
};
StorageObjectMetadatatype StorageObjectMetadata = {
name: string; // Object name (filename)
path: string; // Directory path
fullPath: string; // Complete path including name
size: number; // Size in bytes
contentType: string; // MIME type
lastModified: Date; // Last modification date
isDirectory: boolean; // Whether this is a directory
etag?: string; // Entity tag for caching
cacheControl?: string; // Cache control directives
customMetadata?: Record<string, string>; // Custom metadata
};
StorageListResulttype StorageListResult = {
objects: StorageObjectMetadata[]; // Files found
prefixes: string[]; // Directories found
};
FileSearchOptionstype FileSearchOptions = {
maxResults?: number; // Maximum results (default: 100)
fileTypes?: string[]; // Filter by MIME types or extensions
modifiedAfter?: Date; // Only files modified after this date
modifiedBefore?: Date; // Only files modified before this date
pathPrefix?: string; // Search within specific directory
searchContent?: boolean; // Search file contents (default: false)
providerSpecific?: Record<string, any>; // Provider-specific options
};
FileSearchResulttype FileSearchResult = {
path: string; // Full path to file
name: string; // Filename only
size: number; // Size in bytes
contentType: string; // MIME type
lastModified: Date; // Last modification date
relevance?: number; // Relevance score (0.0-1.0)
excerpt?: string; // Text excerpt with match context
matchInFilename?: boolean; // Whether match is in filename
customMetadata?: Record<string, string>; // Custom metadata
providerData?: Record<string, any>; // Provider-specific data
};
FileSearchResultSettype FileSearchResultSet = {
results: FileSearchResult[]; // Array of matching files
totalMatches?: number; // Total matches (if available)
hasMore: boolean; // More results available?
nextPageToken?: string; // Token for next page
};
All storage providers implement these methods:
CreatePreAuthUploadUrl(objectName: string): Promise<CreatePreAuthUploadUrlPayload>CreatePreAuthDownloadUrl(objectName: string): Promise<string>MoveObject(oldObjectName: string, newObjectName: string): Promise<boolean>DeleteObject(objectName: string): Promise<boolean>ListObjects(prefix: string, delimiter?: string): Promise<StorageListResult>CreateDirectory(directoryPath: string): Promise<boolean>DeleteDirectory(directoryPath: string, recursive?: boolean): Promise<boolean>GetObjectMetadata(objectName: string): Promise<StorageObjectMetadata>GetObject(objectName: string): Promise<Buffer>PutObject(objectName: string, data: Buffer, contentType?: string, metadata?: Record<string, string>): Promise<boolean>CopyObject(sourceObjectName: string, destinationObjectName: string): Promise<boolean>ObjectExists(objectName: string): Promise<boolean>DirectoryExists(directoryPath: string): Promise<boolean>SearchFiles(query: string, options?: FileSearchOptions): Promise<FileSearchResultSet> (throws UnsupportedOperationError for providers without native search)initialize(): Promise<void> (optional, for async initialization)createUploadUrl<T>(provider: FileStorageProviderEntity, input: T): Promise<{ updatedInput: T & { Status: string; ContentType: string }, UploadUrl: string }>createDownloadUrl(provider: FileStorageProviderEntity, providerKeyOrName: string): Promise<string>moveObject(provider: FileStorageProviderEntity, oldProviderKeyOrName: string, newProviderKeyOrName: string): Promise<boolean>deleteObject(provider: FileStorageProviderEntity, providerKeyOrName: string): Promise<boolean>The library uses a class hierarchy with FileStorageBase as the abstract base class that defines the common interface. Each storage provider implements this interface:
FileStorageBase (Abstract Base Class)
├── AWSFileStorage (@RegisterClass: 'AWS S3')
├── AzureFileStorage (@RegisterClass: 'Azure Blob Storage')
├── GoogleFileStorage (@RegisterClass: 'Google Cloud Storage')
├── GoogleDriveFileStorage (@RegisterClass: 'Google Drive')
├── SharePointFileStorage (@RegisterClass: 'SharePoint')
├── DropboxFileStorage (@RegisterClass: 'Dropbox')
└── BoxFileStorage (@RegisterClass: 'Box')
Classes are registered with the MemberJunction global class factory using the @RegisterClass decorator, enabling dynamic instantiation based on provider keys.
This library integrates seamlessly with the MemberJunction platform:
FileStorageProviderEntity from @memberjunction/core-entities@memberjunction/global for dynamic provider instantiationEach storage provider requires specific environment variables. Please refer to the official documentation for each provider for detailed information on authentication and additional configuration options.
STORAGE_AWS_BUCKET: S3 bucket nameSTORAGE_AWS_REGION: AWS region (e.g., 'us-east-1')STORAGE_AWS_ACCESS_KEY_ID: AWS access key IDSTORAGE_AWS_SECRET_ACCESS_KEY: AWS secret access keyFor more information, see AWS S3 Documentation.
STORAGE_AZURE_CONTAINER: Container nameSTORAGE_AZURE_ACCOUNT_NAME: Account nameSTORAGE_AZURE_ACCOUNT_KEY: Account keyFor more information, see Azure Blob Storage Documentation.
STORAGE_GOOGLE_BUCKET: GCS bucket nameSTORAGE_GOOGLE_KEY_FILE_PATH: Path to service account key file (JSON)For more information, see Google Cloud Storage Documentation.
STORAGE_GOOGLE_DRIVE_CLIENT_ID: OAuth client IDSTORAGE_GOOGLE_DRIVE_CLIENT_SECRET: OAuth client secretSTORAGE_GOOGLE_DRIVE_REDIRECT_URI: OAuth redirect URISTORAGE_GOOGLE_DRIVE_REFRESH_TOKEN: OAuth refresh tokenFor more information, see Google Drive API Documentation.
STORAGE_SHAREPOINT_SITE_URL: SharePoint site URLSTORAGE_SHAREPOINT_CLIENT_ID: Azure AD client IDSTORAGE_SHAREPOINT_CLIENT_SECRET: Azure AD client secretSTORAGE_SHAREPOINT_TENANT_ID: Azure AD tenant IDFor more information, see Microsoft Graph API Documentation.
STORAGE_DROPBOX_ACCESS_TOKEN: Dropbox access tokenSTORAGE_DROPBOX_REFRESH_TOKEN: Dropbox refresh token (optional)STORAGE_DROPBOX_APP_KEY: Dropbox app keySTORAGE_DROPBOX_APP_SECRET: Dropbox app secretFor more information, see Dropbox API Documentation.
STORAGE_BOX_CLIENT_ID: Box client IDSTORAGE_BOX_CLIENT_SECRET: Box client secretSTORAGE_BOX_ENTERPRISE_ID: Box enterprise IDSTORAGE_BOX_JWT_KEY_ID: Box JWT key IDSTORAGE_BOX_PRIVATE_KEY: Box private key (base64 encoded)STORAGE_BOX_PRIVATE_KEY_PASSPHRASE: Box private key passphrase (optional)For more information, see Box Platform Documentation.
The library is designed to be extensible. To add a new storage provider:
import { FileStorageBase, StorageObjectMetadata, StorageListResult } from '@memberjunction/storage';
import { RegisterClass } from '@memberjunction/global';
@RegisterClass(FileStorageBase, 'My Custom Storage')
export class MyCustomStorage extends FileStorageBase {
protected readonly providerName = 'My Custom Storage';
constructor() {
super();
// Initialize your storage client here
}
public async initialize(): Promise<void> {
// Optional: Perform async initialization
// e.g., authenticate, verify permissions
}
public async CreatePreAuthUploadUrl(objectName: string): Promise<CreatePreAuthUploadUrlPayload> {
// Implement upload URL generation
// Return { UploadUrl: string, ProviderKey?: string }
}
public async CreatePreAuthDownloadUrl(objectName: string): Promise<string> {
// Implement download URL generation
}
// Implement all other abstract methods...
}
If your provider doesn't support certain operations:
public async CreateDirectory(directoryPath: string): Promise<boolean> {
// If directories aren't supported
this.throwUnsupportedOperationError('CreateDirectory');
}
Document required environment variables:
import * as env from 'env-var';
constructor() {
super();
const apiKey = env.get('STORAGE_MYCUSTOM_API_KEY').required().asString();
const endpoint = env.get('STORAGE_MYCUSTOM_ENDPOINT').required().asString();
// Use these to initialize your client
}
Add to src/index.ts:
export * from './drivers/MyCustomStorage';
Update this README with configuration requirements and any provider-specific notes.
The library provides consistent error handling across all providers:
Thrown when a provider doesn't support a specific operation:
try {
await storage.CreateDirectory('/some/path/');
} catch (error) {
if (error instanceof UnsupportedOperationError) {
console.log(`Provider doesn't support directories: ${error.message}`);
}
}
Each provider may throw errors specific to its underlying SDK. These are not wrapped, allowing you to handle provider-specific error conditions:
try {
await storage.GetObject('non-existent-file.txt');
} catch (error) {
// Handle provider-specific errors
if (error.code === 'NoSuchKey') { // AWS S3
console.log('File not found');
} else if (error.code === 'BlobNotFound') { // Azure
console.log('Blob not found');
}
}
ProviderKey if returned by CreatePreAuthUploadUrldocuments/ not documents)initialize() on providers that require async setupGetObject and PutObject methods load entire files into memory; for large files, consider streaming approachesContributions are welcome! To add a new storage provider:
git checkout -b feature/new-provider)src/drivers/FileStorageBaseISC
Part of the MemberJunction platform.