> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage Manager

> Introduction to the file storage system

Sonamu provides a unified file storage system based on **Flydrive**. You can use the local file system (fs) and AWS S3 with the same interface, and manage multiple disks as needed.

## Why Storage Manager?

### The Problem

```typescript theme={null}
// ❌ Different APIs for each driver
if (env === 'local') {
  fs.writeFileSync('uploads/avatar.png', buffer);
  url = '/uploads/avatar.png';
} else {
  await s3.putObject({
    Bucket: 'my-bucket',
    Key: 'uploads/avatar.png',
    Body: buffer
  });
  url = 'https://s3.amazonaws.com/my-bucket/uploads/avatar.png';
}
```

**Problems**:

* Different code for each environment
* Code changes required when switching drivers
* URL generation logic is scattered

### Using Storage Manager

```typescript theme={null}
// ✅ Unified interface
await Sonamu.storage.use().put('uploads/avatar.png', buffer);
const url = await Sonamu.storage.use().getUrl('uploads/avatar.png');
```

**Benefits**:

* Driver-independent code
* Switch drivers by just changing configuration
* Consistent URL generation

## Core Concepts

### Storage Manager

A manager that handles multiple **Disks**.

```mermaid theme={null}
graph TD
    SM[Storage Manager] --> D1[Disk: fs<br/>Local file system]
    SM --> D2[Disk: s3<br/>AWS S3]
    SM --> D3[Disk: r2<br/>Cloudflare R2]

    D1 --> F1[/uploads/avatar.png]
    D2 --> F2[s3://bucket/uploads/avatar.png]
    D3 --> F3[r2://bucket/uploads/avatar.png]
```

### Disk

A **Disk** is a single driver instance that stores files.

```typescript theme={null}
// Use default disk
const disk = Sonamu.storage.use();

// Use specific disk
const s3Disk = Sonamu.storage.use('s3');
const fsDisk = Sonamu.storage.use('fs');
```

## Basic Configuration

### sonamu.config.ts

```typescript theme={null}
import { drivers, type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  server: {
    storage: {
      default: 'fs',  // Default disk
      drivers: {
        fs: drivers.fs({
          location: './uploads',  // Storage location
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),
      }
    }
  }
};
```

**Configuration elements**:

* `default`: Name of the default disk to use
* `drivers`: Driver factories by disk name

## Using Disks

### Default Disk

```typescript theme={null}
// Use default disk (config.default)
const disk = Sonamu.storage.use();

// Save file
await disk.put('avatar.png', buffer);

// Get URL
const url = await disk.getUrl('avatar.png');
// => '/uploads/avatar.png' (fs)
// => 'https://bucket.s3.amazonaws.com/avatar.png' (s3)
```

### Specific Disk

```typescript theme={null}
// fs disk
const fsDisk = Sonamu.storage.use('fs');
await fsDisk.put('temp/file.txt', 'content');

// s3 disk
const s3Disk = Sonamu.storage.use('s3');
await s3Disk.put('backup/file.txt', 'content');
```

## Managing Multiple Disks

### Multi-Disk Configuration

```typescript theme={null}
export const config: SonamuConfig = {
  server: {
    storage: {
      default: process.env.STORAGE_DISK || 'fs',
      drivers: {
        // Local development
        fs: drivers.fs({
          location: './uploads',
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),

        // Production (S3)
        s3: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
          },
          region: process.env.AWS_REGION!,
          bucket: process.env.AWS_BUCKET!,
          urlBuilder: {
            generateURL: (key) =>
              `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}`,
          }
        }),

        // Public files
        public: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
          },
          region: process.env.AWS_REGION!,
          bucket: process.env.AWS_PUBLIC_BUCKET!,
          urlBuilder: {
            generateURL: (key) =>
              `https://${process.env.AWS_PUBLIC_BUCKET}.s3.amazonaws.com/${key}`,
          }
        }),
      }
    }
  }
};
```

### Using Disks by Purpose

```typescript theme={null}
class UserModelClass extends BaseModelClass {
  // Profile photos → public disk
  @upload()
  async uploadAvatar() {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    const url = await file.saveToDisk(
      'public',
      `avatars/${Sonamu.getContext().user?.id}.png`
    );
    return url;
  }

  // Private documents → default disk
  @upload()
  async uploadDocument() {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    const url = await file.saveToDisk(
      'fs',
      `documents/${Sonamu.getContext().user?.id}/${file.filename}`
    );
    return url;
  }

  // Backup → separate disk
  async backupData(data: any) {
    const json = JSON.stringify(data);
    await Sonamu.storage
      .use('backup')
      .put(`backups/${Date.now()}.json`, json);
  }
}
```

## Environment-based Strategies

### Development/Production Separation

<Tabs>
  <Tab title="Development Environment" icon="laptop-code">
    **Using local file system**

    ```typescript theme={null}
    // .env.development
    STORAGE_DISK=fs

    // sonamu.config.ts
    storage: {
      default: process.env.STORAGE_DISK || 'fs',
      drivers: {
        fs: drivers.fs({
          location: './uploads',
          urlBuilder: {
            generateURL: (key) => `/uploads/${key}`,
          }
        }),
      }
    }
    ```

    **Benefits**:

    * No AWS account needed
    * Fast development
    * No cost
  </Tab>

  <Tab title="Production Environment" icon="server">
    **Using S3**

    ```typescript theme={null}
    // .env.production
    STORAGE_DISK=s3
    AWS_ACCESS_KEY_ID=AKIA...
    AWS_SECRET_ACCESS_KEY=...
    AWS_REGION=ap-northeast-2
    AWS_BUCKET=my-app-uploads

    // sonamu.config.ts
    storage: {
      default: process.env.STORAGE_DISK || 's3',
      drivers: {
        s3: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
          },
          region: process.env.AWS_REGION!,
          bucket: process.env.AWS_BUCKET!,
        }),
      }
    }
    ```

    **Benefits**:

    * Unlimited capacity
    * CDN integration
    * Reliable service
  </Tab>
</Tabs>

### Hybrid Strategy

```typescript theme={null}
export const config: SonamuConfig = {
  server: {
    storage: {
      default: 's3',
      drivers: {
        // Main storage: S3
        s3: drivers.s3({ /* ... */ }),

        // Temporary files: Local
        fs: drivers.fs({
          location: './temp',
          urlBuilder: {
            generateURL: (key) => `/temp/${key}`,
          }
        }),
      }
    }
  }
};

// Usage
class FileModelClass extends BaseModelClass {
  // Permanent storage → S3
  @upload({ consume: 'stream', destination: 's3' })
  async upload() {
    const { uploadedFiles } = Sonamu.getContext();
    return uploadedFiles?.[0]?.url;  // S3 URL (already streamed)
  }

  // Temporary processing → Local
  @upload()
  async processTempFile() {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0];
    const tempPath = `temp/${Date.now()}.tmp`;
    await file.saveToDisk('fs', tempPath);  // Local

    // Delete after processing
    await Sonamu.storage.use('fs').delete(tempPath);
  }
}
```

## Practical Examples

### 1. Profile Photo Upload

```typescript theme={null}
class UserModelClass extends BaseModelClass {
  @upload()
  async uploadAvatar(ctx: Context) {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0]; // Use first file

    if (!file) {
      throw new Error('No file provided');
    }

    // Filename: user-{id}-{timestamp}.{ext}
    const filename = `user-${ctx.user.id}-${Date.now()}.${file.extname}`;
    const key = `avatars/${filename}`;

    // Save to default disk
    const url = await file.saveToDisk('fs', key);

    // Update DB
    await this.updateOne(['id', ctx.user.id], { avatar_url: url });

    return { url };
  }
}
```

### 2. Multiple File Upload

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  @upload()
  async uploadImages(postId: number) {
    const { bufferedFiles } = Sonamu.getContext();

    const urls = await Promise.all(
      (bufferedFiles ?? []).map(async (file, index) => {
        const key = `posts/${postId}/image-${index}.${file.extname}`;
        return await file.saveToDisk('fs', key);
      })
    );

    return { urls };
  }
}
```

### 3. Separation by Disk

```typescript theme={null}
class FileModelClass extends BaseModelClass {
  @upload()
  async upload(type: 'public' | 'private') {
    const { bufferedFiles } = Sonamu.getContext();
    const file = bufferedFiles?.[0]; // Use first file

    if (!file) {
      throw new Error('No file provided');
    }

    // Use different disk based on type
    const diskName = type === 'public' ? 'public' : 'fs';
    const key = `${type}/${Date.now()}-${file.filename}`;

    const url = await file.saveToDisk(diskName, key);

    return { url, type };
  }
}
```

## Storage Manager API

### use(diskName?)

Returns a disk instance.

```typescript theme={null}
// Default disk
const disk = Sonamu.storage.use();

// Specific disk
const s3 = Sonamu.storage.use('s3');
const fs = Sonamu.storage.use('fs');
```

**Return value**: `Disk` instance (Flydrive)

### defaultDisk

Returns the default disk name.

```typescript theme={null}
const defaultName = Sonamu.storage.defaultDisk;
// => 'fs' or 's3', etc.
```

## Disk API (Flydrive)

The Disk returned by Storage Manager is a Flydrive Disk instance.

### Key Methods

```typescript theme={null}
const disk = Sonamu.storage.use();

// Save file
await disk.put('path/to/file.txt', 'content');
await disk.put('path/to/file.txt', buffer);

// Read file
const content = await disk.get('path/to/file.txt');

// Check file existence
const exists = await disk.exists('path/to/file.txt');

// Delete file
await disk.delete('path/to/file.txt');

// Get URL
const url = await disk.getUrl('path/to/file.txt');

// Get Signed URL (S3)
const signedUrl = await disk.getSignedUrl('path/to/file.txt', {
  expiresIn: '1h'
});
```

## Cautions

<Warning>
  **Cautions when using Storage Manager**:

  1. **Environment variables required**: AWS credentials needed for S3
     ```typescript theme={null}
     // ❌ No hardcoding
     credentials: {
       accessKeyId: 'AKIA...',
       secretAccessKey: '...',
     }

     // ✅ Use environment variables
     credentials: {
       accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
       secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
     }
     ```

  2. **Disk name typos**: Error when using non-existent disk
     ```typescript theme={null}
     // ❌ Typo
     Sonamu.storage.use('S3')  // Error!

     // ✅ Correct name
     Sonamu.storage.use('s3')
     ```

  3. **Path separator**: Use `/` for platform independence
     ```typescript theme={null}
     // ✅ Always use forward slash
     await disk.put('uploads/avatar.png', buffer);

     // ❌ Don't use backslash
     await disk.put('uploads\\avatar.png', buffer);
     ```

  4. **URL builder required**: fs driver requires urlBuilder configuration
     ```typescript theme={null}
     // ❌ No urlBuilder
     fs: drivers.fs({
       location: './uploads',
     })

     // ✅ urlBuilder configured
     fs: drivers.fs({
       location: './uploads',
       urlBuilder: {
         generateURL: (key) => `/uploads/${key}`,
       }
     })
     ```

  5. **Lazy Initialization**: Disks are initialized on first use
     ```typescript theme={null}
     // Configuration errors are discovered when use() is called
     try {
       const disk = Sonamu.storage.use('s3');
     } catch (error) {
       // Invalid S3 configuration, etc.
     }
     ```
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Storage Drivers" icon="hard-drive" href="/en/advanced-features/storage/storage-drivers">
    Detailed fs, S3 driver configuration
  </Card>

  <Card title="File Operations" icon="upload" href="/en/advanced-features/storage/file-operations">
    File upload and download
  </Card>

  <Card title="URL Builder" icon="link" href="/en/advanced-features/storage/url-builder">
    Generating file URLs
  </Card>
</CardGroup>
