Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 114
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MigrateFilesToBlob
0.00% covered (danger)
0.00%
0 / 114
0.00% covered (danger)
0.00%
0 / 6
930
0.00% covered (danger)
0.00%
0 / 1
 handle
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 dryRun
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 executeOptimizedMigration
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 migrateFileWithLowMemory
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 readFileInChunks
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 formatBytes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2// app/Console/Commands/MigrateFilesToBlob.php
3
4namespace App\Console\Commands;
5
6use Illuminate\Console\Command;
7use Illuminate\Support\Facades\Storage;
8use Illuminate\Support\Facades\File;
9use Illuminate\Support\Facades\DB;
10use App\Models\TblFiles;
11use Exception;
12
13class MigrateFilesToBlob extends Command
14{
15    protected $signature = 'files:migrate-to-blob
16                            {--chunk=20 : Procesar en chunks más pequeños}
17                            {--dry-run : Solo simular}
18                            {--skip-large : Saltar archivos > 10MB}
19                            {--memory-limit=128 : Límite de memoria en MB}';
20
21    protected $description = 'Migrar archivos físicos a BLOB optimizado para memoria';
22
23    public function handle()
24    {
25        try {
26            $dbName = DB::connection()->getDatabaseName();
27            $recordsCount = DB::table('tbl_files')->count();
28            $this->info("✅ Conectado a: {$dbName} - Registros: " . number_format($recordsCount));
29        } catch (Exception $e) {
30            $this->error("❌ Error BD: " . $e->getMessage());
31            exit(1);
32        }
33
34        // Establecer límite de memoria
35        ini_set('memory_limit', $this->option('memory-limit') . 'M');
36
37        $this->info('🧠 Modo optimizado para memoria: ' . ini_get('memory_limit'));
38
39        if ($this->option('dry-run')) {
40            return $this->dryRun();
41        }
42
43        return $this->executeOptimizedMigration();
44    }
45
46    private function dryRun()
47    {
48        $this->info('🔍 MODO SIMULACIÓN - Analizando archivos para limpiar/generar...');
49
50        $files = TblFiles::whereNotNull('filename')
51            ->whereNotNull('file') // Buscamos los que TIENEN blob
52            ->limit(10) // Solo primeros 10 para prueba
53            ->get();
54
55        $this->info("📁 Probando con " . $files->count() . " archivos...");
56
57        foreach ($files as $index => $file) {
58            // Diferentes rutas posibles
59            $possiblePaths = [
60                storage_path('app/public/uploads/' . $file->filename),
61                base_path('storage/app/public/uploads/' . $file->filename),
62                '/var/www/html/storage/app/public/uploads/' . $file->filename,
63                public_path('uploads/' . $file->filename),
64            ];
65
66            $this->info("Archivo {$index}{$file->filename}");
67
68            $found = false;
69            foreach ($possiblePaths as $path) {
70                $exists = File::exists($path);
71                $this->info("   {$path}" . ($exists ? '✅ EXISTE' : '❌ NO EXISTE'));
72
73                if ($exists) {
74                    $found = true;
75                    $size = File::size($path);
76                    $this->info("      Tamaño físico: " . $this->formatBytes($size));
77                    $this->info("      🗑️  SE ELIMINARÍA EL BLOB DE LA BD (Ya existe físico)");
78                    break;
79                }
80            }
81
82            if (!$found) {
83                $this->info("      ✨ NO EXISTE FÍSICO - SE GENERARÍA DESDE EL BLOB");
84                $this->info("      💾 Tamaño BLOB: " . $this->formatBytes(strlen($file->file)));
85                $this->info("      🗑️  LUEGO SE ELIMINARÍA EL BLOB DE LA BD");
86            }
87
88            $this->info('');
89        }
90
91        return 0;
92    }
93
94    private function executeOptimizedMigration()
95    {
96        $this->info('🚀 Iniciando migración/limpieza optimizada...');
97
98        // Obtener IDs en lugar de objetos completos para ahorrar memoria
99        $fileIds = TblFiles::whereNotNull('filename')
100            ->whereNotNull('file') // Buscamos los que TIENEN blob
101            ->pluck('file_id');
102
103        $this->info("📁 Archivos a procesar: " . number_format($fileIds->count()));
104
105        $successCount = 0;
106        $generatedCount = 0;
107        $errorCount = 0;
108
109        $chunks = $fileIds->chunk($this->option('chunk'));
110
111        foreach ($chunks as $chunkIndex => $chunk) {
112            $this->info("🔄 Chunk " . ($chunkIndex + 1) . "/" . $chunks->count() .
113                " - Memoria: " . $this->formatBytes(memory_get_usage(true)));
114
115            foreach ($chunk as $fileId) {
116                try {
117                    $result = $this->migrateFileWithLowMemory($fileId);
118
119                    if ($result === 'cleaned') {
120                        $successCount++;
121                    } elseif ($result === 'generated_and_cleaned') {
122                        $generatedCount++;
123                    } else {
124                        $errorCount++;
125                    }
126
127                } catch (Exception $e) {
128                    $errorCount++;
129                    $this->error("❌ ID {$fileId}" . $e->getMessage());
130                }
131
132                // Liberar memoria después de cada archivo
133                gc_collect_cycles();
134            }
135
136            $this->info("   ✅ Progreso: {$successCount} limpiados (existían), {$generatedCount} generados, {$errorCount} errores");
137
138            // Pausa más larga entre chunks
139            sleep(1);
140        }
141
142        $this->info("🎉 Proceso completado: {$successCount} limpiados, {$generatedCount} generados y limpiados, {$errorCount} errores");
143        return 0;
144    }
145
146    private function migrateFileWithLowMemory($fileId)
147    {
148        // 1. Verificar si existe físico primero (sin traer el blob aún)
149        $fileInfo = TblFiles::where('file_id', $fileId)
150            ->select('file_id', 'filename')
151            ->first();
152
153        if (!$fileInfo) {
154            return 'error';
155        }
156
157        $filePath = storage_path('app/public/uploads/' . $fileInfo->filename);
158        $directory = dirname($filePath);
159
160        // Si existe el archivo físico
161        if (File::exists($filePath)) {
162            // Solo limpiamos el blob
163            DB::table('tbl_files')
164                ->where('file_id', $fileId)
165                ->update(['file' => null]);
166            
167            return 'cleaned';
168        }
169
170        // Si NO existe, necesitamos traer el blob para crearlo
171        $fileWithBlob = TblFiles::where('file_id', $fileId)
172            ->select('file')
173            ->first();
174            
175        if (!$fileWithBlob || empty($fileWithBlob->file)) {
176            $this->error("❌ ID {$fileId}: No tiene archivo físico NI blob válido.");
177            return 'error';
178        }
179
180        // Asegurar que el directorio existe
181        if (!File::exists($directory)) {
182            File::makeDirectory($directory, 0755, true);
183        }
184
185        // Escribir el archivo
186        $written = File::put($filePath, $fileWithBlob->file);
187
188        if ($written === false) {
189            $this->error("❌ ID {$fileId}: No se pudo escribir el archivo en disco.");
190            return 'error';
191        }
192
193        // Verificar que se escribió correctamente
194        if (File::exists($filePath) && File::size($filePath) > 0) {
195            // Limpiar el blob
196            DB::table('tbl_files')
197                ->where('file_id', $fileId)
198                ->update(['file' => null]);
199                
200            $this->info("   ✨ Generado: {$fileInfo->filename}");
201            return 'generated_and_cleaned';
202        }
203
204        return 'error';
205    }
206
207    private function readFileInChunks($filePath)
208    {
209        $content = '';
210        $handle = fopen($filePath, 'rb');
211
212        if ($handle) {
213            while (!feof($handle)) {
214                $content .= fread($handle, 8192); // 8KB chunks
215
216                // Liberar memoria periódicamente
217                if (strlen($content) > (5 * 1024 * 1024)) { // Cada 5MB
218                    gc_collect_cycles();
219                }
220            }
221            fclose($handle);
222        }
223
224        return $content;
225    }
226
227    private function formatBytes($bytes): string
228    {
229        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
230        $bytes = max($bytes, 0);
231        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
232        $pow = min($pow, count($units) - 1);
233        $bytes /= pow(1024, $pow);
234
235        return round($bytes, 2) . ' ' . $units[$pow];
236    }
237}