# PBX Components Export/Import System

## Overview
This document provides instructions for exporting PBX system components (IVR, Queues, MOH, Recordings, Inbound Routes, Time Conditions, Time Groups) into a portable .zip file that can be imported on another FreePBX/Asterisk server.

## Export Process

### 1. Database Tables to Export

The following tables contain the PBX component configurations:

#### Core Components
- **IVR (Interactive Voice Response)**:
  - `ivr_details` - IVR configuration and settings
  - `ivr_entries` - IVR menu options and destinations

- **Queues**:
  - `queues_config` - Queue configuration (agents, timeouts, announcements)
  - `queues_details` - Queue member assignments

- **Music on Hold (MOH)**:
  - `music` - MOH category definitions and settings

- **System Recordings**:
  - `recordings` - System recording metadata (filenames, descriptions)

- **Announcements**:
  - `announcement` - Announcement configurations and settings
  - Related recordings (automatically included when announcements reference system recordings)

- **Inbound Routes (DIDs)**:
  - `incoming` - Inbound route configurations and destinations

- **Time Conditions**:
  - `timeconditions` - Time-based routing rules

- **Time Groups**:
  - `timegroups_details` - Time group definitions (time ranges)
  - `timegroups_groups` - Time group metadata

- **Outbound Routes**:
  - `outbound_routes` - Outbound route configurations and trunk assignments

- **Trunks**:
  - `trunks` - Trunk configurations (SIP, IAX, DAHDI, etc.)

### 2. File System Components

#### Audio Files Locations
- **MOH Files**: `/var/lib/asterisk/moh/` (only custom categories)
- **System Recordings**: `/var/lib/asterisk/sounds/custom/` (only custom recordings)
- **IVR Prompts**: `/var/lib/asterisk/sounds/custom/` (only custom prompts)

**Note**: The export script automatically skips default FreePBX sound files (language packs like `en/`, `es/`, `fr/`, etc.) and only exports custom content to avoid duplicating files that are already installed on target servers.

**Dependency Handling**: Some components automatically include related components. For example, exporting `announcements` will also export `recordings` since announcements reference system recordings.

### 3. Export Script Implementation

Create a Laravel Artisan command for the export process:

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use ZipArchive;

class ExportPbxComponents extends Command
{
    protected $signature = 'pbx:export {--components= : Comma-separated list of components to export}';
    protected $description = 'Export PBX components to a zip file for migration';

    // Component definitions with their database tables and file paths
    private $components = [
        'ivr' => [
            'tables' => ['ivr_details', 'ivr_entries'],
            'files' => ['/var/lib/asterisk/sounds/'],
            'file_filter' => 'custom'  // Only export custom directory
        ],
        'queues' => [
            'tables' => ['queues_config', 'queues_details'],
            'files' => []
        ],
        'moh' => [
            'tables' => ['music'],
            'files' => ['/var/lib/asterisk/moh/'],
            'file_filter' => 'custom'  // Only export custom MOH categories
        ],
        'recordings' => [
            'tables' => ['recordings'],
            'files' => ['/var/lib/asterisk/sounds/'],
            'file_filter' => 'custom'  // Only export custom recordings
        ],
        'announcements' => [
            'tables' => ['announcement'],
            'files' => [],  // Files are handled through recordings relationship
            'dependencies' => ['recordings']  // Depends on recordings for audio files
        ],
        'inbound_routes' => [
            'tables' => ['incoming'],
            'files' => []
        ],
        'time_conditions' => [
            'tables' => ['timeconditions'],
            'files' => []
        ],
        'time_groups' => [
            'tables' => ['timegroups_details', 'timegroups_groups'],
            'files' => []
        ]
    ];

    public function handle()
    {
        $componentsToExport = $this->option('components')
            ? explode(',', $this->option('components'))
            : array_keys($this->components);

        $exportPath = storage_path('app/pbx_export_' . date('Y_m_d_H_i_s'));
        $zipFile = $exportPath . '.zip';

        // Create export directory
        if (!file_exists($exportPath)) {
            mkdir($exportPath, 0755, true);
        }

        $this->info('Starting PBX components export...');

        foreach ($componentsToExport as $component) {
            if (!isset($this->components[$component])) {
                $this->error("Unknown component: {$component}");
                continue;
            }

            $this->info("Exporting {$component}...");
            $this->exportComponent($component, $exportPath);
        }

        // Create metadata file
        $this->createMetadataFile($exportPath, $componentsToExport);

        // Create zip file
        $this->createZipFile($exportPath, $zipFile);

        // Cleanup
        $this->removeDirectory($exportPath);

        $this->info("Export completed successfully!");
        $this->info("Zip file created: {$zipFile}");
        $this->info("File size: " . $this->formatBytes(filesize($zipFile)));

        return 0;
    }

    private function exportComponent($component, $exportPath)
    {
        $config = $this->components[$component];
        $componentPath = $exportPath . '/' . $component;

        mkdir($componentPath, 0755, true);

        // Export database tables
        foreach ($config['tables'] as $table) {
            $this->exportTable($table, $componentPath);
        }

        // Export associated files
        foreach ($config['files'] as $filePath) {
            if (is_dir($filePath)) {
                $this->exportDirectory($filePath, $componentPath);
            }
        }
    }

    private function exportTable($table, $exportPath)
    {
        try {
            $data = DB::connection('asterisk')->table($table)->get();

            if ($data->count() > 0) {
                $filename = $exportPath . '/' . $table . '.json';
                file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT));
                $this->info("Exported table: {$table} ({$data->count()} records)");
            } else {
                $this->warn("Table {$table} is empty, skipping...");
            }
        } catch (\Exception $e) {
            $this->error("Failed to export table {$table}: " . $e->getMessage());
        }
    }

    private function exportDirectory($sourceDir, $exportPath)
    {
        $targetDir = $exportPath . '/' . basename($sourceDir);

        if (!is_dir($targetDir)) {
            mkdir($targetDir, 0755, true);
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $targetFile = $targetDir . '/' . $iterator->getSubPathName();
                $targetFileDir = dirname($targetFile);

                if (!is_dir($targetFileDir)) {
                    mkdir($targetFileDir, 0755, true);
                }

                copy($file->getPathname(), $targetFile);
            }
        }

        $this->info("Exported directory: {$sourceDir}");
    }

    private function createMetadataFile($exportPath, $components)
    {
        $metadata = [
            'export_date' => date('Y-m-d H:i:s'),
            'export_server' => gethostname(),
            'freepbx_version' => $this->getFreepbxVersion(),
            'asterisk_version' => $this->getAsteriskVersion(),
            'components' => $components,
            'tables_exported' => $this->getAllTables($components),
            'total_records' => $this->getTotalRecords($components)
        ];

        file_put_contents(
            $exportPath . '/metadata.json',
            json_encode($metadata, JSON_PRETTY_PRINT)
        );
    }

    private function createZipFile($sourcePath, $zipFile)
    {
        $zip = new ZipArchive();

        if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
            throw new \Exception("Cannot create zip file: {$zipFile}");
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            $zip->addFile($file->getPathname(), $iterator->getSubPathName());
        }

        $zip->close();
    }

    private function removeDirectory($dir)
    {
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($iterator as $file) {
            if ($file->isDir()) {
                rmdir($file->getPathname());
            } else {
                unlink($file->getPathname());
            }
        }

        rmdir($dir);
    }

    private function getFreepbxVersion()
    {
        try {
            $version = DB::connection('asterisk')->table('modules')
                ->where('rawname', 'framework')
                ->value('version');
            return $version ?: 'Unknown';
        } catch (\Exception $e) {
            return 'Unknown';
        }
    }

    private function getAsteriskVersion()
    {
        try {
            $output = shell_exec('asterisk -V 2>/dev/null');
            return trim($output) ?: 'Unknown';
        } catch (\Exception $e) {
            return 'Unknown';
        }
    }

    private function getAllTables($components)
    {
        $tables = [];
        foreach ($components as $component) {
            if (isset($this->components[$component]['tables'])) {
                $tables = array_merge($tables, $this->components[$component]['tables']);
            }
        }
        return array_unique($tables);
    }

    private function getTotalRecords($components)
    {
        $total = 0;
        $tables = $this->getAllTables($components);

        foreach ($tables as $table) {
            try {
                $count = DB::connection('asterisk')->table($table)->count();
                $total += $count;
            } catch (\Exception $e) {
                // Skip tables that can't be accessed
            }
        }

        return $total;
    }

    private function formatBytes($bytes)
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        return round($bytes, 2) . ' ' . $units[$i];
    }
}
```

### 4. Usage Examples

#### Export All Components
```bash
php artisan pbx:export
```

#### Export Specific Components
```bash
php artisan pbx:export --components=ivr,queues,moh,announcements,outbound_routes,trunks
```

#### Export Only Database Tables (No Audio Files)
```bash
php artisan pbx:export --components=ivr,queues,inbound_routes,time_conditions,time_groups,outbound_routes,trunks
```

### 5. Export File Structure

The generated .zip file will have the following structure:

```
pbx_export_2025_11_17_14_30_00.zip
├── metadata.json                    # Export information and statistics
├── ivr/
│   ├── ivr_details.json            # IVR configurations
│   ├── ivr_entries.json            # IVR menu options
│   └── sounds/custom/              # Only custom IVR audio files
├── queues/
│   ├── queues_config.json          # Queue settings
│   └── queues_details.json         # Queue members
├── moh/
│   ├── music.json                  # MOH categories
│   └── moh/                        # Only custom MOH audio files
├── recordings/
│   ├── recordings.json             # Recording metadata
│   └── sounds/custom/              # Only custom recording files
├── announcements/
│   ├── announcement.json           # Announcement configurations
│   ├── announcement_recordings.json # Related recording metadata (if any)
│   └── sounds/custom/              # Audio files for announcement recordings (if any)
├── inbound_routes/
│   └── incoming.json               # Inbound route configurations
├── time_conditions/
│   └── timeconditions.json         # Time-based routing
└── time_groups/
    ├── timegroups_details.json     # Time range definitions
    └── timegroups_groups.json      # Time group metadata
└── outbound_routes/
    └── outbound_routes.json        # Outbound route configurations
└── trunks/
    └── trunks.json                 # Trunk configurations
```

**Important**: Only custom audio files are exported. Default FreePBX sound packages (language directories like `en/`, `es/`, `fr/`, etc.) are automatically excluded since they should already be installed on target servers.

### 6. Metadata File Content

The `metadata.json` file contains:

```json
{
    "export_date": "2025-11-17 14:30:00",
    "export_server": "pbx-server-01",
    "freepbx_version": "15.0.17.24",
    "asterisk_version": "Asterisk 18.10.0",
    "components": ["ivr", "queues", "moh", "recordings", "announcements", "inbound_routes", "time_conditions", "time_groups", "outbound_routes", "trunks"],
    "tables_exported": ["ivr_details", "ivr_entries", "queues_config", "queues_details", "music", "recordings", "announcement", "incoming", "timeconditions", "timegroups_details", "timegroups_groups", "outbound_routes", "trunks"],
    "total_records": 245,
    "php_version": "8.2.28",
    "laravel_version": "8.83.29",
    "export_filters": {
        "ivr": "custom",
        "moh": "custom",
        "recordings": "custom",
        "announcements": "dependencies: recordings"
    }
}
```

The `export_filters` section shows which components use file filtering to exclude default FreePBX content.

### 6.1 File Filtering Logic

The export system automatically filters out default FreePBX content to avoid exporting files that should already exist on target servers:

- **IVR, Recordings, MOH**: Only files in `custom/` directory or language-specific custom directories (`*/custom/`) are exported
- **Excluded Directories**: Language packs (`en/`, `es/`, `fr/`, etc.) and standard directories (`digits/`, `letters/`, etc.) except for their custom subdirectories
- **Database Tables**: All configuration data is exported regardless of whether it's default or custom

This approach significantly reduces export size and import time while ensuring that custom content is properly migrated.

### 7. Database Connection Configuration

The export/import system uses the default Laravel database connection with cross-database access to the asterisk database. The system automatically prefixes table names with `asterisk.` to access FreePBX tables.

**No additional database configuration is required** - the default connection must have access to both the main application database and the asterisk database.

### 8. Command Registration

The export and import commands are registered in `app/Console/Kernel.php`:

```php
protected $commands = [
    // ... other commands
    \App\Console\Commands\ExportPbxComponents::class,
    \App\Console\Commands\ImportPbxComponents::class,
];
```

### 9. Security Considerations

- **File Permissions**: Ensure the web user can read audio files
- **Database Access**: Use read-only database user for export
- **File Size Limits**: Large audio files may require PHP memory limit increases
- **Storage Space**: Ensure sufficient space for export directory and zip file

### 10. Troubleshooting

#### Common Issues

1. **Database Connection Failed**
   - Verify asterisk database credentials
   - Check if MySQL server is running
   - Ensure user has SELECT permissions on asterisk database

2. **File Access Denied**
   - Check permissions on `/var/lib/asterisk/` directories
   - Ensure web user (www-data/apache) can read audio files

3. **Memory Limit Exceeded**
   - Increase PHP memory_limit for large exports
   - Export components individually if needed

4. **Zip Creation Failed**
   - Check available disk space
   - Ensure zip extension is installed
   - Verify write permissions on storage directory

#### Verification Steps

After export, verify the zip file contains expected data:

```bash
# Check zip contents
unzip -l pbx_export_*.zip

# Verify JSON files are valid
python3 -m json.tool metadata.json

# Check file counts
find extracted_dir -type f -name "*.json" -exec wc -l {} \;
```

### 11. Performance Optimization

For large systems with many audio files:

- **Chunked Export**: Export components in batches
- **File Filtering**: Only include used audio files
- **Compression**: Use higher compression for audio files
- **Progress Bar**: Add progress indicators for long exports

### 12. Backup Strategy

- **Pre-Export Backup**: Always backup the source system before export
- **Test Import**: Test the import process on a development system first
- **Version Control**: Keep track of export versions and changes
- **Documentation**: Document any custom configurations or dependencies

This import system provides a complete migration solution when used together with the export command.

## Import Process

### 1. Import Command

The `pbx:import` command imports PBX components from a zip file created by the export command:

```bash
php artisan pbx:import --file=/path/to/pbx_export.zip
```

### 2. Import Options

#### Import All Components
```bash
php artisan pbx:import --file=/path/to/pbx_export.zip
```

#### Import Specific Components
```bash
php artisan pbx:import --file=/path/to/pbx_export.zip --components=ivr,queues,trunks
```

#### Import Database Tables Only (Skip Audio Files)
```bash
php artisan pbx:import --file=/path/to/pbx_export.zip --components=ivr,queues,inbound_routes,outbound_routes,trunks
```

### 3. Import Process Flow

1. **Validation**: Verifies the zip file exists and contains valid metadata
2. **Extraction**: Extracts the zip file to a temporary directory
3. **Metadata Reading**: Reads export information and component list
4. **Component Processing**: Imports each component in order:
   - Database tables (with existing data truncation)
   - Associated audio files
   - Special handling for announcements with recordings
5. **Cleanup**: Removes temporary files

### 4. Import Behavior

- **Database Tables**: Existing data is truncated before importing new data
- **Audio Files**: Files are copied to their appropriate directories
- **Dependencies**: Components with dependencies are handled automatically
- **Error Handling**: Import continues with other components if one fails
- **Progress Feedback**: Shows progress bar and detailed status messages

### 5. Import Metadata

The import process reads the same metadata as the export:

```json
{
    "export_date": "2025-11-17 13:05:07",
    "export_server": "source-server",
    "freepbx_version": "15.0.17.24",
    "asterisk_version": "Asterisk 18.10.0",
    "components": ["ivr", "queues", "trunks"],
    "tables_exported": ["ivr_details", "ivr_entries", "queues_config", "queues_details", "trunks"],
    "total_records": 45,
    "export_filters": {
        "ivr": "custom",
        "queues": null
    }
}
```

### 6. Import Command Registration

The import command is registered in `app/Console/Kernel.php`:

```php
protected $commands = [
    // ... other commands
    \App\Console\Commands\ImportPbxComponents::class,
];
```

### 7. Import Troubleshooting

#### Common Import Issues

1. **Zip File Not Found**
   - Ensure the file path is correct and accessible
   - Check file permissions

2. **Database Connection Failed**
   - Verify asterisk database credentials in `.env`
   - Ensure MySQL server is running

3. **Table Import Failed**
   - Check database user permissions (needs INSERT, TRUNCATE)
   - Verify table structure matches export

4. **File Import Failed**
   - Check write permissions on audio directories
   - Ensure sufficient disk space

5. **Memory Issues**
   - Large imports may require increased PHP memory_limit
   - Import components individually if needed

#### Import Verification

After import, verify the data was imported correctly:

```bash
# Check database records
php artisan tinker
>>> DB::connection('asterisk')->table('trunks')->count()
>>> DB::connection('asterisk')->table('ivr_details')->count()

# Check audio files
ls -la /var/lib/asterisk/sounds/custom/
ls -la /var/lib/asterisk/moh/
```

### 8. Import Safety Considerations

- **Backup First**: Always backup the target system before importing
- **Test Environment**: Test imports on development systems first
- **Component Selection**: Import specific components to avoid conflicts
- **Version Compatibility**: Ensure source and target FreePBX versions are compatible
- **File Permissions**: Ensure proper permissions on imported files

### 9. Import vs Export Feature Comparison

| Feature | Export | Import |
|---------|--------|--------|
| Zip File Handling | Creates zip | Extracts zip |
| Database Operation | SELECT | TRUNCATE + INSERT |
| File Operation | Copy to zip | Copy from zip |
| Dependencies | Adds required | Handles automatically |
| Progress Feedback | Progress bar | Progress bar |
| Error Handling | Continues on error | Continues on error |
| Metadata | Creates | Reads and validates |

This import system provides a complete migration solution when used together with the export command.