Member Junction
    Preparing search index...

    Module @memberjunction/storage - v5.49.0

    @memberjunction/storage

    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.

    Part of the MemberJunction framework.

    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. The package supports both simple single-tenant deployments using environment variables and enterprise multi-tenant deployments using the MemberJunction Credential Engine for secure credential management.

    The library is organized around an abstract base class (FileStorageBase) with concrete driver implementations for each supported cloud storage provider. A set of high-level utility functions in util.ts bridge the gap between MemberJunction entities and the underlying drivers. Configuration is handled through Zod-validated schemas loaded from mj.config.cjs or environment variables.

    graph TB
        subgraph Application["Application Layer"]
            style Application fill:#2d6a9f,stroke:#1a4971,color:#fff
            UtilFunctions["Utility Functions
    createUploadUrl, createDownloadUrl,
    moveObject, deleteObject, listObjects,
    copyObjectBetweenProviders,
    searchAcrossProviders"] end subgraph Core["Core Abstractions"] style Core fill:#7c5295,stroke:#563a6b,color:#fff FSBase["FileStorageBase
    (Abstract Base Class)"] Config["StorageConfig
    (Zod Schema)"] end subgraph Drivers["Storage Provider Drivers"] style Drivers fill:#2d8659,stroke:#1a5c3a,color:#fff AWS["AWSFileStorage
    'AWS S3'"] Azure["AzureFileStorage
    'Azure Blob Storage'"] GCS["GoogleFileStorage
    'Google Cloud Storage'"] GDrive["GoogleDriveFileStorage
    'Google Drive Storage'"] SP["SharePointFileStorage
    'SharePoint'"] Dropbox["DropboxFileStorage
    'Dropbox'"] Box["BoxFileStorage
    'Box'"] end subgraph MJ["MemberJunction Integration"] style MJ fill:#b8762f,stroke:#8a5722,color:#fff ClassFactory["MJGlobal ClassFactory
    @RegisterClass"] Entities["FileStorageProviderEntity
    FileStorageAccountEntity"] CredEngine["CredentialEngine
    Secure credential decryption"] end UtilFunctions --> FSBase UtilFunctions --> ClassFactory UtilFunctions --> Entities UtilFunctions --> CredEngine FSBase --> Config AWS --> FSBase Azure --> FSBase GCS --> FSBase GDrive --> FSBase SP --> FSBase Dropbox --> FSBase Box --> FSBase ClassFactory --> AWS ClassFactory --> Azure ClassFactory --> GCS ClassFactory --> GDrive ClassFactory --> SP ClassFactory --> Dropbox ClassFactory --> Box
    • Unified API: Consistent methods across all storage providers via the FileStorageBase abstract class
    • Type-Safe: Full TypeScript support with comprehensive type definitions
    • Flexible Provider Selection: Use any number of storage providers simultaneously based on your application needs
    • Pre-authenticated URLs: Secure upload and download operations using time-limited URLs
    • HTTP Range Streaming: Serve large audio/video via GetObjectStream without buffering whole files in memory (all seven providers: AWS S3, Azure Blob, Google Cloud Storage, Google Drive, SharePoint, Dropbox, Box). Capability is introspectable via SupportsStreaming. See docs/STREAMING.md.
    • Metadata Support: Store and retrieve custom metadata with your files
    • Error Handling: Provider-specific errors are normalized with clear error messages
    • Zod-Validated Configuration: All configuration schemas are validated at load time via Zod
    • Enterprise Credential Management: Integrates with @memberjunction/credentials for secure, multi-tenant credential storage and decryption
    • Cross-Provider Copy: Copy files between different storage providers server-side
    • Multi-Provider Search: Search for files across multiple providers or accounts in parallel
    • Token Refresh Persistence: Automatic callback system to persist new OAuth tokens (critical for providers like Box that rotate refresh tokens)
    • Extensible: Easy to add new storage providers by extending FileStorageBase
    Provider Driver Key Search Pre-auth URLs Native Directories Range Streaming
    AWS S3 AWS S3 No Yes Simulated Yes
    Azure Blob Storage Azure Blob Storage No Yes Simulated Yes
    Google Cloud Storage Google Cloud Storage No Yes Simulated Yes
    Google Drive Google Drive Storage Yes (content) Yes Native Yes (non-Workspace files)
    Microsoft SharePoint SharePoint Yes (content) Yes Native Yes
    Dropbox Dropbox Yes (content) Yes Native Yes
    Box Box Yes (metadata) Yes Native Yes
    • Upload files (via pre-authenticated URLs or direct Buffer upload)
    • Download files (via pre-authenticated URLs or direct Buffer download)
    • Copy and move files (within and across providers)
    • Delete files and directories (with optional recursive deletion)
    • List files and directories with full metadata
    • Create and manage directories
    • Get detailed file metadata without downloading content
    • Check file/directory existence
    • Search files using native provider search APIs
    npm install @memberjunction/storage
    

    This package depends on:

    Package Purpose
    @memberjunction/core Core MemberJunction functionality (logging, UserInfo)
    @memberjunction/core-entities Entity definitions (FileStorageProviderEntity, FileStorageAccountEntity)
    @memberjunction/global Global class factory and @RegisterClass decorator
    @memberjunction/credentials Credential Engine for secure credential decryption
    @aws-sdk/client-s3, @aws-sdk/s3-request-presigner AWS S3 SDK
    @azure/storage-blob, @azure/identity Azure Blob Storage SDK
    @google-cloud/storage Google Cloud Storage SDK
    googleapis Google Drive API
    @microsoft/microsoft-graph-client SharePoint via Microsoft Graph
    dropbox Dropbox SDK
    box-node-sdk Box SDK
    cosmiconfig Configuration file loading
    zod Configuration schema validation
    env-var Environment variable parsing
    mime-types MIME type detection

    Every storage driver follows a two-step initialization pattern. The initialize() method is smart enough to handle both simple deployments (environment variables) and multi-tenant deployments (database credentials).

    flowchart TD
        Start(["Create Driver Instance"]) --> Constructor["Constructor
    Loads env vars / config defaults"] Constructor --> Init{"Call initialize()"} Init -->|"No config"| EnvPath["Use environment variables
    already loaded by constructor"] Init -->|"With config"| ConfigPath["Override credentials
    with provided config"] EnvPath --> SetAccount["Set accountId / accountName
    if provided"] ConfigPath --> SetAccount SetAccount --> Reinit["Reinitialize SDK client
    with final credentials"] Reinit --> Ready(["Driver Ready"]) style Start fill:#2d6a9f,stroke:#1a4971,color:#fff style Ready fill:#2d8659,stroke:#1a5c3a,color:#fff style Init fill:#b8762f,stroke:#8a5722,color:#fff style Constructor fill:#7c5295,stroke:#563a6b,color:#fff style EnvPath fill:#7c5295,stroke:#563a6b,color:#fff style ConfigPath fill:#7c5295,stroke:#563a6b,color:#fff style SetAccount fill:#7c5295,stroke:#563a6b,color:#fff style Reinit fill:#7c5295,stroke:#563a6b,color:#fff

    For single-tenant applications, development, testing, or simple production deployments:

    # Example: Azure Blob Storage
    export STORAGE_AZURE_CONTAINER=your-container-name
    export STORAGE_AZURE_ACCOUNT_NAME=your-account-name
    export STORAGE_AZURE_ACCOUNT_KEY=your-account-key
    import { AzureFileStorage } from '@memberjunction/storage';

    // Constructor loads environment variables
    const storage = new AzureFileStorage();

    // ALWAYS call initialize() - no config needed for env var deployments
    await storage.initialize();

    // Provider is now ready to use
    await storage.ListObjects('/');

    For enterprise applications managing multiple storage accounts via the MemberJunction entity system and Credential Engine:

    import { FileStorageEngine } from '@memberjunction/core-entities';
    import { initializeDriverWithAccountCredentials } from '@memberjunction/storage';

    // Load account from database
    const engine = FileStorageEngine.Instance;
    await engine.Config(false, contextUser);

    const accountWithProvider = engine.GetAccountWithProvider(accountId);

    // Infrastructure utility handles credential decryption and initialization
    const storage = await initializeDriverWithAccountCredentials({
    accountEntity: accountWithProvider.account,
    providerEntity: accountWithProvider.provider,
    contextUser
    });

    // Provider is ready - credentials were automatically decrypted and initialized
    await storage.ListObjects('/');

    The initializeDriverWithAccountCredentials() utility:

    • Creates the driver instance via the MJGlobal ClassFactory
    • Retrieves the credential by ID from the Credential Engine
    • Decrypts the credential values
    • Configures a token refresh callback to persist rotated tokens back to the database
    • Calls initialize() with decrypted values and account information
    sequenceDiagram
        participant App as Application
        participant Util as initializeDriverWithAccountCredentials
        participant CF as ClassFactory
        participant CE as CredentialEngine
        participant Driver as Storage Driver
    
        App->>Util: { accountEntity, providerEntity, contextUser }
        Util->>CF: CreateInstance(FileStorageBase, driverKey)
        CF-->>Util: driver instance
    
        alt Account has CredentialID
            Util->>CE: Config(false, contextUser)
            Util->>CE: getCredentialById(credentialID)
            CE-->>Util: credentialEntity
            Util->>CE: getCredential(name, options)
            CE-->>Util: { values: decrypted credentials }
            Util->>Util: Create onTokenRefresh callback
            Util->>Driver: initialize({ accountId, ...decryptedValues, onTokenRefresh })
        else No CredentialID
            alt Provider has Configuration JSON
                Util->>Driver: initialize({ accountId, ...providerConfig })
            else No configuration
                Util->>Driver: initialize({ accountId })
            end
        end
    
        Driver-->>App: initialized driver
    

    The library provides high-level utility functions that work with MemberJunction's entity system:

    import {
    createUploadUrl,
    createDownloadUrl,
    moveObject,
    deleteObject,
    listObjects,
    copyObject
    } from '@memberjunction/storage';
    import { FileStorageProviderEntity } from '@memberjunction/core-entities';
    import { Metadata } from '@memberjunction/core';

    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;

    // Create pre-authenticated download URL
    const downloadUrl = await createDownloadUrl(provider, fileIdentifier);

    // List directory contents
    const contents = await listObjects(provider, 'documents/');
    for (const file of contents.objects) {
    console.log(`${file.name} (${file.size} bytes)`);
    }

    // Copy a file
    await copyObject(provider, fileIdentifier, 'documents/report-backup.pdf');

    // Move a file
    await moveObject(provider, fileIdentifier, 'archive/report.pdf');

    // Delete a file
    await deleteObject(provider, 'archive/report.pdf');
    }

    You can work directly with a storage provider by instantiating it:

    import { AzureFileStorage, FileStorageBase } from '@memberjunction/storage';
    import { MJGlobal } from '@memberjunction/global';

    async function directProviderExample() {
    // Method 1: Direct instantiation (simple deployment with env vars)
    const storage = new AzureFileStorage();
    await storage.initialize();

    // Method 2: Using class factory (dynamic provider selection)
    const storage2 = MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
    FileStorageBase,
    'Azure Blob Storage'
    );
    await storage2.initialize();

    // List all files in a directory
    const result = await storage.ListObjects('documents/');
    console.log('Files:', result.objects);
    console.log('Directories:', result.prefixes);

    // Upload a file directly with metadata
    const content = Buffer.from('Hello, World!');
    await storage.PutObject(
    'documents/reports/hello.txt',
    content,
    'text/plain',
    { author: 'John Doe', department: 'Engineering' }
    );

    // Get file metadata without downloading content
    const metadata = await storage.GetObjectMetadata({
    fullPath: 'documents/reports/hello.txt'
    });
    console.log('File metadata:', metadata);

    // Download file content
    const fileContent = await storage.GetObject({
    fullPath: 'documents/reports/hello.txt'
    });
    console.log('File content:', fileContent.toString('utf8'));

    // Copy a file
    await storage.CopyObject(
    'documents/reports/hello.txt',
    'documents/archive/hello-backup.txt'
    );

    // Check if a file exists
    const exists = await storage.ObjectExists('documents/reports/hello.txt');

    // Check if a directory exists
    const dirExists = await storage.DirectoryExists('documents/reports/');

    // Delete a directory and all its contents
    await storage.DeleteDirectory('documents/reports/', true);
    }

    Transfer files between different storage providers server-side:

    import { copyObjectBetweenProviders } from '@memberjunction/storage';

    const result = await copyObjectBetweenProviders(
    sourceProviderEntity,
    destProviderEntity,
    'documents/report.pdf',
    'imported/report.pdf',
    {
    sourceUserContext: { userID: currentUser.ID, contextUser },
    destinationUserContext: { userID: currentUser.ID, contextUser }
    }
    );

    if (result.success) {
    console.log(`Transferred ${result.bytesTransferred} bytes`);
    }
    flowchart LR
        Source["Source Provider
    (e.g. Dropbox)"] -->|"GetObject()"| Server["MJ Server
    (in-memory Buffer)"] Server -->|"PutObject()"| Dest["Destination Provider
    (e.g. Google Drive)"] style Source fill:#2d6a9f,stroke:#1a4971,color:#fff style Server fill:#b8762f,stroke:#8a5722,color:#fff style Dest fill:#2d8659,stroke:#1a5c3a,color:#fff

    Providers with native search capabilities support the SearchFiles method:

    import { FileStorageBase, UnsupportedOperationError } from '@memberjunction/storage';

    try {
    // Simple search
    const results = await storage.SearchFiles('quarterly report');

    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 filtered = await storage.SearchFiles('budget 2024', {
    fileTypes: ['pdf', 'docx'],
    modifiedAfter: new Date('2024-01-01'),
    pathPrefix: 'documents/finance/',
    maxResults: 50,
    searchContent: true
    });

    if (filtered.hasMore) {
    console.log(`Total matches: ${filtered.totalMatches}`);
    }
    } catch (error) {
    if (error instanceof UnsupportedOperationError) {
    console.log('This provider does not support file search');
    }
    }

    Search across multiple providers or accounts in parallel:

    import { searchAcrossProviders, searchAcrossAccounts } from '@memberjunction/storage';

    // Search across multiple providers
    const providerResults = await searchAcrossProviders(
    [googleDriveProvider, dropboxProvider, boxProvider],
    'quarterly report',
    {
    maxResultsPerProvider: 25,
    fileTypes: ['pdf', 'docx'],
    providerUserContexts: new Map([
    [googleDriveProvider.ID, { userID: currentUser.ID, contextUser }],
    [dropboxProvider.ID, { userID: currentUser.ID, contextUser }]
    ])
    }
    );

    for (const pr of providerResults.providerResults) {
    if (pr.success) {
    console.log(`${pr.providerName}: ${pr.results.length} results`);
    } else {
    console.log(`${pr.providerName}: ${pr.errorMessage}`);
    }
    }

    // Enterprise: Search across multiple accounts (including multiple accounts of same type)
    const accountResults = await searchAcrossAccounts(
    [
    { accountEntity: researchDropbox, providerEntity: dropboxProvider },
    { accountEntity: marketingDropbox, providerEntity: dropboxProvider },
    { accountEntity: engineeringGDrive, providerEntity: gdriveProvider }
    ],
    'quarterly report',
    {
    maxResultsPerAccount: 25,
    fileTypes: ['pdf', 'docx'],
    contextUser: currentUser
    }
    );

    console.log(`Total results: ${accountResults.totalResultsReturned}`);
    console.log(`Successful: ${accountResults.successfulAccounts}`);
    console.log(`Failed: ${accountResults.failedAccounts}`);

    Storage providers can be configured via the mj.config.cjs file at the repository root. The configuration is validated using Zod schemas at load time.

    module.exports = {
    storageProviders: {
    aws: {
    accessKeyID: 'your-key',
    secretAccessKey: 'your-secret',
    region: 'us-east-1',
    defaultBucket: 'my-bucket',
    keyPrefix: '/'
    },
    azure: {
    accountName: 'your-account',
    accountKey: 'your-key',
    connectionString: 'optional-conn-string',
    defaultContainer: 'my-container'
    },
    googleCloud: {
    projectID: 'your-project',
    keyFilename: '/path/to/keyfile.json',
    keyJSON: '{"type":"service_account",...}',
    defaultBucket: 'my-bucket'
    },
    googleDrive: {
    clientID: 'your-client-id',
    clientSecret: 'your-client-secret',
    refreshToken: 'your-refresh-token',
    rootFolderID: 'optional-root-folder'
    },
    dropbox: {
    accessToken: 'your-access-token',
    refreshToken: 'your-refresh-token',
    clientID: 'your-app-key',
    clientSecret: 'your-app-secret',
    rootPath: '/optional/root'
    },
    box: {
    clientID: 'your-client-id',
    clientSecret: 'your-client-secret',
    accessToken: 'your-access-token',
    refreshToken: 'your-refresh-token',
    enterpriseID: 'your-enterprise-id',
    rootFolderID: '0'
    },
    sharePoint: {
    clientID: 'your-client-id',
    clientSecret: 'your-client-secret',
    tenantID: 'your-tenant-id',
    siteID: 'your-site-id',
    driveID: 'your-drive-id',
    rootFolderID: 'optional-root-folder'
    }
    }
    };

    Each provider also supports environment variable configuration. Environment variables take lower priority than config file values (config file wins if both are present).

    Variable Description
    STORAGE_AWS_ACCESS_KEY_ID AWS access key ID
    STORAGE_AWS_SECRET_ACCESS_KEY AWS secret access key
    STORAGE_AWS_REGION AWS region (e.g., us-east-1)
    STORAGE_AWS_BUCKET_NAME S3 bucket name
    STORAGE_AWS_KEY_PREFIX Key prefix (defaults to /)
    Variable Description
    STORAGE_AZURE_ACCOUNT_NAME Storage account name
    STORAGE_AZURE_ACCOUNT_KEY Storage account key
    STORAGE_AZURE_CONNECTION_STRING Connection string (alternative to name/key)
    STORAGE_AZURE_CONTAINER or STORAGE_AZURE_DEFAULT_CONTAINER Container name
    Variable Description
    STORAGE_GOOGLE_KEY_JSON JSON string of service account credentials
    STORAGE_GOOGLE_CLOUD_KEY_FILENAME Path to service account key file
    STORAGE_GOOGLE_BUCKET_NAME or STORAGE_GOOGLE_CLOUD_DEFAULT_BUCKET GCS bucket name
    STORAGE_GOOGLE_CLOUD_PROJECT_ID Google Cloud project ID
    Variable Description
    STORAGE_GOOGLE_DRIVE_CLIENT_ID OAuth client ID
    STORAGE_GOOGLE_DRIVE_CLIENT_SECRET OAuth client secret
    STORAGE_GOOGLE_DRIVE_REFRESH_TOKEN OAuth refresh token
    STORAGE_GOOGLE_DRIVE_REDIRECT_URI OAuth redirect URI
    STORAGE_GDRIVE_ROOT_FOLDER_ID Root folder ID (optional)
    STORAGE_GDRIVE_KEY_FILE Service account key file (legacy)
    STORAGE_GDRIVE_CREDENTIALS_JSON Service account credentials JSON (legacy)
    Variable Description
    STORAGE_SHAREPOINT_CLIENT_ID Azure AD client ID
    STORAGE_SHAREPOINT_CLIENT_SECRET Azure AD client secret
    STORAGE_SHAREPOINT_TENANT_ID Azure AD tenant ID
    STORAGE_SHAREPOINT_SITE_ID SharePoint site ID
    STORAGE_SHAREPOINT_DRIVE_ID Document library drive ID
    STORAGE_SHAREPOINT_ROOT_FOLDER_ID Root folder ID (optional)
    Variable Description
    STORAGE_DROPBOX_ACCESS_TOKEN Dropbox access token
    STORAGE_DROPBOX_REFRESH_TOKEN Dropbox refresh token
    STORAGE_DROPBOX_CLIENT_ID or STORAGE_DROPBOX_APP_KEY App key
    STORAGE_DROPBOX_CLIENT_SECRET or STORAGE_DROPBOX_APP_SECRET App secret
    STORAGE_DROPBOX_ROOT_PATH Root path (optional)
    Variable Description
    STORAGE_BOX_CLIENT_ID Box client ID
    STORAGE_BOX_CLIENT_SECRET Box client secret
    STORAGE_BOX_ACCESS_TOKEN Box access token
    STORAGE_BOX_REFRESH_TOKEN Box refresh token
    STORAGE_BOX_ENTERPRISE_ID Box enterprise ID (for JWT auth)
    STORAGE_BOX_ROOT_FOLDER_ID Root folder ID (optional)
    type CreatePreAuthUploadUrlPayload = {
    UploadUrl: string; // Pre-authenticated URL for upload
    ProviderKey?: string; // Optional provider-specific key for future reference
    };
    type GetObjectParams = {
    objectId?: string; // Provider-specific ID (preferred for performance)
    fullPath?: string; // Full path to the object (fallback)
    };
    type 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>;
    };
    type StorageListResult = {
    objects: StorageObjectMetadata[]; // Files found
    prefixes: string[]; // Directories found
    };
    type 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, unknown>;// Provider-specific options
    };
    type 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
    objectId?: string; // Provider-specific ID for direct access
    customMetadata?: Record<string, string>;
    providerData?: Record<string, unknown>;
    };
    type FileSearchResultSet = {
    results: FileSearchResult[]; // Array of matching files
    totalMatches?: number; // Total matches (if available)
    hasMore: boolean; // More results available?
    nextPageToken?: string; // Token for next page
    };
    interface StorageProviderConfig {
    accountId?: string; // FileStorageAccount ID (multi-tenant tracking)
    accountName?: string; // Account display name (logging)
    [key: string]: unknown; // Provider-specific configuration values
    }

    Custom error class thrown when a provider does not support a specific operation (e.g., SearchFiles on AWS S3).

    All storage providers implement these methods:

    Method Returns Description
    initialize(config?) Promise<void> Initialize the driver. Always call after construction.
    CreatePreAuthUploadUrl(objectName) Promise<CreatePreAuthUploadUrlPayload> Generate pre-authenticated upload URL
    CreatePreAuthDownloadUrl(objectName) Promise<string> Generate pre-authenticated download URL
    MoveObject(oldName, newName) Promise<boolean> Move/rename a file
    DeleteObject(objectName) Promise<boolean> Delete a file
    ListObjects(prefix, delimiter?) Promise<StorageListResult> List files and directories
    CreateDirectory(directoryPath) Promise<boolean> Create a directory
    DeleteDirectory(path, recursive?) Promise<boolean> Delete a directory
    GetObjectMetadata(params) Promise<StorageObjectMetadata> Get file metadata without downloading
    GetObject(params) Promise<Buffer> Download full file content into memory
    get SupportsStreaming boolean Whether the provider supports GetObjectStream (all seven built-in drivers: true)
    GetObjectStream(params) Promise<ObjectStreamResult> Stream file content (optionally a byte Range) without buffering; throws StreamingNotSupportedError if unsupported. See docs/STREAMING.md
    PutObject(name, data, contentType?, metadata?) Promise<boolean> Upload file content directly
    CopyObject(source, destination) Promise<boolean> Copy a file
    ObjectExists(objectName) Promise<boolean> Check if a file exists
    DirectoryExists(directoryPath) Promise<boolean> Check if a directory exists
    SearchFiles(query, options?) Promise<FileSearchResultSet> Search files (throws UnsupportedOperationError if not supported)
    get IsConfigured boolean Check if driver is properly configured
    get AccountId string | undefined Get the associated account ID
    get AccountName string | undefined Get the associated account name

    High-level functions that integrate with MemberJunction's entity system:

    Function Description
    createUploadUrl(provider, input, userContext?) Create pre-authenticated upload URL with automatic MIME type detection
    createDownloadUrl(provider, keyOrName, userContext?) Create pre-authenticated download URL
    moveObject(provider, oldKey, newKey, userContext?) Move a file within a provider
    copyObject(provider, source, destination, userContext?) Copy a file within a provider
    deleteObject(provider, keyOrName, userContext?) Delete a file
    listObjects(provider, prefix, delimiter?, userContext?) List files and directories
    copyObjectBetweenProviders(source, dest, sourcePath, destPath, options?) Transfer file between providers
    searchAcrossProviders(providers, query, options?) Search multiple providers in parallel
    searchAcrossAccounts(accounts, query, options) Search multiple accounts in parallel (enterprise)
    initializeDriverWithAccountCredentials(options) Initialize driver using enterprise credential model
    initializeDriverWithUserCredentials(options) Initialize driver with user context (deprecated)
    Function Description
    getStorageConfig() Get full storage configuration (loads from mj.config.cjs on first call)
    getStorageProvidersConfig() Get just the storage providers configuration
    getProviderConfig(provider) Get configuration for a specific provider by key
    clearStorageConfig() Clear cached configuration (useful for testing)

    The library is designed to be extensible. To add a new storage provider:

    import {
    FileStorageBase,
    StorageProviderConfig,
    StorageObjectMetadata,
    StorageListResult,
    CreatePreAuthUploadUrlPayload,
    GetObjectParams,
    GetObjectMetadataParams,
    FileSearchOptions,
    FileSearchResultSet,
    } from '@memberjunction/storage';
    import { RegisterClass } from '@memberjunction/global';

    @RegisterClass(FileStorageBase, 'My Custom Storage')
    export class MyCustomStorage extends FileStorageBase {
    protected readonly providerName = 'My Custom Storage';

    private _apiKey: string | undefined;
    private _isConfigured = false;

    constructor() {
    super();
    // Load from environment variables
    this._apiKey = process.env.STORAGE_MYCUSTOM_API_KEY;
    }

    public async initialize(config?: StorageProviderConfig): Promise<void> {
    await super.initialize(config); // Sets accountId and accountName
    if (config) {
    // Override env var defaults with config values if present
    if (config.apiKey) this._apiKey = config.apiKey as string;
    }
    this._isConfigured = !!this._apiKey;
    }

    public get IsConfigured(): boolean {
    return this._isConfigured;
    }

    // Implement all abstract methods...

    public async SearchFiles(query: string, options?: FileSearchOptions): Promise<FileSearchResultSet> {
    // If not supported:
    this.throwUnsupportedOperationError('SearchFiles');
    }
    }

    Add to src/index.ts:

    export * from './drivers/MyCustomStorage';
    
    classDiagram
        class FileStorageBase {
            <>
            #providerName: string
            #_accountId: string
            #_accountName: string
            +initialize(config?) Promise~void~
            +get IsConfigured() boolean
            +get AccountId() string
            +get AccountName() string
            +CreatePreAuthUploadUrl(name) Promise
            +CreatePreAuthDownloadUrl(name) Promise
            +MoveObject(old, new) Promise
            +DeleteObject(name) Promise
            +ListObjects(prefix, delimiter?) Promise
            +CreateDirectory(path) Promise
            +DeleteDirectory(path, recursive?) Promise
            +GetObjectMetadata(params) Promise
            +GetObject(params) Promise
            +PutObject(name, data, type?, meta?) Promise
            +CopyObject(source, dest) Promise
            +ObjectExists(name) Promise
            +DirectoryExists(path) Promise
            +SearchFiles(query, options?) Promise
            #throwUnsupportedOperationError(method) never
        }
    
        class AWSFileStorage {
            @RegisterClass 'AWS S3'
        }
        class AzureFileStorage {
            @RegisterClass 'Azure Blob Storage'
        }
        class GoogleFileStorage {
            @RegisterClass 'Google Cloud Storage'
        }
        class GoogleDriveFileStorage {
            @RegisterClass 'Google Drive Storage'
        }
        class SharePointFileStorage {
            @RegisterClass 'SharePoint'
        }
        class DropboxFileStorage {
            @RegisterClass 'Dropbox'
        }
        class BoxFileStorage {
            @RegisterClass 'Box'
        }
    
        FileStorageBase <|-- AWSFileStorage
        FileStorageBase <|-- AzureFileStorage
        FileStorageBase <|-- GoogleFileStorage
        FileStorageBase <|-- GoogleDriveFileStorage
        FileStorageBase <|-- SharePointFileStorage
        FileStorageBase <|-- DropboxFileStorage
        FileStorageBase <|-- BoxFileStorage
    

    Thrown when a provider does not support a specific operation:

    try {
    await storage.SearchFiles('quarterly report');
    } catch (error) {
    if (error instanceof UnsupportedOperationError) {
    console.log(`Provider doesn't support search: ${error.message}`);
    // Fall back to ListObjects with client-side filtering
    }
    }

    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({ fullPath: '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');
    }
    }

    The package includes Jest-based unit tests for the base class behavior and the enterprise credential initialization flow.

    # Run tests
    npm test

    # Run tests in watch mode
    npm run test:watch

    # Run tests with coverage
    npm run test:coverage

    Test files are located at:

    • src/__tests__/FileStorageBase.test.ts -- Tests for the FileStorageBase abstract class, initialize() method, and account information handling
    • src/__tests__/util.test.ts -- Tests for initializeDriverWithAccountCredentials and the enterprise credential model
    # Build the package
    npm run build

    # Watch mode for development
    npm run watch

    The package uses TypeScript with tsc and tsc-alias for path alias resolution. Output is emitted to the dist/ directory.

    1. Always call initialize(): After creating a provider instance, always call initialize() before using the driver, even if no configuration is needed.
    2. Use ProviderKey: Always check for and use ProviderKey if returned by CreatePreAuthUploadUrl for subsequent operations.
    3. Use objectId when available: The GetObject and GetObjectMetadata methods accept either objectId or fullPath. Using objectId bypasses path resolution and is significantly faster.
    4. Use enterprise credential model: Prefer initializeDriverWithAccountCredentials over direct instantiation for multi-tenant applications.
    5. Handle unsupported operations: Wrap SearchFiles calls in try/catch for UnsupportedOperationError since not all providers support search.
    6. Directory paths: Always use trailing slashes for directory paths (e.g., documents/ not documents).
    7. Content types: Always specify content types for better browser handling and security.
    8. Buffer operations: GetObject and PutObject load entire files into memory; consider pre-authenticated URLs for large files.
    9. Batch searches: Use searchAcrossProviders or searchAcrossAccounts for parallel multi-source search instead of sequential calls.

    ISC

    Classes

    AWSFileStorage
    AzureFileStorage
    BoxFileStorage
    DropboxFileStorage
    FileStorageBase
    FileStorageEngine
    GoogleDriveFileStorage
    GoogleFileStorage
    SharePointFileStorage
    StreamingNotSupportedError
    UnsupportedOperationError

    Interfaces

    AccountSearchInput
    AccountSearchResult
    AccountStorageDriverOptions
    CopyBetweenProvidersOptions
    CopyBetweenProvidersResult
    ExtendedUserContextOptions
    MultiAccountSearchResult
    MultiProviderSearchResult
    OAuthStorageConfig
    ProviderSearchResult
    SearchAcrossAccountsOptions
    SearchAcrossProvidersOptions
    StorageProviderConfig
    UploadFileOptions
    UploadFileResult
    UserContextOptions
    UserStorageDriverOptions

    Type Aliases

    ByteRange
    CreatePreAuthUploadUrlPayload
    FileSearchOptions
    FileSearchResult
    FileSearchResultSet
    GetObjectMetadataParams
    GetObjectParams
    GetObjectStreamParams
    ObjectStreamResult
    StorageConfig
    StorageListResult
    StorageObjectMetadata
    StorageProvidersConfig
    TokenRefreshCallback

    Functions

    BuildHttpRangeHeader
    clearStorageConfig
    copyObject
    copyObjectBetweenProviders
    createDownloadUrl
    createUploadUrl
    deleteObject
    getProviderConfig
    getStorageConfig
    getStorageProvidersConfig
    initializeDriverWithAccountCredentials
    initializeDriverWithUserCredentials
    listObjects
    moveObject
    searchAcrossAccounts
    searchAcrossProviders